自定义快捷键实操与踩坑
0. 缘起
要做一个自定义快捷键的功能,web 端实现。这里分为两块逻辑,一部分是快捷键的应用,一部分是快捷键的定义。先从应用说起,快捷键实际上是对浏览器按键动作的监听,不过由于浏览器本身也有快捷键,就会有冲突的情况,自定义的要求应运而生。快捷键的定义,其实类似于设置的功能,也是存、取两个要点,不多解释。
1. 快捷键的监听
键盘事件中,event
事件类型为 KeyboardEvent
。
在本项目中,有两处特殊判断,1.26 字母触发以 keyCode(72)判断,2.兼容中文输入法下的快捷键,keyCode 为 229 时以 code(KeyH)判断,其他情况以 key(h)判断。
2. 功能函数 2024-1-10
export const AlphabetList =
"A、B、C、D、E、F、G、H、I、J、K、L、M、N、O、P、Q、R、S、T、U、V、W、X、Y、Z".split(
"、"
);
// 判断是否为26英文字符
export const is26Letter = (e) => {
// A 65 Z 90
const isEqualOrLargerThanA = e.keyCode >= 65;
const isEqualOrSmallerThanZ = e.keyCode <= 90;
return isEqualOrLargerThanA && isEqualOrSmallerThanZ;
};
// 根据keyCode获得字母 65 -> A
export const getKeyCode2Letter = (e) => {
const letter = AlphabetList[e.keyCode - 65];
return letter;
};
// 中文输入法特殊处理
export const getChineseInputLetter = (e) => {
// 根据code KeyH 获得按下的字母
const letter = e.code?.replace("Key", "");
return letter;
};
// 修饰符键组
const DecoratorKeyList = ["alt", "ctrl", "meta", "shift"];
// 首字母大写
export const getFirstLetterWordUpper = (word) => {
if (!(word && word.length)) {
return "";
}
const capitalized = word.charAt(0).toUpperCase() + word.slice(1);
return capitalized;
};
// 首字母小写
export const getFirstLetterWordLower = (word) => {
if (!(word && word.length)) {
return "";
}
const capitalized = word.charAt(0).toLowerCase() + word.slice(1);
return capitalized;
};
// 获取当前按键事件组合 eg. ['Ctrl','1']
export const getInputKey = (e) => {
// 如果keyCode处于26字母期间,这里需转换为对应的字符
const isLetter = is26Letter(e);
if (isLetter) {
return getKeyCode2Letter(e);
}
// ATTENTION: 中文输入法下,26键为Process keyCode 229 无法判断具体哪个
// 以code判断
const isProcess = e.keyCode === 229;
if (isProcess) {
return getChineseInputLetter(e);
}
// 字母全部转为大写,其他字符用key
// if (e.key && AlphabetList.includes(e.key.toUpperCase())) {
// return e.key.toUpperCase();
// }
return e.key;
};
// 获取修饰符组合
export const getDecoratorKey = (e) => {
const totalDecoratorKeyStatusList = DecoratorKeyList.reduce((prev, cur) => {
// 修饰符键组是否按下
if (e[`${cur}Key`]) {
// mac机型处理 如果按下command按键 效果同等于按下ctrlKey
if (cur === "meta") {
prev.push("Ctrl");
} else {
prev.push(getFirstLetterWordUpper(cur));
}
}
return prev;
}, []);
return totalDecoratorKeyStatusList;
};
// 接受事件,返回当前输入的快捷键组合 形如['Ctrl','A']
export const getShortcut = (e) => {
const array = getDecoratorKey(e);
array.push(getInputKey(e));
return array;
};
// 将文本字符串的按键记忆 以+号分割 转化为快捷键组合
// eg. 批量向下替换 Ctrl+Shift+1 => ['ctrl','shift','1']
export const getString2Shortcut = (text) => {
const array = text.split("+");
const letterUpperArray = array.map((key) => {
if (DecoratorKeyList.includes(key)) {
return getFirstLetterWordLower(key);
}
return key;
});
return letterUpperArray;
};
// 接受事件,返回当前输入的快捷键组合 形如Ctrl+A
export const getShortcut2Sring = (e) => {
const array = getDecoratorKey(e);
array.push(getInputKey(e));
return array.join("+");
};
// 判断当前按键是否符合传入事件 只有触发快捷键动作时才会启用
export const isSuitableEvent = (e, action) => {
// 否定守卫,如果在编辑快捷键期间,不允许触发已定义的快捷键
const isEdit = localStorage.getItem("isShortcutEditor")?.length;
if (isEdit) {
return false;
}
// step 1 获取按键事件
const inputActions = getShortcut(e);
// step 2 拆解传入事件
const needJudgeEventList = getString2Shortcut(action);
// step 3 是否每个按键都能找到对应值
const isEveryOneSuit = needJudgeEventList.every((input) =>
inputActions.includes(input)
);
return isEveryOneSuit;
};
// 快捷键重复的判断
// [{label:'保存', shortcut:'Ctrl+S',key:'save'}]
export const isExistShortcut = (str, shortCutsArray) => {
// step 1 拆分快捷键 Ctrl+A -> ['Ctrl','A']
const inputActions = getString2Shortcut(str);
// step 2 遍历当前快捷键组,看是否有分开后,每个按键都能找到对应
const isExist = shortCutsArray.some((item) => {
const { shortcut } = item;
// 这部分逻辑 和上面判断按键是否符合类似
const needJudgeEventList = getString2Shortcut(shortcut);
const isEveryOneSuit = needJudgeEventList.every((input) =>
inputActions.includes(input)
);
return isEveryOneSuit;
});
return isExist;
};
// 首字母大写的修饰符键组
const FirstLetterUpperDecoratorKeyList = DecoratorKeyList.map((key) =>
getFirstLetterWordUpper(key)
);
// 快捷键符合规定的判断 必须为一个修饰符+26字母
export const isLegalShortcut = (str) => {
// step 1 拆解快捷键
const inputActions = getString2Shortcut(str);
// step 2 判断是否合规 同时拥有一个修饰符、一个字母
// 因为这里要判断长度 所以用filter
// 一个修饰符
const isHaveOneDecoratorKey =
inputActions.filter((key) => FirstLetterUpperDecoratorKeyList.includes(key))
?.length === 1;
// 一个字母
const isHaveOneLetter =
inputActions.filter((key) => AlphabetList.includes(key))?.length === 1;
const isLegal = isHaveOneDecoratorKey && isHaveOneLetter;
return isLegal;
};
3. 使用
使用上方的功能 判断当前按下是否符合规则
const rule = "Alt+V";
const isValid = isSuitableEvent(e, rule);
console.log("isValid: ", isValid);
// if (e.keyCode === 49 && e.ctrlKey && e.shiftKey)
if (isValid) {
console.log("批量向下填充并替换");
// 批量向下替换
elem.blur();
that.batchDownReplacement(elem, 1);
// 注意此行代码,特殊快捷键一定要阻止浏览器动作!!!
e.preventDefault();
}
4. 坑
中文输入法的兼容处理
keyCode
为 229 时,判断为是中文输入法,以 code(KeyH)判断
mac 机型兼容
mac 机器上的ctrl
功能其实是command
键位,对应的是修饰符中的meta
。判断修饰符的时候,如果meta
键触发,则判定为ctrl
被按下即可。
浏览器级别按键冲突 2024-1-9
可参考下方的知乎回答,其中关于 e 动作解释的很好
https://zhuanlan.zhihu.com/p/300659062
CMD + W 类的事件和 CMD + S 类的事件有着本质差别,我们可以在原有的流程图上继续做一个推测,当浏览器对这部分优先级更高的快捷键做出不可逆的副作用响应时,listener 的 cb 即便 preventDefault 也将变得无能为力,因为更高优先级的副作用已经产生了
对于部分可以阻断的浏览器快捷键事件,可如下方操作提前阻断
e.preventDefault();
5. 参考
速查 key\which\code
https://www.zhangxinxu.com/wordpress/2021/01/js-keycode-deprecated/
由 code 查询对应字符
https://segmentfault.com/a/1190000005828048#comment-area
在线查询 key
https://www.dute.org/keycodes
两篇吃透按键事件:你应该了解的 js 键盘事件和使用注意事项
https://juejin.cn/post/7034682307667558437