vue自定義元件——search-box

longxiaoming發表於2023-05-17

github地址: https://github.com/lxmghct/my-vue-components

元件介紹

  • props:
    • value/v-model: 檢索框的值, default: ''
    • boxStyle: 檢索框的樣式, default: 'position: fixed; top: 0px; right: 100px;'
    • highlightColor: 高亮顏色, default: 'rgb(246, 186, 130)'
    • currentColor: 當前高亮顏色, default: 'rgb(246, 137, 31)'
    • selectorList: 檢索的選擇器列表, default: []
    • iFrameId: 檢索的iframe的id, default: null, 若需要搜尋iframe標籤中的內容, 則將該引數設為目標iframe的id
    • beforeJump: 跳轉前的回撥函式, default: () => {}
    • afterJump: 跳轉後的回撥函式, default: () => {}
    • (注: 上述兩個回撥函式引數為currentIndex, currentSelector, lastIndex, lastSelector)
  • events:
    • @search: 檢索時觸發, 引數為input和total
    • @goto: 跳轉時觸發, 引數為index
    • @close: 關閉時觸發
  • methods:
    • clear() 清空檢索框
    • search() 檢索

效果展示

設計思路

完整程式碼見github: https://github.com/lxmghct/my-vue-components
在其中的src/components/SearchBox下。

1. 介面

介面上比較簡單, 輸入框、當前/總數、上一個、下一個、關閉按鈕。

<div class="search-box" :style="boxStyle">
  <input
    v-model="input"
    placeholder="請輸入檢索內容"
    class="search-input"
    type="text"
    @input="search"
  >
  <!--當前/總數、上一個、下一個、關閉-->
  <span class="input-append">
    &nbsp;&nbsp;{{ current }}/{{ total }}&nbsp;&nbsp;
  </span>
  <span class="input-append" @click="searchPrevious">
    <div class="svg-container">
      <svg width="100px" height="100px">
        <path d="M 100 0 L 0 50 L 100 100" stroke="black" fill="transparent" stroke-linecap="round"/>
      </svg>
    </div>
  </span>
  <span class="input-append" @click="searchNext">
    <div class="svg-container">
      <svg width="100px" height="100px" transform="rotate(180)">
        <path d="M 100 0 L 0 50 L 100 100" stroke="black" fill="transparent" stroke-linecap="round"/>
      </svg>
    </div>
  </span>
  <span class="input-append" @click="searchClose">
    <div class="svg-container">
      <svg width="100%" height="100%">
        <line x1="0" y1="0" x2="100%" y2="100%" stroke="black" stroke-width="1" />
        <line x1="100%" y1="0" x2="0" y2="100%" stroke="black" stroke-width="1" />
      </svg>
    </div>
  </span>
</div>

2. 檢索與跳轉

