最近終於抽空給 Saladict 實現了滑鼠懸浮取詞功能,使用了較為簡潔的實現方式,這裡分享一下原理以及坑的處理。
初嘗試
這個需求其實很早就被人提 issue 了,當時做了一番搜尋,最後嘗試了 document.caretPositionFromPoint
/ document.caretRangeFromPoint
,效果不太理想。
如果看 mdn 給的例子,就會發現,它是遍歷每個元素新增事件的。這麼做的原因是當使用這個方法的時候,如果滑鼠指向元素空白的地方,它會就近取位置。所以例子通過給粒度更細的元素繫結來避免這個問題。然而實際上這麼做還是不足夠的,一個段落末行也許只有幾個字元,這時空出接近一行,也會有上面的問題。
所以當時就擱置了這個功能。
靈感
直到最近,看到一個同類的開源劃詞翻譯擴充套件 FairyDict 實現了取詞功能,遍觀摩了一番原始碼。
它的原理是深度優先遞迴遍歷這個元素以及其子元素,通過不斷試探選中區域,並與滑鼠座標對比來定位確切位置。
有沒有發現問題,這個遍歷過程不正是上面 document.caretPositionFromPoint
乾的事麼,那麼我們只需要最後量一下滑鼠是否在取詞範圍中即可。
原理
現在總結一下原理:
- 通過
document.caretPositionFromPoint
獲得滑鼠所指最接近的元素以及文字位置 offset。 - 找出 offset 最接近的單詞。
- 通過
Range
獲得部分文字(單詞)的尺寸和座標。 - 驗證滑鼠此時在單詞區域範圍中。
- 選中這個單詞。
Selection
支援直接新增Range
。
實現
按原理來實現就很簡單了。本文上按 alt 可體驗取詞效果。
/**
* @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 來處理複雜的邏輯,可參考原始碼。