最近終於抽空給 Saladict 實現了鼠標懸浮取詞功能,使用了較爲簡潔的實現方式,這裏分享一下原理以及坑的處理。javascript
這個需求其實很早就被人提 issue 了,當時作了一番搜索,最後嘗試了 document.caretPositionFromPoint
/ document.caretRangeFromPoint
,效果不太理想。java
若是看 mdn 給的例子,就會發現,它是遍歷每一個元素添加事件的。這麼作的緣由是當使用這個方法的時候,若是鼠標指向元素空白的地方,它會就近取位置。因此例子經過給粒度更細的元素綁定來避免這個問題。然而實際上這麼作仍是不足夠的,一個段落末行也許只有幾個字符,這時空出接近一行,也會有上面的問題。node
因此當時就擱置了這個功能。git
直到最近,看到一個同類的開源劃詞翻譯擴展 FairyDict 實現了取詞功能,遍觀摩了一番源碼。github
它的原理是深度優先遞歸遍歷這個元素以及其子元素,經過不斷試探選中區域,並與鼠標座標對比來定位確切位置。翻譯
有沒有發現問題,這個遍歷過程不正是上面 document.caretPositionFromPoint
乾的事麼,那麼咱們只須要最後量一下鼠標是否在取詞範圍中便可。3d
如今總結一下原理:code
document.caretPositionFromPoint
得到鼠標所指最接近的元素以及文本位置 offset。Range
得到部分文本(單詞)的尺寸和座標。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 來處理複雜的邏輯,可參考源碼。遞歸