自定义快捷键实操与踩坑

发布时间 2024-01-10 14:56:50作者: 乐盘游

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();
}

image.png

4. 坑

中文输入法的兼容处理

keyCode为 229 时,判断为是中文输入法,以 code(KeyH)判断

浏览器级别按键冲突 2024-1-9

可参考下方的知乎回答,其中关于 e 动作解释的很好
https://zhuanlan.zhihu.com/p/300659062

CMD + W 类的事件和 CMD + S 类的事件有着本质差别,我们可以在原有的流程图上继续做一个推测,当浏览器对这部分优先级更高的快捷键做出不可逆的副作用响应时,listener 的 cb 即便 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