资讯专栏INFORMATION COLUMN

选中鼠标附近的文字

shuibo / 579人阅读

摘要:它的原理是深度优先递归遍历这个元素以及其子元素,通过不断试探选中区域,并与鼠标座标对比来定位确切位置。原理现在总结一下原理通过获得鼠标所指最接近的元素以及文本位置。验证鼠标此时在单词区域范围中。

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/95615.html

相关文章

  • 宇宙最强vscode教程(基础篇)

    摘要:在命令面板中你可以输入命令进行搜索中英文都可以,然后执行。命名面板中可以执行各种命令,包括编辑器自带的功能和插件提供的功能。 本文主要介绍vscode在工作中常用的快捷键及插件,目标在于提高工作效率本文的快捷键是基于mac的,windows下的快捷键放在括号里 Cmd+Shift+P(win Ctrl+Shift+P) [TOC] 零、快速入门 有经验的可以跳过快速入门或者大致浏览一...

    Jason_Geng 评论0 收藏0
  • 原生JS写一个功能强大编辑器

    摘要:主要采用了原生与调用结合的功能实现功能。所以根据这种方法,读者可以根据自己的需求添加更多的功能,比如在编辑框里面插入一个可以点击的标签或者添加一个代码块希望能读到此文章的读者,能在下方一起交流,更希望大佬提出错误,谢谢地址 因为一个同学,要做一个能加入图片的留言板功能,类型与QQ空间留言板和百度贴吧发帖的那种形式,同时在网上找了找发生网上对这方面的交流很少,所以发表这篇文章抛砖引玉,希...

    luck 评论0 收藏0
  • 原生JS写一个功能强大编辑器

    摘要:主要采用了原生与调用结合的功能实现功能。所以根据这种方法,读者可以根据自己的需求添加更多的功能,比如在编辑框里面插入一个可以点击的标签或者添加一个代码块希望能读到此文章的读者,能在下方一起交流,更希望大佬提出错误,谢谢地址 因为一个同学,要做一个能加入图片的留言板功能,类型与QQ空间留言板和百度贴吧发帖的那种形式,同时在网上找了找发生网上对这方面的交流很少,所以发表这篇文章抛砖引玉,希...

    王晗 评论0 收藏0

发表评论

0条评论

shuibo

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<