打造一個媲美Ctrf+F的搜索組件

背景

Ctrf+F相信你們必定不陌生,不少人都依賴Ctrf+F來搜索網頁上想要看到的內容,也是最頻繁使用的功能之一。瀏覽器之間存在兼容性問題,可是確定都提供原生搜索框(並且快捷鍵都是ctrf+f)javascript

原生搜索功能

這裏簡單介紹下原生搜索框提供了哪些功能
image.pnghtml

  1. 關鍵詞高亮
  2. 當前頁面命中關鍵詞數量和當前選中的關鍵詞序號
  3. 當前選中關鍵詞高亮
  4. 切換到上/下一個關鍵詞
  5. esc關閉搜索

功能簡單且強大,只要是頁面內渲染出來的文本都能搜索且定位到目標java

不足

ctrf+f的功能知足咱們平常所需,那麼是否是存在不足之處呢?來看看幾個案例node

  1. 2個以上div標籤組成的連續文本
    image.pnggit

    <div style="display:flex">  
        <div>搜索</div>  
        <div>測試</div>  
    </div>
  2. 手風琴組件被摺疊的文本也會搜索出來
    image.pnggithub

    <div>搜索</div>  
    <div style="height: 0px;">測試</div>
  3. 頁面級別的搜索,某些非正文內容也在搜索範圍,好比導航、底部、廣告

想法

針對不足點1,其實document.querySelector('.box').textContent打印出來結果是搜索測試中間有個換行符,因此致使瀏覽器沒法搜索出來。在富文本的場景下,這種結構的html很常見,好比某段文字中插入幾個高亮文字來作tooltip解釋說明。理想的結果固然是能自動生成連續的文字再搜索數組

針對不足點二、3,也是由於咱們不但願站內某些內容被索引到,或者說索引到的時候能自動展開,好比手風琴瀏覽器

方案

須要實現一個相似ctrf+f的搜索功能,而且跟框架無關,那麼只有從dom上面入手框架

  1. 須要一個標識來識別連續文本,經過深度遍歷的方式拼接文本同時記錄文本節點所在的位置,主要是爲了避免破壞原有dom結構的同時高亮跨標籤的文本,好比dom

    <div style="display:flex">  
        <div>搜索</div>  
        <div>測試</div>  
    </div>
    <!-- 替換成 -->
    <div style="display:flex">  
        <div><mark>搜索</mark></div>      <!-- mark就是高亮標籤 -->
        <div><mark>測試</mark></div>  
    </div>
  2. 深度遍歷dom結構的同時須要作幾個邏輯

    1. 記錄滾動節點,能夠根據scrollHeight、clientHeight、scrollWidth、clientWidth判斷元素是否存在滾動條
    2. 過濾無關標籤,好比設置了display:none或者height:0,固然也能夠本身定義不搜索特定的class
  3. 圈定搜索範圍,知足不足點3
  4. 修復不合理的文本標籤,具體緣由後面解釋

    <div id="error">
        這是異常的
        <span>節點</span>
    </div>
    <!-- 替換爲 -->
    <div id="error">
        <span>這是異常的</span>
        <span>節點</span>
    </div>
  5. 替換高亮標籤的同時保留原有的文本,便於後續恢復

    <div>  
        搜索測試
    </div>
    <!-- 關鍵詞 搜索,dom結構替換爲 -->
    <div>  
      <mark>搜索</mark>  
      測試
      <template>搜索測試</template>
    </div>
  6. 命中關鍵詞總數量和上/下功能經過數組和當前下標值來肯定
  7. 提供上下定位到某個高亮關鍵詞功能~~~~

邏輯處理

圈定搜索範圍

傳入一個class或者id便可,作爲後續深度遍歷的頂層節點
const dom = document.querySelector(classname)

深度遞歸遍歷dom結構

  1. 記錄滾動節點
  2. 過濾不顯示(height:0display:none)和黑名單標籤
  3. 記錄長文本(拼接搜索區域出現過關鍵詞字串的文本)和節點文本在長文本中起始和結束位置
