blog
blog copied to clipboard
优雅的区分浏览器的双击选词与三击选段
迫于 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 作为判断双击和三击的时间间隔。
我爱用三击,可以接受这点延时。 但是很多人(也许大部分人)不用三击。所以这最好是个选项。
有些软件,比如Tclock2,支持(各键)1-4击,它的优化策略是: 如果用户没有为2/3/4击指派操作,那么1击就瞬发 不用等待了。以此类推。
有些文本编辑器可以停用三击, 然而Chromium没有这开关,用户不能自撸(所以最好是你来照顾 三击不爽用户), Firefox倒是有(然并卵):browser.triple_click_selects_paragraph
其实浏览器的双击也是个问题: Chromium从某版开始,双击中文也是选词,这大概是国际范儿,触屏范儿。 Firefox双击是选择连续中文,虽然显得没技术含量,但我觉得对中文用户更有意义。 IE11,双击中文是选单个字……(我怎么记得以前不是这样)
鸡蛋里挑个鸡腿儿: 复制粘贴文字 是搜不到你的公众号的…… :a:
应该是“优雅 地 区分”。 其实“优雅区分”更好。
阅后可焚
@GH01 我也想过要用选项来控制双击 / 三击,但是我不太想给划词翻译塞更多的选项了。我希望划词翻译的选项越少越好,让用户不需要进行调整就能获得比较好的使用体验。
老实说我一直分不清“的”、“地”该怎么用。。
@GH01 对了,方便加个微信吗?你给划词翻译提了很多有意思的建议(虽然我一直懒没实现……),我觉得还是实时沟通比较便捷。如果你方便的话,关注一下划词翻译的公众号“划词翻译”,然后给我发个消息
就像我这姗姗的回复,想要实时你会失望的。 私下当然可以方便聊多一些,但其实我这一小肚汁儿就都在这章鱼馆里吐完了,而且都是作弊的,这种人你懂的。 然后,email你肯定不乐意,那顶多到QQ,不行要不就算了。
@GH01 我在 GitHub 上回复的更慢 😂
QQ 真的是很久没用了,email 也可以的,我看 email 比较勤。那我们还是保持 GitHub 联系吧,也欢迎随时 email 我。
OK,反正我已对你不着急了 :sleeping:
我上面在说,你文中的公众号DoneIsBetterThenPerfect单词拼错了……
OK,反正我已对你不着急了 😴
我上面在说,你文中的公众号DoneIsBetterThenPerfect单词拼错了……
提醒我了……是 Than 不是 Then