当前位置: 首页 > 工具软件 > Keymaster.js > 使用案例 >

js快捷键第三方库hotkeys.js,适配重复按键修改源码

吴修洁
2023-12-01

使用背景

在web项目开发过程中,经常会有配置快捷键的需求,我在网上搜索找到用的比较多的两个库: hotkeys-js(5.7K star),keymaster(6.5K start)。测试起来还是hotkeys-js好用一些,毕竟keymaster已经很久没有维护了。
但是hotkeys-js这个库有个问题是,不支持重复按键的快捷键,比如 G+E+E这种,但是不会有E+G+E这种需求,所以为了适配项目功能,才有了对hotkeys-js源码简单改造的事情

改造过程

1.根据需求,分析源码

1.1 keyup keydown

想要适配本地化需求,首先当然是分析为什么现在不能支持重复键,hotkeys-js代码是纯js写的,读起来也并不复杂,通读代码后发现主要是在处理keyup事件时,会将这个键从按键列表中删除,导致多次重复按同一个键时,只能触发一个。

// scr/index.js
 if (typeof element !== 'undefined' && !isElementBind(element) && window) {
    elementHasBindEvent.push(element);
    addEvent(element, 'keydown', (e) => {
      dispatch(e, element);
    }, capture);
    if (!winListendFocus) {
      winListendFocus = true;
      addEvent(window, 'focus', () => {
        _downKeys = [];
      }, capture);
    }
    addEvent(element, 'keyup', (e) => {
      dispatch(e, element);
      clearModifier(e);
    }, capture);
  }

在触发keydown和keyup事件时都调用了dispatch(e, element)函数,而keyup还另外触发了clearModifier(e),那这两个函数做了什么事情呢?分别来看看

1.2 dispatch

// scr/index.js
function dispatch(event, element) {
  const asterisk = _handlers['*'];
  let key = event.keyCode || event.which || event.charCode;

  // 表单控件过滤 默认表单控件不触发快捷键
  if (!hotkeys.filter.call(this, event)) return;

  // Gecko(Firefox)的command键值224,在Webkit(Chrome)中保持一致
  // Webkit左右 command 键值不一样
  if (key === 93 || key === 224) key = 91;

  /**
 * Collect bound keys
 * If an Input Method Editor is processing key input and the event is keydown, return 229.
 * https://stackoverflow.com/questions/25043934/is-it-ok-to-ignore-keydown-events-with-keycode-229
 * http://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html
   */
  if (_downKeys.indexOf(key) === -1 && key !== 229) _downKeys.push(key);
  /**
 * Jest test cases are required.
 * ===============================
   */
  ['ctrlKey', 'altKey', 'shiftKey', 'metaKey'].forEach((keyName) => {
    const keyNum = modifierMap[keyName];
    if (event[keyName] && _downKeys.indexOf(keyNum) === -1) {
      _downKeys.push(keyNum);
    } else if (!event[keyName] && _downKeys.indexOf(keyNum) > -1) {
      _downKeys.splice(_downKeys.indexOf(keyNum), 1);
    } else if (keyName === 'metaKey' && event[keyName] && _downKeys.length === 3) {
      /**
       * Fix if Command is pressed:
       * ===============================
       */
      if (!(event.ctrlKey || event.shiftKey || event.altKey)) {
        _downKeys = _downKeys.slice(_downKeys.indexOf(keyNum));
      }
    }
  });

这里看起来很长,但其实需要关注的只有一行if (_downKeys.indexOf(key) === -1 && key !== 229) _downKeys.push(key);,这行代码也能解释为什么hotkeys-js不能支持重复键的问题,当有重复键输入的时候,会被过滤掉。

1.3 clearModifier

// 清除修饰键
// scr/index.js
function clearModifier(event) {
  let key = event.keyCode || event.which || event.charCode;
  const i = _downKeys.indexOf(key);

  // 从列表中清除按压过的键
  if (i >= 0) {
    _downKeys.splice(i, 1);
  }
  // 特殊处理 cmmand 键,在 cmmand 组合快捷键 keyup 只执行一次的问题
  if (event.key && event.key.toLowerCase() === 'meta') {
    _downKeys.splice(0, _downKeys.length);
  }

  // 修饰键 shiftKey altKey ctrlKey (command||metaKey) 清除
  if (key === 93 || key === 224) key = 91;
  if (key in _mods) {
    _mods[key] = false;

    // 将修饰键重置为false
    for (const k in _modifier) if (_modifier[k] === key) hotkeys[k] = false;
  }
}

而keyup回调里触发的另一个函数clearModifier,也对重复键进行了限制_downKeys.splice(i, 1);会在keyup的时候进行清除。
所以这就是两个地方对重复键的限制,弄清楚哪里有限制,才能更好的对代码进行破坏性最小的修改

2.修改源码

2.1 解决方案

回到需求,想要支持重复按钮,有以下几个条件:

  • 按键列表支持储存相同的键
  • 触发keyup事件后,不能马上将刚刚触发的键清除,否则无法实现类似"G+E+E"这种快捷键组合;但也不能不清除,否则就影响了正常的功能逻辑,导致快捷键功能不可用。

想清楚需要做的事情以后,就可以着手改源码了。

2.2 修改

首先定义一个变量let lastKeyDownKey = null;用来记录上次keydown的键,达到可以连续记录相同键的先决条件

if (typeof element !== 'undefined' && !isElementBind(element) && window) {
    elementHasBindEvent.push(element);
    addEvent(
      element,
      'keydown',
      (e) => {
        dispatch(e, element, 'keydown'); // 增加'keydown'参数
      },
      capture,
    );
    if (!winListendFocus) {
      winListendFocus = true;
      addEvent(
        window,
        'focus',
        () => {
          _downKeys = [];
        },
        capture,
      );
    }
    addEvent(
      element,
      'keyup',
      (e) => {
        dispatch(e, element, 'keyup'); // 增加 'keyup' 参数
        lastKeyDownKey = e.keyCode || e.which || e.charCode; // 保存上次按下的键
        setTimeout(() => { 
          clearModifier(e); // 延时触发clearModifier给触发连续按钮留下时间
        }, 500);			
      }, 
      capture,
    );
  }

// 处理keydown事件
function dispatch(event, element, type) {
  ...code...
  if (
    (type === 'keydown' && lastKeyDownKey === key) ||  // 当type为'keydown'时并且本次按下与上次按下的键相同时,可以存入重复键
    (_downKeys.indexOf(key) === -1 && key !== 229)
  )
    _downKeys.push(key);
 ...code...
};

// 清除修饰键
function clearModifier(event) {
  ...code...
  if (key in _mods) {
    _mods[key] = false;

    // 将修饰键重置为false
    for (const k in _modifier) if (_modifier[k] === key) hotkeys[k] = false;
    lastKeyDownKey = null; // 清空lastKeyDownKey的值
  }
}

具体修改如上,在每次keyup触发clearModifier事件时增加延时,并且记录每一次按下的键,与下次按键做对比,这样在同一个键连续keydown的时候可以被识别,实现类似G+E+E的快捷键

缺陷

虽然能满足重复键快捷键的需求,但是这明显有很多限制,比如延时清除太快,可能来不及按键就被清除;延时按键太慢,可能导致连续用两组快捷键会受影响;又比如原本hotkeys-js只支持同时按下,一定能触发快捷键,但现在可以分开按键,不过如果按键速度不对又可能导致不能触发对应想要的快捷键,必须有说明才行。所以本次修改算不完美,还有很多地方有待优化。

 类似资料: