Skip to content
返回

优雅的区分浏览器的双击选词与三击选段

迫于 GitHub 越来越难访问了,我开通了微信公众号,目前正在慢慢的将博客的内容搬运过去,未来文章会优先发布在公众号里,然后同步在 GitHub 中,欢迎关注:

如果看不到图片,可以在微信里搜索 DoneIsBetterThanPerfect 关注。

正文开始

在浏览器中,要选中一段文本有三种方式:

在我开发划词翻译的时候,我需要在用户选中了文本之后弹出一个窗口,显示这段文本的翻译结果。实现的方式很简单,只需要监听 mouseup 事件就可以了:

document.addEventListener('mouseup', () => {
  if (window.getSelection().toString().trim()) {
    console.log('弹出翻译窗口')
  }
})

但这么做会有一个问题:当用户三击选段时,控制台会打印两次,因为三击选段操作必定会先触发一次双击选词。

也就是说,每次用户三击选段的时候,划词翻译会先弹出被双击的单词的翻译结果,然后会立刻改成段落的翻译结果,这在用户体验上是不太友好的,而且也确实有用户反馈了这个问题

不太完美的解决方式:debounce

用 debounce 是我脑海中第一个想到的方案:每次 mouseup 事件触发的时候,不要立刻弹出翻译窗口,而是先等待一小段时间,如果这段时间内又触发了一次 mouseup,就取消第一次的翻译操作。

代码实现如下:

import debounce from 'lodash/debounce'
// 给事件处理函数套一层 debounce
document.addEventListener('mouseup', debounce(function() {
  if (window.getSelection().toString().trim()) {
    console.log('弹出翻译窗口')
  }
}, 500)) // 500ms 的计算方式见文末

这样做之后,三击选段的时候确实不会再触发双击选词了,但这又带来另外一个问题:500ms 的延迟太明显了,而且,现在无论是用哪种翻译方式,用户都需要等待 500ms 才能看到弹出窗口。

进一步优化

这里有两个可以优化的点:

综上所述,只有当用户双击的时候才需要等待 500ms,因为需要确认用户这次双击的后面会不会跟着一个三连击。

有了思路之后,代码就很好实现了:

let clickTimes = 0
let clickTimeoutId

const mousedownPoint = {
  x: 0,
  y: 0,
}

function trigger() {
  console.log('显示翻译结果')
}

document.addEventListener('mousedown', (event) => {
  mousedownPoint.x = event.clientX
  mousedownPoint.y = event.clientY
})

document.addEventListener('mouseup', (event) => {
  window.clearTimeout(clickTimeoutId)

  if (
    !(mousedownPoint.x === event.clientX && mousedownPoint.y === event.clientY)
  ) {
    console.log('mousedown 和 mouseup 的位置不一样,触发鼠标划选翻译')
    trigger()
    clickTimes = 0
    return
  }

  clickTimes += 1
  console.log('位置一样,clickTimes 加 1,当前已点击次数', clickTimes)

  if (clickTimes === 3) {
    console.log('连续点击了 3 次,触发三击选段翻译')
    trigger()
    clickTimes = 0
  } else {
    clickTimeoutId = window.setTimeout(() => {
      console.log('500ms 内没有点击,重置连击次数')
      if (clickTimes === 2) {
        console.log('点击了两次但没有点击第三次,触发双击选词翻译')
        trigger()
      }
      clickTimes = 0
    }, 500)
  }
})

这么做之后,就只有双击的情况会等待 500ms 显示翻译结果了。

附录:500ms 的计算方式

为了测试浏览器会将两次间隔多长时间的单击视为“双击”事件,我写了这段代码:

/**
 * @overview 测试浏览器的一次双击事件中,第一次单击到双击事件触发间隔了多长事件
 *
 * 使用方式:
 * 1. 在 Console 中粘贴代码
 * 2. 在页面中快速单击两次,Console 中会显示这两次单击的间隔时间
 * 3. 重复第二步,但逐步增加两次单击的时间间隔,如果 Console 中打印了 'trigger second click' 但没有打印 'trigger dblclick',说明超过了浏览器的双击判定时间间隔
 */

let firstClick = true
let firstClickTimeoutId

document.addEventListener('click', () => {
  if (firstClick) {
    console.time('delay between click and dblclick')
    console.log('trigger first click')
    firstClick = false

    // 间隔 1s 之后肯定不会再触发双击事件了,所以重置状态
    firstClickTimeoutId = window.setTimeout(() => {
      firstClick = true
      console.log('second click timeout')
      console.timeEnd('delay between click and dblclick')
    }, 1000)
  } else {
    console.timeLog('delay between click and dblclick')
    console.log('trigger second click')
    window.clearTimeout(firstClickTimeoutId)
  }
})

document.addEventListener('dblclick', () => {
  console.timeEnd('delay between click and dblclick')
  console.log('trigger dblclick')
  firstClick = true
})

在多次尝试之后,我在 Chrome v80 中测出来的能触发双击事件的最大的时间间隔是 506ms,所以文中使用了 500ms 作为判断双击和三击的时间间隔。


分享这篇文章:

上一篇
Shadow DOM 在浏览器扩展程序中的应用
下一篇
为什么苹果要完全屏蔽第三方 Cookie?