前端实现基于后端返回的文档内容关键字搜索高亮
需求背景及技术实现
针对上传的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';
}
}
由于需求背景不一样,可能不能完全复用,但思路和核心代码应该可以借鉴的 _ _