场景
当我们使用addEventListener进行键盘事件监听时,对冒泡一定又爱又恨!爱是它使KeyboardEvent在DOM Tree向上传递,恨是它使KeyboardEvent在DOM Tree向上传递。试想如果要监听俩个不同Div的不同Delete按键,要怎么做:首先给Div设置不同id,然后监听键盘事件并根据id做状态转换。假如有十个区域呢,或者可以动态添加的区域呢?
这里就要介绍tabindex,它是是HTML全局属性,当给div加上tabindex=0时,鼠标(click)或者键盘(tab)可以触发其:focus伪类。最为关键一点是,当div:focus时,它有且仅有接受它子代事件的冒泡,这样就能把不同标签(不互为子代)的事件区分开来,成功了一半。
再试想,你想监听Div的Delete,但是它下面存在标签Input,当按下Delete时,Input原生事件和你监听的事件回调都会被执行,是不是很头疼,如果该Div下存在多个Input,直接爆炸。基于上面的理论,我们只需要阻止Input的事件冒泡或者Div主动防御Input传递过来的事件即可(可根据Div下可focus标签数量决定使用哪种方案,本篇为第二种)。
实现
import { Directive, DirectiveBinding } from "vue";
export type BindingType = {
keyMap?: {
// 比如:'Enter': () => log('按下了Enter')
[key: string]: <T>() => T
},
// 禁止的标签
banTitle?: string[],
}
// Vue3指令类型没有存储事件方法的属性(这样移除的时候不好操作),所以这里定义了一个Map
// 当然,你还可以使用mixin混入完整的生命周期,或者继承Vue3指令类型
const keyboardNodeList = new Map();
function getKey(event: KeyboardEvent) {
const key = event.key;
// 捕捉三个特殊键(Meta键位没有考虑)
if (["Control", "Shift", "Alt"].includes(key)) return "";
let preKey = "";
if (event.ctrlKey) {
preKey += "Ctrl+";
}
if (event.shiftKey) {
preKey += "Shift+";
}
if (event.altKey) {
preKey += "Alt+";
}
// shift键会导致字母大写
return preKey + key[0].toUpperCase() + key.slice(1);
}
function addKeyboardEventListener(el: HTMLElement, binding: DirectiveBinding<BindingType>) {
if (!el || !binding) return null;
return function (event: KeyboardEvent) {
const BindingValue = binding.value as BindingType;
if (!BindingValue.keyMap) return;
// 默认禁止input、a的键盘事件冒泡
const BanTitle = BindingValue.banTitle?.length ? BindingValue.banTitle : ['INPUT', 'A'];
// 全部转为大写
for (let i = 0; i < BanTitle.length; ++i) BanTitle[i].toLocaleUpperCase();
const modifiers = binding.modifiers;
// 如果带有阻止符,阻止此键盘事件的冒泡
if (modifiers.stop) event.stopPropagation?.();
// 如果是禁止的标签,不监听
const TagName = (event.target as HTMLElement).tagName || '';
if (TagName && BanTitle.includes(TagName)) return;
// 获取键盘key
const keyboard = getKey(event);
if (keyboard) {
// 执行映射的事件
BindingValue.keyMap[keyboard]?.();
}
};
}
export const DirectiveKeyboard: Directive<HTMLElement, BindingType> = {
created(el, binding) {
el.tabIndex = 0;
const keyboardFn = addKeyboardEventListener(el, binding);
if (keyboardFn) {
keyboardNodeList.set(el, keyboardFn);
el.addEventListener('keydown', keyboardFn);
}
},
mounted(el, binding) {
// el没有挂载时不能执行focus,必须在挂载之后
el.focus();
},
beforeUnmount(el, binding) {
const existKeyboardFn = keyboardNodeList.get(el);
// 卸载键盘监听事件
if (existKeyboardFn) {
el.removeEventListener('keydown', existKeyboardFn);
keyboardNodeList.delete(el);
}
},
};
然后就是在app启动的时候use这个指令啦!大家还可以创建directive.d.ts文件,使开发的时候能够得到指令的提示,不至于出现下面这种情况,至于怎么定义,可以参考下你所使用的ui组件库,这里我就不介绍了(我不会)。