angular+TS实现搜索关键字高亮

前端实现基于后端返回的文档内容关键字搜索高亮

需求背景及技术实现

针对上传的word文档实现关键字搜索高亮 且需要通过向上向下查找按钮实现当前关键字位置高亮颜色不一样
后端返回文档的html内容
前端实现文档搜索关键字高亮 –>前端查找文档存在的关键字提取创建font标签包裹关键字并加上样式后放到原位置,开发自提优化屏幕滚动到当前高亮关键字的位置

注意点

1、关键字可能分散在不同的标签内,不做处理可能无法准备匹配搜索关键字
2、搜索结果出来后通过点击查找上一个或者下一个高亮处,不做处理会重复创建font标签
3、需要给自建的font标签加上自定义属性用于找到当前高亮关键字并滚动到视图开始位置

一、创建dom用于展示后端返回的html形式文档

<div class="html-content" [innerHtml]="fileInfo.fileContent | html"><div>

二、

public setHighLightText(): void {
	// 文档内容为空
    if (!this.fileInfo.fileContent) {
      return;
    }
    // 找到页面中渲染文档的dom元素
    const doms = document.querySelector('.html-content');
    // 遍历dom找出文本节点
    const textNodes = this._utilService.getTextNodeList(doms);
    const textList = this._utilService.getTextInfoList(textNodes);
    // 拼接文本内容
    const content = textList.map(({ text }) => text)
                      .join('');
    this.matchList = this._utilService.getMatchList(content, this.searchText);
    // 重置索引index值
    this.setRetrievalIndex();
    this._utilService.replaceMatchResult(textNodes, textList, this.matchList, this.retrievalIndex, 'html-content');

  }
/**
   * 遍历DOM树取出所有文本节点
   * @param dom dom节点
   * @returns 文本节点集合
   */
   // tslint:disable-next-line: no-any
   public getTextNodeList (dom: any): any[] {
    const nodeList = [...dom.childNodes];
    const textNodes = [];
    while (nodeList.length) {
      const node = nodeList.shift();
      if (node.nodeType === node.TEXT_NODE) {
        textNodes.push(node);
      } else {
        nodeList.unshift(...node.childNodes);
      }
    }
    return textNodes;
  }
  /**
   * 获取文本节点列表,可以取出所有文本内容并记录每个文本片段在拼接结果中的开始、结束索引
   * @param textNodes 文本节点集合
   * @returns 文本集合
   */
  // tslint:disable-next-line: no-any
  public getTextInfoList (textNodes: any[]): ITextData[] {
    let length = 0;
    const textList = textNodes.map(node => {
      const startIdx = length;
      const endIdx = length + node.wholeText.length;
      length = endIdx;
      return {
        text: node.wholeText,
        startIdx,
        endIdx
      };
    });
    return textList;
  }
  /**
   * 匹配关键词
   * @param content 拼接后的文本内容
   * @param keyword 关键字
   * @returns 匹配到的关键字列表
   */
  // tslint:disable-next-line: no-any
  public getMatchList (content: string, keyword: string): any[] {
    const characters = [...'\\[](){}?.+*^$:|'].reduce((r, c) => (r[c] = true, r), {});
    keyword = keyword.split('')
      .map(s => characters[s] ? `\\${s}` : s)
        .join('[\\s\\n]*');
    // g: 全局匹 m: 多行匹配 i:不区分大小写
    const reg = new RegExp(keyword, 'gm');
    const matchList = [];
    let match = reg.exec(content);
    while (match) {
      matchList.push(match);
      match = reg.exec(content);
    }
    return matchList;
  }
  /**
   * 重新设置检索文字index
   * @param total 匹配到的文字数组长度
   */
  public setRetrievalIndex(): void {
    const total = this.matchList && this.matchList.length;
    // this.retrievalIndex为文字检索当前位置
    if (this.retrievalIndex > total - 1) {
      this.retrievalIndex = 0;
      return;
    } else if (this.retrievalIndex < 0) {
      this.retrievalIndex = total - 1;
    }
  }
  /**
   * 关键词使用font标签替换
   * @param textNodes 文本节点
   * @param textList 文本以及文本气质索引
   * @param matchList 匹配到的关键字列表
   */
  // tslint:disable-next-line: no-any
  public replaceMatchResult (textNodes: any[], textList: ITextData[], matchList: any[],
     retrievalIndex: number, eleClassName: string): void {
    const fontNodes = document.querySelectorAll(`.${eleClassName} font`);
    if (fontNodes && fontNodes.length) {
      const validFontNodes = [];
      // tslint:disable-next-line: prefer-for-of
      for (let i = 0; i < fontNodes.length; i++) {
        if(fontNodes[i].innerHTML && fontNodes[i].getAttribute('retrieval') === 'retrieval') {
          validFontNodes.push(fontNodes[i]);
        }
      }
      if (validFontNodes && validFontNodes.length) {
        for (let i = 0; i < validFontNodes.length; i++) {
          this.setFontBgColor(i, retrievalIndex, validFontNodes[i], eleClassName);
        }
        return;
      }
    }
    // 对于每一个匹配结果,可能分散在多个标签中,找出这些标签,截取匹配片段并用font标签替换出
    for (let i = matchList.length - 1; i >= 0; i--) {
      const match = matchList[i];
      const matchStart = match.index;
      const matchEnd = matchStart + match[0].length; // 匹配结果在拼接字符串中的起止索引
      // 遍历文本信息列表,查找匹配的文本节点
      for (let textIdx = 0; textIdx < textList.length; textIdx++) {
        const { text, startIdx, endIdx } = textList[textIdx]; // 文本内容、文本在拼接串中开始、结束索引
        if (endIdx < matchStart) {
          continue;
        } // 匹配的文本节点还在后面
        if (startIdx >= matchEnd) {
          break;
        } // 匹配文本节点已经处理完了
        let textNode = textNodes[textIdx]; // 这个节点中的部分或全部内容匹配到了关键词,将匹配部分截取出来进行替换
        const nodeMatchStartIdx = Math.max(0, matchStart - startIdx); // 匹配内容在文本节点内容中的开始索引
        const nodeMatchLength = Math.min(endIdx, matchEnd) - startIdx - nodeMatchStartIdx; // 文本节点内容匹配关键词的长度
        if (nodeMatchStartIdx > 0) {
          textNode = textNode.splitText(nodeMatchStartIdx);
        } // textNode取后半部分
        if (nodeMatchLength < textNode.wholeText.length) {
          textNode.splitText(nodeMatchLength);
        }
        const font = document.createElement('font');
        font.setAttribute('retrieval', 'retrieval');
        font.innerText = text.substr(nodeMatchStartIdx, nodeMatchLength);
        font.style.verticalAlign = 'baseline';
        this.setFontBgColor(i, retrievalIndex, font, eleClassName);
        textNode.parentNode.replaceChild(font, textNode);
      }
    }
  }
  /**
   * 设置font背景色
   */
  public setFontBgColor(index: number, retrievalIndex: number, ele: HTMLElement, eleClassName: string): void {
    if (index === retrievalIndex) {
      ele.style.background = '#FF838E';
      const timer = setTimeout(() => {
        clearTimeout(timer);
        // 当前高亮滚动到可视区域开始位置
        ele.scrollIntoView();
      }, 0);
    } else {
      ele.style.background = '#FFDF66';
    }
  }

由于需求背景不一样,可能不能完全复用,但思路和核心代码应该可以借鉴的 _ _

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值