利用原生JS实现类似浏览器查找高亮功能

最近在完成 前端资源导航(fe.congm.in) 时,想要增加一个类似浏览器ctrl+f查找并该高亮的功能时,花了一些的时间和精力,在此进行一点总结:

需求

.content中有许多.box,需要在.box中找出搜索的字符串,再将无搜索结果的.box隐藏掉,此外还要将字符串高亮。这个页面用vue.js实现了数据交互,不想用jquery来实现得查找高亮功能,得用原生js来实现该功能。

原理

将各个.box的文本内容提取,利用正则判断是否匹配字符串,若无搜索字符串,则隐藏该.box;否则继续找到具体的与该字符串匹配的结点及文本。通过对父节点的childNodes,然后利用nodeType筛选出文本结点,并利用匹配字符串将该文本结点分割,然后给匹配字符串加上<span class="highlight">,同时将原文本结点放入<template>中暂存,最后一起拼接后插入替换匹配结点。 更改或删除搜索的字符串时,将去掉.highlight,遍历所有的.highlight,找到里面父节点中的<template>,将<template>内容与父节点的内容替换,从而达到复原的效果。

思路&代码

// 原生事件监听
document.querySelector("search").addEventListener('input', function(e){  
  var target = e.target,
      value = target.value;  // value即搜索的字符串
  var reg = new RegExp(value, "gi"),  // 匹配大小写
      content = document.querySelector(".content"),
      box = document.querySelectorAll(".box"),
      count = box.length;
  rmHighlight(content);  // 清除.content内之前添加的.highlight (见下文)
  for(var i = 0; i < count; i++){  // 遍历.box
    box[i].classList.remove("hidden");  // 清除之前隐藏的.box
    if(value.length == 0){
      continue;  // 如果搜索的字符串为空,不进行下列操作
    }
    var range = box[i].querySelectorAll(".section-heading, .list-title, .item-name"),  // .box内目标结点范围
        rangeLen = range.length,
        rangeText = '';
    for(var j = 0; j < rangeLen; j++){
      if(range[j].innerText.match(reg)){
        highlight(range[j], value);  // 目标结点匹配则执行highlight函数 (见下文)
      }
      rangeText += range[j].innerText + '\n';  //拼接.box范围内的文本
    }
    if(!rangeText.match(reg)){  //判断拼接文本是否匹配,从而对.box进行隐藏
      box[i].classList.add("hidden");
    }else{
      box[i].classList.remove("hidden");
    }
  }
}
// highlight函数
function highlight(el, value){  
  var childList = el.childNodes,  // 找到子节点
    childCount = childList.length;
  if(!childCount || !value.length){
    return;  // 无子节点或无查询值则结束函数
  }
  var reg = new RegExp(value, "gi");
  for(var i = 0; i < childCount; i++){  // 遍历子节点
    var oChild = childList[i];
    if(oChild.nodeType == 1 && oChild.classList && !oChild.classList.contains('highlight') && !/(script|style|template)/i.test(oChild.tagName)){
      // 如果子节点是元素结点,而且class没有.highlight,而且元素标签不是script或style或template,则递归该函数
      highlight(oChild, value);
    }else if(oChild.nodeType == 3){
      // 如果子节点是文本结点,则对该文本进行操作
      var highlightList = oChild.data.match(reg);  // 得出文本结点匹配出的字符串数组
      if(highlightList){
        var highlightSplit = oChild.data.split(reg),  // 得出被搜索字符串分割出的文本结点数组
            highlightSplitLen = highlightSplit.length;
        var highlightHtml = '';
        for(var j = 0; j < highlightSplitLen; j++){
          // 遍历分割的文本结点数组,将匹配出的字符串加上.highlight并依次插入
          var highlightSpan = (j < highlightSplitLen - 1) ? ('<span class="highlight">' + highlightList[j] + '</span>') : ('<template>' + oChild.data +'</template>');  // 在拼接的字符串末尾加上文本结点原本的内容
          highlightHtml += highlightSplit[j] + highlightSpan;
        }
        oChild.parentNode.innerHTML = highlightHtml;  // 将拼接的字符串结果替换文本结点的父节点内容
      }
    }
  }
}
// 移除.highlight函数
function rmHighlight(el) {  
  var highlightSpans = el.querySelectorAll("span.highlight");  // 找到所有.highlight并遍历
  for(var i = 0; i < highlightSpans.length; i++){
    if(highlightSpans[i].parentNode){
    highlightSpans[i].parentNode.innerHTML = highlightSpans[i].parentNode.querySelector("template").innerHTML;  // 找到父节点中的template,将自己内容替换为template内容
    }
  }
}

完整代码参见 dev-nav (Github)