vue實現(xiàn)搜索并高亮文字的兩種方式總結(jié)
在做文字處理的項目時經(jīng)常會遇到搜索文字并高亮的需求,常見的實現(xiàn)方式有插入標簽和貼標簽兩種。這兩種方式適用于不同的場景,各有優(yōu)劣。為了方便操作,直接起一個Vue項目,在里面演示。
插入標簽的方式
簡單做一個布局,handleSearch 中放主要邏輯
<script setup>
import { ref } from 'vue'
const text = ref('豫章故郡,洪都新府。星分翼軫,地接衡廬。襟三江而帶五湖,控蠻荊而引甌越。物華天寶,龍光射牛斗之墟;人杰地靈,徐孺下陳蕃之榻。雄州霧列,俊采星馳。臺隍枕夷夏之交,賓主盡東南之美。都督閻公之雅望,棨戟遙臨;宇文新州之懿范,襜帷暫駐。十旬休假,勝友如云;千里逢迎,高朋滿座。騰蛟起鳳,孟學士之詞宗;紫電青霜,王將軍之武庫。家君作宰,路出名區(qū);童子何知,躬逢勝餞。')
const search = ref('')
const handleSearch = () => {
console.log(search.value)
}
</script>
<template>
<div class="editor">{{ text }}</div>
<input type="text" v-model="search">
<button @click="handleSearch">搜索</button>
</template>
<style scoped>
.editor {
width: 200px;
height: 200px;
border: 1px solid #ddd;
overflow: auto;
}
</style>
補充 handleSearch 的處理邏輯:
const handleSearch = () => {
const regExp = new RegExp(search.value, 'g')
text.value = text.value.replace(regExp, `<span style="background: yellow;">${search.value}</span>`)
}
用輸入框中的內(nèi)容創(chuàng)建一個正則,然后將內(nèi)容做替換,外面裹上 span 標簽并加背景顏色。
對 editor 稍作修改,否則標簽渲染不出來
<div class="editor" v-html="text"></div>
于是就實現(xiàn)了預(yù)期:

然而在有些業(yè)務(wù)場景中被搜索的區(qū)域會是 contenteditable 可編輯區(qū)域,如果再使用插入標簽的方式會污染原文,這時這種方式就行不通了。
貼標簽的方式
這種方式需要兩個前置的知識儲備,一個是 Document.createRange() ,該方法用以創(chuàng)建一個包含節(jié)點與文本節(jié)點的一部分的文檔片段。另一個是 Range.getBoundingClientRect() ,雖然是一個實驗中的方法,但是主流瀏覽器基本都支持,該方法會返回一個 DOMRect 對象,包含8個屬性,文檔中有詳細的介紹,在此就不贅述了。
對頁面稍作修改:
<script setup>
import { ref, watch, onMounted } from 'vue'
const text = ref('豫章故郡,洪都新府。星分翼軫,地接衡廬。襟三江而帶五湖,控蠻荊而引甌越。物華天寶,龍光射牛斗之墟;人杰地靈,徐孺下陳蕃之榻。雄州霧列,俊采星馳。臺隍枕夷夏之交,賓主盡東南之美。都督閻公之雅望,棨戟遙臨;宇文新州之懿范,襜帷暫駐。十旬休假,勝友如云;千里逢迎,高朋滿座。騰蛟起鳳,孟學士之詞宗;紫電青霜,王將軍之武庫。家君作宰,路出名區(qū);童子何知,躬逢勝餞。')
const search = ref('')
const highlight = ref([])
const editorRef = ref(null)
const wrapperRef = ref(null)
const handleSearch = () => {
}
</script>
<template>
<div class="container">
<div class="wrapper" ref="wrapperRef">
<div class="editor" ref="editorRef" contenteditable>{{ text }}</div>
<div class="highlight"></div>
</div>
</div>
<input type="text" v-model="search">
<button @click="handleSearch">搜索</button>
</template>
<style scoped>
.container {
width: 200px;
height: 200px;
border: 1px solid #ddd;
overflow: auto;
}
.wrapper {
position: relative;
}
.highlight {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
z-index: -1;
}
</style>
增加了一個 highlight 框,用來存放高亮的塊, highlight 數(shù)組用來存放需要高亮的塊的位置信信息。
補充搜索函數(shù)中的邏輯
const len = search.value.length
const regExp = new RegExp(search.value, 'g')
const textNode = editorRef.value.firstChild
let result = null
while (result = regExp.exec(text.value)) {
const { index } = result
const range = document.createRange()
range.setStart(textNode, index)
range.setEnd(textNode, index + len)
const rangeReact = range.getBoundingClientRect()
highlight.value.push(rangeReact)
}
將要搜索的詞創(chuàng)建一個正則,并獲取文本框的文字節(jié)點。用 exec 來遍歷原文內(nèi)容,這樣可以實現(xiàn)全文搜索并得到搜索信息,拿到 index 屬性。此時就用到了前面提到的 createRange ,在文本結(jié)點根據(jù)起始位置和長度創(chuàng)建一個選中區(qū)域,并獲取選中區(qū)域的dom信息,將它們存放到一個數(shù)組中。此時可以拿到一個dom信息的數(shù)組:

