選中鼠標附近的文字

最近終於抽空給 Saladict 實現了鼠標懸浮取詞功能,使用了較爲簡潔的實現方式,這裏分享一下原理以及坑的處理。javascript

初嘗試

這個需求其實很早就被人提 issue 了,當時作了一番搜索,最後嘗試了 document.caretPositionFromPoint / document.caretRangeFromPoint ,效果不太理想。java

若是看 mdn 給的例子,就會發現,它是遍歷每一個元素添加事件的。這麼作的緣由是當使用這個方法的時候,若是鼠標指向元素空白的地方,它會就近取位置。因此例子經過給粒度更細的元素綁定來避免這個問題。然而實際上這麼作仍是不足夠的,一個段落末行也許只有幾個字符,這時空出接近一行,也會有上面的問題。node

因此當時就擱置了這個功能。git

靈感

直到最近,看到一個同類的開源劃詞翻譯擴展 FairyDict 實現了取詞功能,遍觀摩了一番源碼github

它的原理是深度優先遞歸遍歷這個元素以及其子元素,經過不斷試探選中區域,並與鼠標座標對比來定位確切位置。翻譯

有沒有發現問題,這個遍歷過程不正是上面 document.caretPositionFromPoint 乾的事麼,那麼咱們只須要最後量一下鼠標是否在取詞範圍中便可。3d

原理

如今總結一下原理:code

  1. 經過 document.caretPositionFromPoint 得到鼠標所指最接近的元素以及文本位置 offset。
  2. 找出 offset 最接近的單詞。
  3. 經過 Range 得到部分文本(單詞)的尺寸和座標。
  4. 驗證鼠標此時在單詞區域範圍中。
  5. 選中這個單詞。Selection 支持直接添加 Range

實現

按原理來實現就很簡單了。本文上按 alt 可體驗取詞效果。blog

/**
 * @param {MouseEvent} e
 * @returns {void}
 */
function selectCursorWord (e) {
  const x = e.clientX
  const y = e.clientY

  let offsetNode
  let offset

  const sel = window.getSelection()
  sel.removeAllRanges()

  if (document['caretPositionFromPoint']) {
    const pos = document['caretPositionFromPoint'](x, y)
    if (!pos) { return }
    offsetNode = pos.offsetNode
    offset = pos.offset
  } else if (document['caretRangeFromPoint']) {
    const pos = document['caretRangeFromPoint'](x, y)
    if (!pos) { return }
    offsetNode = pos.startContainer
    offset = pos.startOffset
  } else {
    return
  }

  if (offsetNode.nodeType === Node.TEXT_NODE) {
    const textNode = offsetNode
    const content = textNode.data
    const head = (content.slice(0, offset).match(/[-_a-z]+$/i) || [''])[0]
    const tail = (content.slice(offset).match(/^([-_a-z]+|[\u4e00-\u9fa5])/i) || [''])[0]
    if (head.length <= 0 && tail.length <= 0) {
      return
    }

    const range = document.createRange()
    range.setStart(textNode, offset - head.length)
    range.setEnd(textNode, offset + tail.length)
    const rangeRect = range.getBoundingClientRect()

    if (rangeRect.left <= x &&
        rangeRect.right >= x &&
        rangeRect.top <= y &&
        rangeRect.bottom >= y
    ) {
      sel.addRange(range)
    }

    range.detach()
  }
}

交互

最後,若是要提供功能開關或者設置不一樣按鍵的話,簡單的處理能夠參考 FairyDict 讓事件處理空轉。但對於 mousemove 這類比較頻繁的事件,在關閉的時候取消事件監聽可能更好一些。在 Saladict 中甚至將「面板被釘住」跟「普通狀況」分開爲不一樣的模式,這裏藉助 RxJS 來處理複雜的邏輯,可參考源碼遞歸

相關文章
相關標籤/搜索