125 lines
3.8 KiB
TypeScript
125 lines
3.8 KiB
TypeScript
function walk(el: HTMLElement, filter: (node: HTMLElement) => boolean) {
|
||
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT)
|
||
const matchNodes = [] as HTMLElement[]
|
||
|
||
// 1、遍历节点
|
||
while (walker.nextNode()) {
|
||
const node = walker.currentNode as HTMLElement
|
||
|
||
filter(node) && matchNodes.push(node)
|
||
}
|
||
return matchNodes
|
||
}
|
||
|
||
function editNodes(el: HTMLElement, nodes: HTMLElement[], query: string) {
|
||
// 2、处理收集后的节点,字符串搜索性能优于正则替换
|
||
nodes.forEach((node) => {
|
||
const content = node.textContent as string
|
||
const start = content.indexOf(query)
|
||
const startText = content.substring(0, start)
|
||
const endText = content.substring(start + query.length)
|
||
|
||
// 前半段
|
||
node.textContent = startText
|
||
// 高亮节点
|
||
const span = document.createElement('span')
|
||
span.classList.add('tiny-hl-query-node')
|
||
span.textContent = query
|
||
// 后半段
|
||
const end = document.createTextNode(endText)
|
||
|
||
node.after(span, end)
|
||
})
|
||
|
||
// 3、如果此次有匹配值,则递归再处理一遍,防止一段文字内,出现了多次query的场景
|
||
if (nodes.length) {
|
||
edit(el, query)
|
||
}
|
||
}
|
||
|
||
function restoreNodes(nodes: HTMLElement[]) {
|
||
// node是文字节点, node.parentElement是 .tiny-hl-query-node节点, node.parentElement.parentElement 才是高亮节点的父节点
|
||
// 父节点可能是重复的
|
||
const parents = new Set()
|
||
nodes.forEach((node) => parents.add(node.parentElement?.parentElement))
|
||
|
||
parents.forEach((node: HTMLElement) => {
|
||
node.innerText = node.textContent
|
||
})
|
||
}
|
||
|
||
/** 查找所有文字节点,并编辑 */
|
||
function edit(el: HTMLElement, query: string) {
|
||
if (query) {
|
||
const matchNodes = walk(el, (node: HTMLElement) => {
|
||
// 忽略已高亮的节点
|
||
if ((node.parentElement as HTMLElement).classList.contains('tiny-hl-query-node')) {
|
||
return false
|
||
}
|
||
|
||
const content = node.textContent || ''
|
||
return content.includes(query)
|
||
})
|
||
|
||
editNodes(el, matchNodes, query)
|
||
}
|
||
}
|
||
|
||
/** 查找所有高亮的节点,并还原 */
|
||
function restore(el: HTMLElement) {
|
||
const matchNodes = walk(el, (node: HTMLElement) => {
|
||
return (node.parentElement as HTMLElement).classList.contains('tiny-hl-query-node')
|
||
})
|
||
|
||
restoreNodes(matchNodes)
|
||
}
|
||
|
||
const mounted = (el: HTMLElement, binding: { value: string }) => {
|
||
el.classList.toggle('tiny-hl-query', true)
|
||
|
||
const query = binding.value
|
||
edit(el, query)
|
||
}
|
||
|
||
const beforeUpdate = (el: HTMLElement) => {
|
||
restore(el)
|
||
}
|
||
|
||
const updated = (el: HTMLElement, binding: { value: string }) => {
|
||
const query = binding.value
|
||
edit(el, query)
|
||
}
|
||
|
||
/** 高亮搜索字的指令: 遍历dom内所有的文字节点,替换匹配为高亮节点
|
||
*@example
|
||
* <div v-highlight-query='test'>
|
||
* ......包含众多的dom, test .......
|
||
* </div>
|
||
* @example 输出为
|
||
* <div class="tiny-hl-query">
|
||
* .... <span class="tiny-hl-query-node">test</span> ....
|
||
* </div>
|
||
*
|
||
* 注意: 1、模板中,所有文字的地方需要使用同一个标签包裹起来,避免还原时误伤
|
||
* <div v-highlight-query='test'>
|
||
* 这里一段话 {{ somevalue}} <tiny-select>...</tiny-select>
|
||
* ^^^ ^^^
|
||
* </div>
|
||
* 这样的写法不合适,会造成文本节点与非文本节点共享一个父节点的。
|
||
* 需要将 ^^^ 之间的内容,用标签包裹一下
|
||
* 2、指令不能直接包含文字节点。 避免vue编译优化带来的一个问题。
|
||
* <div v-highlight-query='test'>
|
||
* {{ someValue }}
|
||
* </div>
|
||
* 需要用任意标签,比如 <span> 包裹一下 someValue
|
||
* */
|
||
export default {
|
||
mounted,
|
||
beforeUpdate,
|
||
updated,
|
||
// vue2周期
|
||
inserted: mounted,
|
||
update: beforeUpdate,
|
||
componentUpdated: updated
|
||
}
|