formatDom(el, value) {
        const childList = el.childNodes
        if (!childList.length || !value.length) return // 無子節點或無查詢值,則不進行下列操做
        childList.forEach(el => {
            // 遍歷其內子節點
            if (el.nodeType === 1 || el.nodeType === 3) {
                //頁面內存在滾動節點的話,須要記錄
                if (isRealNode(el)) {
                    if(el.scrollHeight > el.clientHeight) {
                        //縱向滾動條
                        this.overflowYDom.push(el)
                    }
                    if(el.scrollWidth > el.clientWidth) {
                        //橫向滾動條
                        this.overflowXDom.push(el)
                    }
                }
                if (
                    isRealNode(el) && // 若是是元素節點
                    checkClassName(el, this.blackClassName) &&
                    !/(script|style|template)/i.test(el.tagName)
                ) {
                    // 而且元素標籤不是script或style或template等特殊元素
                    this.formatDom(el, value) // 那麼就繼續遍歷(遞歸)該元素節點
                } else if (el.nodeType === 3) {
                    // 記錄關鍵詞中字串出現過的文本節點
                    for (let j = 0; j < value.length; j++) {
                        if (el.data.indexOf(value[j]) > -1) {
                            const start = this.searchDom.text.length
                            this.searchDom.text = this.searchDom.text + el.parentNode.innerText     //拼接文本,便於後續處理跨文本標籤匹配
                            this.searchDom.data[`${start}-${this.searchDom.text.length - 1}`] = el                  //記錄每一個文本節點內容在全文本中起始下標位置
                            break
                        }
                    }
                }
            }
        })
    }

處理跨標籤文本

假設原始dom長這樣

<div>
    <h2>跨標籤文案</h2>
    <div class="flex">
        <div>
            這是一段跨標籤
        </div>
        <div>
            跨多個標籤
        </div>
        <div>
            組成的文案
        </div>
        <div>
            測試
        </div>
    </div>
</div>

搜索關鍵詞:文案測試

上一步已經獲取到長文本(搜索區域拼接的所有文本內容)和節點出如今長文本的下標
長文本跨標籤文案組成的文案測試
節點出如今長文本的下標:

{
    '0-4': textElement,
    '5-9': textElement,
    '10-11': textElement
}

注:這裏只收集包含關鍵詞字串的節點和文本內容,好比 這是一段跨標籤 這個節點就不收集

  1. 先找到搜索關鍵詞在長文本中出現的起始位置

    searchSubStr(str, subStr) {
            //str 長文本
            //subStr 關鍵詞
            let arr = []
            let index = str.indexOf(subStr)
            while (index > -1) {
                arr.push(index)
                index = str.indexOf(subStr, index + 1)
            }
            return arr
        }
        //返回 [8]
  2. 根據起始位置加上關鍵詞長度獲得匹配的區域
    好比關鍵詞所在的位置就是 8 - 12(關鍵詞長度 4),從收集的節點出如今長文本的下標,發現跨2個text節點,那麼就須要同時高亮2個節點
  3. 計算高亮節點(可能跨標籤),具體邏輯這裏就很少描述了,感興趣能夠參考源碼

高亮節點

代碼細節能夠參考源碼,有幾個須要注意的事項

  1. 須要修復異常節點,好比

    <div id="error">
         這是異常的
         <span>節點</span>
     </div>
     <!-- 替換爲 -->
     <div id="error">
         <span>這是異常的</span>
         <span>節點</span>
     </div>

    緣由在於這是異常的這個text節點存在一個兄弟節點 span,取消高亮以後沒法復原(可能有別的辦法,可是考慮成本太高,就不考慮),復原方案參考下面

  2. 替換高亮節點的同時保留template方便後續復原,跨標籤節點標記同個id(mark-id),方便上下選中的計算
    image.png

總結

demo傳送門
篇幅有限,其它細節就不一一解釋,你們能夠看下源碼,順便求個start~~~

相關文章
相關標籤/搜索