這部分是search-box的核心功能,一共有以下幾個需要解決的問題:

  1. 獲取待搜尋的容器
    • 為提高元件的通用性,可以透過傳入選擇器列表來獲取容器,如['.container', '#containerId'],使用document.querySelector()獲取容器。
  2. 獲取所有文字
    • 不能單獨對某個dom節點獲取文字, 因為某個待搜尋詞可能被分割在多個節點中, 例如<span>hello</span><span>world</span>,所以需要獲取整個容器內的所有文字拼接起來, 然後再進行檢索。
    • 使用innetText獲取文字會受到樣式影響, 具體見文章最後的其它問題。所以需要遍歷所有節點將文字拼接起來。
    • 遍歷文字節點時, 可以用node.nodeType === Node.TEXT_NODE判斷是否為文字節點。
    if (node.nodeType === Node.TEXT_NODE) { // text node
        callback(node)
    } else if (node.nodeType === Node.ELEMENT_NODE) { // element node
        for (let i = 0; i < node.childNodes.length; i++) {
            traverseTextDom(node.childNodes[i], callback)
        }
    }
    
  3. 檢索結果的儲存
    • 由於查詢完之後需要實現跳轉, 所以為方便處理, 將檢索到的結果所在的dom節點儲存起來, 以便後續跳轉時使用。每個結果對應一個domList。
  4. 高亮檢索詞
    • 使用span標籤包裹檢索詞, 並設定樣式, 實現高亮。
    • 為了避免檢索詞被html標籤分割, 可以對檢索詞的每個字元都用span標籤包裹, 例如檢索詞為hello,則可以將其替換為<span>h</span><span>e</span><span>l</span><span>l</span><span>o</span>
    • 樣式設定可以給span設定background-color, 為了方便修改並減小整體html長度, 可以改為給span設定class, 注意這種情況下在style標籤設定的樣式未必有效, 可以採用動態新增樣式的方式。
    function createCssStyle (css) {
        const style = myDocument.createElement('style')
        style.type = 'text/css'
        try {
            style.appendChild(myDocument.createTextNode(css))
        } catch (ex) {
            style.styleSheet.cssText = css
        }
        myDocument.getElementsByTagName('head')[0].appendChild(style)
    }
    
    • 將span標籤插入到原先文字節點的位置, 若使用innerHtml直接進行替換, 處理起來略有些麻煩。可以考慮使用insertBefore和removeChild方法。
    const tempNode = myDocument.createElement('span')
    tempNode.innerHTML = textHtml
    const children = tempNode.children
    if (children) {
      for (let i = 0; i < children.length; i++) {
        domList.push(children[i])
      }
    }
    // 將節點插入到parent的指定位置
    // insertBofore會將節點從原來的位置移除,導致引錯誤,所以不能用forEach
    while (tempNode.firstChild) {
      parent.insertBefore(tempNode.firstChild, textNode)
    }
    parent.removeChild(textNode)
    
  5. 跳轉
    由於結果對應的dom節點已儲存,所以跳轉起來比較容易。跳轉時修改當前高亮的dom節點的類名, 然後將其滾動到可視區域。
    setCurrent (index) {
        const lastSelector = this.searchResult[this.currentIndex] ? this.searchResult[this.currentIndex].selector : null
        const currentSelector = this.searchResult[index] ? this.searchResult[index].selector : null
        if (this.currentIndex >= 0 && this.currentIndex < this.searchResult.length) {
            this.searchResult[this.currentIndex].domList.forEach((dom) => {
                dom.classList.remove(this.currentClass)
            })
            this.searchResult[this.currentIndex].domList[0].scrollIntoView({ behavior: 'smooth', block: 'center' })
        }
        this.currentIndex = index
        if (this.currentIndex >= 0 && this.currentIndex < this.searchResult.length) {
            this.searchResult[this.currentIndex].domList.forEach((dom) => {
                dom.classList.add(this.currentClass)
            })
        }
    }
    
  6. 移除高亮效果
    • 由於高亮效果是透過給text節點新增span標籤實現, 所以需要將span標籤移除, 並替換為原先的文字節點。
    • 使用insertBeforeremoveChild方法。
    • 替換完節點後需要呼叫normalize()方法, 將相鄰的文字節點合併為一個文字節點。
    function convertHighlightDomToTextNode (domList) {
        if (!domList || !domList.length) { return }
        domList.forEach(dom => {
            if (dom && dom.parentNode) {
                const parent = dom.parentNode
                const textNode = myDocument.createTextNode(dom.textContent)
                parent.insertBefore(textNode, dom)
                parent.removeChild(dom)
                parent.normalize() // 合併相鄰的文字節點
            }
        })
    }
    

3. 新增對iframe的支援

有時候頁面中可能會包含iframe標籤, 如果需要檢索iframe中的內容, 直接使用當前的document是無法獲取到iframe中的內容的, 需要拿到iframe的document物件。

const myIframe = document.getElementById(this.iframeId)
if (myIframe) {
  myDocument = myIframe.contentDocument || myIframe.contentWindow.document
} else {
  myDocument = document
}
if (myIframe && this.lastIframeSrc !== myIframesrc) {
  const css = `.${this.highlightClass} { background-color: ${this.highlightColor}; } .${this.currentClass} { background-color: ${this.currentColor}; }`
  createCssStyle(css)
  this.lastIframeSrc = myIframe.src
}

同一個iframe, 如果src發生變化, 則需要重新給其生成樣式, 否則樣式會失效。

其他問題

  1. 使用svg畫按鈕圖示時,雙擊svg按鈕會自動觸發全選
    • 解決方法: 在svg標籤所在容器上新增user-select: none;樣式
  2. 使用node.nodeType === Node.TEXT_NODE判斷文字節點時,會遇到一些空節點,導致檢索錯誤
    • 解決方法: 在判斷文字節點時,加上node.textContent.trim() !== ''的判斷, 獲取所有元素的文字時。
    • 後續修改: 可以不單獨處理這些空的文字節點, 只要保證所有使用到獲取文字的地方都統一使用或不使用trim()即可。儘量都不使用trim(), 如果隨意使用trim(),可能會導致部分空白字元被誤刪。

相關文章