Better

Ethan的博客,欢迎访问交流

组合快捷键功能实现

当初实现组合快捷键功能的时候,觉得这是个并不复杂的功能,同时结合项目自身的特殊需要,并没有考虑引入第三方解决方案,于是乎自己简单实现了一个,keydown/keyup 事件安排起来,直到有一天发现有时候快捷键会失效,比如当我使用系统快捷键切换窗口,在切回时,快捷键就失效了。

原因

调试发现,当我通过快捷键切换窗口时,比如 ctrl+arrowright,会导致 ctrl keydown 事件触发, 但 keyup 事件并没有触发,从而导致我代码中维护的 pressedKeys Set 数据不对,从而导致无法正确触发。

所以这里我犯了一个错误,那就是 keydown 事件触发,并不代表 keyup 事件一定会触发,比如当你绑定事件元素失去焦点时,再松开按键,此时 keyup 事件就不会触发。

详细理解下三个事件

  • keydown:在控件有焦点的情况下按下键时发生
  • keypress:在控件有焦点的情况下按下键时发生
  • keyup:在控件有焦点的情况下释放键时发生

他们的前提是控件有焦点,但当你松开按键之前,你是有可能让控件失去焦点的。

keypress 和 keydown 的区别

  • keypress:keydown 比较底层,而 keypress 比较高级,它只有在按下能产生字符的按键时才会触发,比如字母、数字、标点符号等,对于修饰符按键、功能按键则不会触发。查阅 mdn 提示该事件已经过时,不推荐再使用。
  • keydown:可以捕获所有按键,不区分字母大小写,而 keypress 区分

keydown、keypress 按着不动会持续执行事件,keyup 执行一次

Mousetrap

于是乎,我就很好奇第三方库是如何解决这个问题的,比如 MouseTrap。

查阅后,MouseTrap 不同于我的实现方式,并没有通过类似 pressedKeys 记录按键按下情况,仅仅是通过当前的 event 对象确定回调函数,因此猜测只能实现修饰符按键组合单个字符的组合方式,无法实现类型 ctrl+a+b 的快捷键组合,经验证,的确如此!

Mousetrap 使用注意点

  • 如果你在脚本中绑定相同的键事件,它会覆盖你指定的原始回调。回调函数中返回 false,会阻止浏览器默认行为和冒泡
  • 每个实例都跟踪自己的回调,如果将相同的键绑定到多个元素,所有单独的回调都将触发,除非使用 event.stopPropagation()

KeyboardJS

KeyboardJS 相比 Mousetrap 而言体积要大很多,但提供了很多其他的特性

  • 兼容 nodejs
  • 提供上下文概念,对于单页应用而言十分有用,允许将绑定范围限定到应用程序的各个部分
  • 支持更复杂的绑定,例如 a+b

那么为啥它可以实现 a+b 的逻辑呢,简单翻阅下源代码,它同样通过 pressedKeys 记录按下的值,那么为啥它不存在我这里的问题呢。

看到如下一段代码

this._bindEvent(targetElement, 'keydown', this._targetKeyDownBinding);
this._bindEvent(targetElement, 'keyup',   this._targetKeyUpBinding);
// Notice: here is window object
this._bindEvent(targetWindow,  'focus',   this._targetResetBinding);
this._bindEvent(targetWindow,  'blur',    this._targetResetBinding);

解决方式是监听 focusblur 事件,当触发时,清空所有的按键,这样导致的结果是,你需要松开当前按键,然后重新按下,但这无伤大雅。

还看到一个我自己实现时踩的坑的解决方法如下

this._targetKeyDownBinding = (event) => {
    this.pressKey(event.keyCode, event);
    this._handleCommandBug(event, platform);
}
 _handleCommandBug(event, platform) {
    // On Mac when the command key is kept pressed, keyup is not triggered for any other key.
    // In this case force a keyup for non-modifier keys directly after the keypress.
    const modifierKeys = ["shift", "ctrl", "alt", "capslock", "tab", "command"];
    if (platform.match("Mac") && this._locale.pressedKeys.includes("command") &&
        !modifierKeys.includes(this._locale.getKeyNames(event.keyCode)[0])) {
        this._targetKeyUpBinding(event);
    }
}

处理方式是当你按下非修饰符键时(已交给 pressKey 处理完后),如果 command 已经被按下,代码里面手动释放掉该键,这样的后果是,cmd+c 已经触发,但 cmd+c+v 已经就无法触发了(未验证)

其他坑

已知问题:在 Mac 上,command 键按下时,非修饰符按键的 up 事件不触发,比如 cmd+z,松开 z,此时 up 事件不会触发。可以借鉴 KeyboardJS 的处理方式。

资料



留言