可以用這個數(shù)組渲染高亮塊:
<div class="highlight">
<span
v-for="item in highlight"
class="tag"
:style="{
left: item.left + 'px',
top: item.top + 'px',
width: item.width + 'px',
height: item.height + 'px' }"></span>
</div>
增加對應(yīng)的樣式:
.tag {
position: fixed;
background: yellow;
}
這里使用 fixed 的原因是得到的距離信息時是相對于文檔,而不是父元素。但是這種方式是不可靠的,因為例子中可編輯區(qū)域是可以滾動的,一滾動高亮區(qū)域就錯位了:

所以還是要采用相對父元素定位,其實實現(xiàn)方式很簡單,先算出父元素相對于頁面的定位,再用剛才得出的距離詳見,最后得出高亮標簽相對于父元素的定位。畫個簡單的示意圖:

如圖所示,想得到距離3也就是高亮標簽相對于父元素的距離,就是距離2減去距離1。
const wrapperInfo = ref({})
onMounted(() => {
wrapperInfo.value = wrapperRef.value.getBoundingClientRect()
})
在mounted狀態(tài)下獲取父元素的信息。
封裝一個計算位置信息的函數(shù),并修改搜索函數(shù),獲取 rangeReact 后增加和修改代碼:
const calRectInfo = (rangeReact) => {
let rectInfo = {}
rectInfo.width = rangeReact.width
rectInfo.height = rangeReact.height
rectInfo.left = rangeReact.left - wrapperInfo.value.left
rectInfo.top = rangeReact.top - wrapperInfo.value.top
return rectInfo
}
highlight.value.push(calRectInfo(rangeReact))
這時就可以把定位改為 position: absolute; 了。
此時再滾動樣式也不會錯亂:

但是當被搜索詞在跨行時會出現(xiàn)bug:

搜索“星分翼軫“,然而兩行都被高亮了,通過調(diào)試可以看出它并不會很智能的分塊返回,所以這段邏輯就需要手動去實現(xiàn)。

首先是如何知道需要高亮的區(qū)域是多行。從 DOMReact 中得以得到需要高亮的行高,如果知道一行的高度,就可以知道是不是多行了。
const standardRange = document.createRange() standardRange.setStart(textNode, 0) standardRange.setEnd(textNode, 0) const standardRangeReact = standardRange.getBoundingClientRect() const lineHeight = standardRangeReact.height
在空白處創(chuàng)建一個 range ,就可以得到行高。然后根據(jù)行高判斷兩種情況:
if (rangeReact.height === lineHeight) {
highlight.value.push(calRectInfo(rangeReact))
} else {
// 多行的情況
}
多行的情況可以用雙指針來試,還以“星分翼軫”為例,設(shè)置 i = 0; j = 1; ,截取文字得到“星”,計算高度信息,然后 j++; 得到“星分”,當文字為“星分翼”的時候,行高變?yōu)閮尚校瑒t應(yīng)高亮“星分”。然后將 i 設(shè)置為 j - 1 。繼續(xù)重復之前的操作。
let i = 0
let j = 1
while (j <= len) {
const subRange = document.createRange()
subRange.setStart(textNode, result.index + i)
subRange.setEnd(textNode, result.index + j)
const subRangeReact = subRange.getBoundingClientRect()
if (subRangeReact.height === lineHeight) {
if (j !== 1) highlight.value.pop()
j++
} else {
i = j - 1
}
highlight.value.push(calRectInfo(subRangeReact))
}
每次不管是否是最終的結(jié)果都要把計算結(jié)果 push 到 highlight 中, 當后面的結(jié)果可以覆蓋前面的時候則再 pop 出來。 if (j !== 1) 的判斷是因為第一次截取時不應(yīng)該把以前的結(jié)果也刪除掉。
此時再進行搜索,可以折行顯示了。

但其實還有不盡如人意的地方,因為這個框是可編輯區(qū)域,當插入新的文字時高亮框不會實時改變,留在了原地。

此時我們要監(jiān)聽文本框文字的改變
<div class="editor" ref="editorRef" contenteditable @input="handleChange">{{ text }}</div>
當文字改變時重新計算高亮區(qū)域。
const handleChange = () => {
text.value = editorRef.value.innerText
}
watch(text, () => {
highlight.value = []
handleSearch()
})
這樣當輸入文字時,高亮區(qū)域可以實時計算:

以上就是vue實現(xiàn)搜索并高亮文字的兩種方式總結(jié)的詳細內(nèi)容,更多關(guān)于vue高亮文字的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue中的$emit 與$on父子組件與兄弟組件的之間通信方式
本文主要對vue 用$emit 與 $on 來進行組件之間的數(shù)據(jù)傳輸。重點給大家介紹vue中的$emit 與$on父子組件與兄弟組件的之間通信方式,感興趣的朋友一起看看2018-05-05
Vue自定義v-has指令實現(xiàn)按鈕權(quán)限判斷
這篇文章主要給大家介紹了關(guān)于Vue自定義v-has指令實現(xiàn)按鈕權(quán)限判斷的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-04-04

