在web项目开发过程中,经常会有配置快捷键的需求,我在网上搜索找到用的比较多的两个库: hotkeys-js(5.7K star),keymaster(6.5K start)。测试起来还是hotkeys-js好用一些,毕竟keymaster已经很久没有维护了。
但是hotkeys-js这个库有个问题是,不支持重复按键的快捷键,比如 G+E+E这种,但是不会有E+G+E这种需求,所以为了适配项目功能,才有了对hotkeys-js源码简单改造的事情
想要适配本地化需求,首先当然是分析为什么现在不能支持重复键,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)
,那这两个函数做了什么事情呢?分别来看看
// 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不能支持重复键的问题,当有重复键输入的时候,会被过滤掉。
// 清除修饰键
// 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的时候进行清除。
所以这就是两个地方对重复键的限制,弄清楚哪里有限制,才能更好的对代码进行破坏性最小的修改
回到需求,想要支持重复按钮,有以下几个条件:
想清楚需要做的事情以后,就可以着手改源码了。
首先定义一个变量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只支持同时按下,一定能触发快捷键,但现在可以分开按键,不过如果按键速度不对又可能导致不能触发对应想要的快捷键,必须有说明才行。所以本次修改算不完美,还有很多地方有待优化。