近來終究抽閑給 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 來處置懲罰龐雜的邏輯,可參考源碼。