最近终于抽空给Saladict实现了鼠标悬停取词功能,实现方式比较简单。这里分享一下原理和陷阱的处理。这个要求的第一次尝试实际上很早就作为一个问题提出了。当时搜了一下,最后试了下document.caretPositionFromPoint/document.caretRangeFromPoint,效果都不理想。如果你看mdn给的例子,你会发现它遍历每一个元素来添加事件。这样做的原因是,在使用该方法时,如果鼠标指向元素中的空白区域,它会取最近的位置。所以这个例子通过绑定更细粒度的元素来避免这个问题。然而,这在实践中是不够的。一段的最后一行可能只有几个字符。这时候如果有近一行空着,也会出现上面的问题。所以这个功能当时就被搁置了。灵感直到最近看到一个类似开源的分词翻译插件FairyDict,实现了分词功能,看了下源码。它的原理是首先递归地遍历元素及其子元素,通过不断地检测选中的区域并与鼠标坐标进行比较,从而定位到准确的位置。你有没有发现任何问题?这个遍历过程不就是上面的document.caretPositionFromPoint所做的吗?那么我们只需要测量鼠标是否在末尾的单词范围内即可。原理下面总结一下原理:通过document.caretPositionFromPoint获取鼠标指向的距离最近的元素和文本位置偏移量。找到最接近偏移量的词。通过Range获取一段文字(word)的大小和坐标。确认鼠标现在位于单词区域的边界内。检查这个词。Selection支持直接添加Range。按照原理实现很简单。在这篇文章上按alt,体验一下抽词效果。/***@param{MouseEvent}e*@returns{void}*/functionselectCursorWord(e){constx=e.clientXconsty=e.clientYletoffsetNodeletoffsetconstsel=window.getSelection()sel.removeAllRanges()if(document['caretPositionFromPoint']){constpos=document['caretPositionFromPoint'](x,y)if(!pos){return}offsetNode=pos.offsetNodeoffset=pos.offset}elseif(document['caretRangeFromPoint']){constpos=document['caretRangeFromPoint'](x,y)if(!pos){return}offsetNode=pos.startContaineroffset=pos.startOffset}else{return}if(offsetNode.nodeType===Node.TEXT_NODE){consttextNode=offsetNodeconstcontent=textNode.dataconsthead=(content.slice(0,offset).match(/[-_a-z]+$/i)||[''])[0]consttail=(content.slice(offset).match(/^([-_a-z]+|[\u4e00-\u9fa5])/i)||[''])[0]如果(head.length<=0&&tail.length<=0){return}constrange=document.createRange()range.setStart(textNode,offset-head.length)range.setEnd(textNode,offset+tail.length)constrangeRect=range.getBoundingClientRect()if(rangeRect.left<=x&&rangeRect.right>=x&&rangeRect.top<=y&&rangeRect.bottom>=y){sel.addRange(range)}range.detach()}}Interaction最后,如果你想提供功能开关或设置不同的按钮,只需处理方面可以参考FairyDict让事件处理闲置,但是对于mousemove等比较频繁的事件,关闭时取消事件监听可能会更好。在Saladict中,甚至“固定面板”和“正常情况”也分为不同的模式。在这里,RxJS用于处理复杂的逻辑。请参考源代码。
