lmk123 / blog

个人技术博客,博文写在 Issues 里。
https://github.com/lmk123/blog/issues
625 stars 35 forks source link

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

Open lmk123 opened 4 years ago

lmk123 commented 4 years ago

迫于 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 作为判断双击和三击的时间间隔。

GH01 commented 4 years ago

我爱用三击,可以接受这点延时。 但是很多人(也许大部分人)不用三击。所以这最好是个选项。

有些软件,比如Tclock2,支持(各键)1-4击,它的优化策略是: 如果用户没有为2/3/4击指派操作,那么1击就瞬发 不用等待了。以此类推。

有些文本编辑器可以停用三击, 然而Chromium没有这开关,用户不能自撸(所以最好是你来照顾 三击不爽用户), Firefox倒是有(然并卵):browser.triple_click_selects_paragraph

其实浏览器的双击也是个问题: Chromium从某版开始,双击中文也是选词,这大概是国际范儿,触屏范儿。 Firefox双击是选择连续中文,虽然显得没技术含量,但我觉得对中文用户更有意义。 IE11,双击中文是选单个字……(我怎么记得以前不是这样)

GH01 commented 4 years ago

鸡蛋里挑个鸡腿儿: 复制粘贴文字 是搜不到你的公众号的…… :a:

应该是“优雅 区分”。 其实“优雅区分”更好。

阅后可焚

lmk123 commented 4 years ago

@GH01 我也想过要用选项来控制双击 / 三击,但是我不太想给划词翻译塞更多的选项了。我希望划词翻译的选项越少越好,让用户不需要进行调整就能获得比较好的使用体验。

老实说我一直分不清“的”、“地”该怎么用。。

lmk123 commented 4 years ago

@GH01 对了,方便加个微信吗?你给划词翻译提了很多有意思的建议(虽然我一直懒没实现……),我觉得还是实时沟通比较便捷。如果你方便的话,关注一下划词翻译的公众号“划词翻译”,然后给我发个消息

GH01 commented 4 years ago

就像我这姗姗的回复,想要实时你会失望的。 私下当然可以方便聊多一些,但其实我这一小肚汁儿就都在这章鱼馆里吐完了,而且都是作弊的,这种人你懂的。 然后,email你肯定不乐意,那顶多到QQ,不行要不就算了。

lmk123 commented 4 years ago

@GH01 我在 GitHub 上回复的更慢 😂

QQ 真的是很久没用了,email 也可以的,我看 email 比较勤。那我们还是保持 GitHub 联系吧,也欢迎随时 email 我。

GH01 commented 4 years ago

OK,反正我已对你不着急了 :sleeping:

我上面在说,你文中的公众号DoneIsBetterThenPerfect单词拼错了……

lmk123 commented 4 years ago

OK,反正我已对你不着急了 😴

我上面在说,你文中的公众号DoneIsBetterThenPerfect单词拼错了……

提醒我了……是 Than 不是 Then