最近终于抽空给 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 来处理复杂的逻辑,可参考源码。递归