vue實(shí)現(xiàn)搜索并高亮文字的兩種方式總結(jié)
在做文字處理的項(xiàng)目時(shí)經(jīng)常會(huì)遇到搜索文字并高亮的需求,常見(jiàn)的實(shí)現(xiàn)方式有插入標(biāo)簽和貼標(biāo)簽兩種。這兩種方式適用于不同的場(chǎng)景,各有優(yōu)劣。為了方便操作,直接起一個(gè)Vue項(xiàng)目,在里面演示。
插入標(biāo)簽的方式
簡(jiǎn)單做一個(gè)布局,handleSearch
中放主要邏輯
<script setup> import { ref } from 'vue' const text = ref('豫章故郡,洪都新府。星分翼軫,地接衡廬。襟三江而帶五湖,控蠻荊而引甌越。物華天寶,龍光射牛斗之墟;人杰地靈,徐孺下陳蕃之榻。雄州霧列,俊采星馳。臺(tái)隍枕夷夏之交,賓主盡東南之美。都督閻公之雅望,棨戟遙臨;宇文新州之懿范,襜帷暫駐。十旬休假,勝友如云;千里逢迎,高朋滿(mǎn)座。騰蛟起鳳,孟學(xué)士之詞宗;紫電青霜,王將軍之武庫(kù)。家君作宰,路出名區(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>
補(bǔ)充 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)建一個(gè)正則,然后將內(nèi)容做替換,外面裹上 span
標(biāo)簽并加背景顏色。
對(duì) editor
稍作修改,否則標(biāo)簽渲染不出來(lái)
<div class="editor" v-html="text"></div>
于是就實(shí)現(xiàn)了預(yù)期:
然而在有些業(yè)務(wù)場(chǎng)景中被搜索的區(qū)域會(huì)是 contenteditable
可編輯區(qū)域,如果再使用插入標(biāo)簽的方式會(huì)污染原文,這時(shí)這種方式就行不通了。
貼標(biāo)簽的方式
這種方式需要兩個(gè)前置的知識(shí)儲(chǔ)備,一個(gè)是 Document.createRange() ,該方法用以創(chuàng)建一個(gè)包含節(jié)點(diǎn)與文本節(jié)點(diǎn)的一部分的文檔片段。另一個(gè)是 Range.getBoundingClientRect() ,雖然是一個(gè)實(shí)驗(yàn)中的方法,但是主流瀏覽器基本都支持,該方法會(huì)返回一個(gè) DOMRect 對(duì)象,包含8個(gè)屬性,文檔中有詳細(xì)的介紹,在此就不贅述了。
對(duì)頁(yè)面稍作修改:
<script setup> import { ref, watch, onMounted } from 'vue' const text = ref('豫章故郡,洪都新府。星分翼軫,地接衡廬。襟三江而帶五湖,控蠻荊而引甌越。物華天寶,龍光射牛斗之墟;人杰地靈,徐孺下陳蕃之榻。雄州霧列,俊采星馳。臺(tái)隍枕夷夏之交,賓主盡東南之美。都督閻公之雅望,棨戟遙臨;宇文新州之懿范,襜帷暫駐。十旬休假,勝友如云;千里逢迎,高朋滿(mǎn)座。騰蛟起鳳,孟學(xué)士之詞宗;紫電青霜,王將軍之武庫(kù)。家君作宰,路出名區(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>
增加了一個(gè) highlight
框,用來(lái)存放高亮的塊, highlight
數(shù)組用來(lái)存放需要高亮的塊的位置信信息。
補(bǔ)充搜索函數(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)建一個(gè)正則,并獲取文本框的文字節(jié)點(diǎn)。用 exec
來(lái)遍歷原文內(nèi)容,這樣可以實(shí)現(xiàn)全文搜索并得到搜索信息,拿到 index
屬性。此時(shí)就用到了前面提到的 createRange
,在文本結(jié)點(diǎn)根據(jù)起始位置和長(zhǎng)度創(chuàng)建一個(gè)選中區(qū)域,并獲取選中區(qū)域的dom信息,將它們存放到一個(gè)數(shù)組中。此時(shí)可以拿到一個(gè)dom信息的數(shù)組:
可以用這個(gè)數(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>
增加對(duì)應(yīng)的樣式:
.tag { position: fixed; background: yellow; }
這里使用 fixed
的原因是得到的距離信息時(shí)是相對(duì)于文檔,而不是父元素。但是這種方式是不可靠的,因?yàn)槔又锌删庉媴^(qū)域是可以滾動(dòng)的,一滾動(dòng)高亮區(qū)域就錯(cuò)位了:
所以還是要采用相對(duì)父元素定位,其實(shí)實(shí)現(xiàn)方式很簡(jiǎn)單,先算出父元素相對(duì)于頁(yè)面的定位,再用剛才得出的距離詳見(jiàn),最后得出高亮標(biāo)簽相對(duì)于父元素的定位。畫(huà)個(gè)簡(jiǎn)單的示意圖:
如圖所示,想得到距離3也就是高亮標(biāo)簽相對(duì)于父元素的距離,就是距離2減去距離1。
const wrapperInfo = ref({}) onMounted(() => { wrapperInfo.value = wrapperRef.value.getBoundingClientRect() })
在mounted狀態(tài)下獲取父元素的信息。
封裝一個(gè)計(jì)算位置信息的函數(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))
這時(shí)就可以把定位改為 position: absolute;
了。
此時(shí)再滾動(dòng)樣式也不會(huì)錯(cuò)亂:
但是當(dāng)被搜索詞在跨行時(shí)會(huì)出現(xiàn)bug:
搜索“星分翼軫“,然而兩行都被高亮了,通過(guò)調(diào)試可以看出它并不會(huì)很智能的分塊返回,所以這段邏輯就需要手動(dòng)去實(shí)現(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)建一個(gè) range
,就可以得到行高。然后根據(jù)行高判斷兩種情況:
if (rangeReact.height === lineHeight) { highlight.value.push(calRectInfo(rangeReact)) } else { // 多行的情況 }
多行的情況可以用雙指針來(lái)試,還以“星分翼軫”為例,設(shè)置 i = 0; j = 1;
,截取文字得到“星”,計(jì)算高度信息,然后 j++;
得到“星分”,當(dāng)文字為“星分翼”的時(shí)候,行高變?yōu)閮尚?,則應(yīng)高亮“星分”。然后將 i
設(shè)置為 j - 1
。繼續(xù)重復(fù)之前的操作。
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é)果都要把計(jì)算結(jié)果 push
到 highlight
中, 當(dāng)后面的結(jié)果可以覆蓋前面的時(shí)候則再 pop
出來(lái)。 if (j !== 1)
的判斷是因?yàn)榈谝淮谓厝r(shí)不應(yīng)該把以前的結(jié)果也刪除掉。
此時(shí)再進(jìn)行搜索,可以折行顯示了。
但其實(shí)還有不盡如人意的地方,因?yàn)檫@個(gè)框是可編輯區(qū)域,當(dāng)插入新的文字時(shí)高亮框不會(huì)實(shí)時(shí)改變,留在了原地。
此時(shí)我們要監(jiān)聽(tīng)文本框文字的改變
<div class="editor" ref="editorRef" contenteditable @input="handleChange">{{ text }}</div>
當(dāng)文字改變時(shí)重新計(jì)算高亮區(qū)域。
const handleChange = () => { text.value = editorRef.value.innerText } watch(text, () => { highlight.value = [] handleSearch() })
這樣當(dāng)輸入文字時(shí),高亮區(qū)域可以實(shí)時(shí)計(jì)算:
以上就是vue實(shí)現(xiàn)搜索并高亮文字的兩種方式總結(jié)的詳細(xì)內(nèi)容,更多關(guān)于vue高亮文字的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue.js項(xiàng)目使用原生js實(shí)現(xiàn)移動(dòng)端的輪播圖
這篇文章主要為大家介紹了vue.js項(xiàng)目中使用原生js實(shí)現(xiàn)移動(dòng)端的輪播圖,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-04-04vue項(xiàng)目之?dāng)?shù)量占比進(jìn)度條實(shí)現(xiàn)方式
這篇文章主要介紹了vue項(xiàng)目之?dāng)?shù)量占比進(jìn)度條實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-12-12vue中的$emit 與$on父子組件與兄弟組件的之間通信方式
本文主要對(duì)vue 用$emit 與 $on 來(lái)進(jìn)行組件之間的數(shù)據(jù)傳輸。重點(diǎn)給大家介紹vue中的$emit 與$on父子組件與兄弟組件的之間通信方式,感興趣的朋友一起看看2018-05-05Vue自定義v-has指令實(shí)現(xiàn)按鈕權(quán)限判斷
這篇文章主要給大家介紹了關(guān)于Vue自定義v-has指令實(shí)現(xiàn)按鈕權(quán)限判斷的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04