找做网站公司,教程推广优化网站排名,佛山网站优化多少钱,基本建筑网站大家好#xff0c;我是前端西瓜哥。
快捷键操作在图形编辑器中是很高频的操作#xff0c;能让用户快速高效地执行特定命令#xff0c;今天讲讲图形编辑器如果管理快捷键。 编辑器 github 地址#xff1a; https://github.com/F-star/suika 线上体验#xff1a; https://b…大家好我是前端西瓜哥。
快捷键操作在图形编辑器中是很高频的操作能让用户快速高效地执行特定命令今天讲讲图形编辑器如果管理快捷键。 编辑器 github 地址 https://github.com/F-star/suika 线上体验 https://blog.fstars.wang/app/suika/ 简单的快捷键绑定
我们先看看原生的键盘事件能否满足需求。
假设我们需要判断用户是否按下了 Ctrl C需要精准匹配如果按下了就执行 copy 方法。
用原生事件我们要这样写
window.addEventListener(keydown, (e) {const { ctrlKey, shiftKey, altKey, metaKey } e;if (ctrlKey !shiftKey !altKey !metaKey e.code KeyC) {copy();}
})写法有点繁琐。我们希望能简化一下写法。
一开始我并不太在意快捷键绑定的管理因为复杂度还没起来就找了一个轮子 hotkeys-js。
import hotkeys from hotkeys-js;hotkeys(ctrlc, copy);hotkeys-js 是原生事件的一层简单的封装简化了写法并提高了可读性。
如果你的图形编辑器并不复杂用一些通用性不错的快捷键库是不错的选择。
快捷键高级能力
原生事件和一些简单的快捷键库可以处理一些简单的情况但图形编辑器的场景往往更复杂。
图形编辑器还需要的快捷键高级能力有
给一个响应函数设置多个不同快捷键比如 Delete 或 Backspace 都可以删除选中元素这个大多第三方快捷键轮子是支持的可以根据不同操作系统绑定不同的快捷键比如复制我希望在 Windows 系统为 CtrlC在 MacOS 系统则是 CommandC提供环境上下文绑定的函数可以通过它决定是否被调用比如我希望移动图形的时候不能执行 Delete 对应删除操作支持短路匹配只执行第一个匹配条件。这是为了防止快捷键冲突一个快捷键执行了多个行为。当然如果你就是希望一个快捷键要执行多个行为那可以考虑补充一个 next 方法。某个快捷键绑定可以设置为高优先级比如激活某个工具时要注册一些快捷键需要高优先级以便覆盖掉和其他的同名快捷键
快捷键管理类
考虑上面这些功能点我们来实现这个快捷键管理类 KeyBindingManager。
class KeyBindingManager {// 传入一个入口类对象 Editor之后需要用到它的变量constructor(private editor: Editor) {}
}keyBinding 对象
一份快捷键绑定keyBinding由下面几个部分组成
1key快捷键描述。理论上应该用 CtrlC 这种字符串来描述但它实现起来比较麻烦要解析要转换比如 / 要转成 Slash 去匹配 event.code。
所以我换成了一个对象{ CtrlKey: true, keyCode: KeyC }。不用解析不用转换直接和 event 的属性对比即可。这个是 精准 匹配即不能有多余的修饰键。
此外key 也支持传入数组这种情况比较少对应一个行为有多个快捷键的情况。比如删除操作我们可以传入 [{ keyCode: Delete }, { keyCode: Backspace }]。
2winKey快捷键描述Windows 特供版。这个参数是可选的如果不提供所有系统都会使用 key 参数。如果提供且用户操作系统为 Windows会使用 winKey忽略 key。
3when是否满足上下文。也是可选的。when 是一个方法可以通过它拿到一些上下文参数通过这些参数决定返回的布尔值。如果为 true表示匹配到了并执行对应的响应行为如果为 false没匹配到继续找下一个。when 可不提供表示永远满足条件。
4action快捷键匹配后要执行的方法。
TypeScript 类型签名为
interface IKeyBinding {key: IKey | IKey[];winKey?: IKey | IKey[];when?: (ctx: IWhenCtx) boolean;action: (e: KeyboardEvent) void;
}interface IKey {ctrlKey?: boolean;shiftKey?: boolean;altKey?: boolean;metaKey?: boolean;/*** KeyboardEvent[code] 或 *(匹配任何案件)*/keyCode: string;
}interface IWhenCtx {isToolDragging: boolean; // 是否在拖拽中比如移动工具移动图形中
}快捷键注册
我们需要用有序表来根据注册顺序保存 keyBinding 的这里我选择用 Map 数据结构它是一种有序数据结构。
class KeyBindingManager {// 用 Map private keyBindingMap new Mapnumber, IKeyBinding();private id 0;//...// 注册一个快捷键register(keybinding: IKeyBinding) {const id this.id;this.keyBindingMap.set(id, keybinding);this.id;return id;}// 注销快捷键unregister(id: number) {this.keyBindingMap.delete(id);}
}注册方法 register 会返回一个唯一 id如果需要注销需要将这个 id 传给注销方法 unregister。 事件的解绑方式有 3 种这里选择的是类似 setTimeout 返回一个订阅 id 的风格。 《事件订阅的几种实现风格》 实际上 3 种写法都没啥差别都是要把绑定事件方法返回的结果保存下来在合适的时机调用解绑方法。 哦对了还有注册高优先级快捷键的方法
class KeyBindingManager {// ...// 绑定一个高优先级快捷键绑定会放到 Map 的开头registerWithHighPrior(keybinding: IKeyBinding) {const id this.id;const map new Mapnumber, IKeyBinding();map.set(id, keybinding);for (const [key, val] of this.keyBindingMap) {map.set(key, val);}this.keyBindingMap map;this.id;return id;}
}其实就是把这个快捷键注册到 Map 的开头。
如果你需要更细的粒度比如低优先级、中优先级、高优先级那你可以考虑传多一个优先级枚举值或一个数值然后在正确的位置插入。感觉并没有太多需要用到这种粒度的场景。
短路匹配逻辑
然后就是快捷键的匹配逻辑
匹配顺序根据注册顺序有特例就是前面说的高优先级快捷键绑定会插队插到队伍开头使用精准匹配key 或 winKey以及 when 方法是否为 true都为 true 时执行 action使用短路逻辑即只执行第一个匹配的后面可能也有其他匹配的但不执行。这个其实是设计模式的责任链模式像是 express 或 koa 的路由匹配机制也是责任链模式。
实现如下
const isWindows navigator.platform.toLowerCase().includes(win) ||navigator.userAgent.includes(Windows);class KeyBindingManager {// ...// 绑定到原生键盘按下事件上bindEvent() {if (this.isBound) return;this.isBound true;document.addEventListener(keydown, this.handleAction);}// 找到匹配的 keyBinding执行其 actionprivate handleAction (e: KeyboardEvent) {if (e.target instanceof HTMLInputElement ||e.target instanceof HTMLTextAreaElement) {return;}let isMatch false;// 生成上下文对象可根据需要扩充const ctx: IWhenCtx {isToolDragging: this.editor.toolManager.isDragging,};for (const keyBinding of this.keyBindingMap.values()) {// 先看看 when 是否为 truewhen 可不提供if (!keyBinding.when || keyBinding.when(ctx)) {// 如果是 Windows 操作系统看看 winKey 对不对if (isWindows) {if (keyBinding.winKey this.isKeyMatch(keyBinding.winKey, e)) {isMatch true;}}// 其他操作系统看 key 是否匹配else if (this.isKeyMatch(keyBinding.key, e)) {isMatch true;}}// 匹配if (isMatch) {e.preventDefault();keyBinding.action(e); // 执行对应 action行为break; // 结束不继续遍历}}};private isKeyMatch(key: IKey | IKey[], e: KeyboardEvent): boolean {if (Array.isArray(key)) {return key.some((k) this.isKeyMatch(k, e));}if (key.keyCode *) return true;const {ctrlKey false,shiftKey false,altKey false,metaKey false,} key;return (ctrlKey e.ctrlKey shiftKey e.shiftKey altKey e.altKey metaKey e.metaKey key.keyCode e.code);}
}用法举例
类写好了看看用法。
删除快捷键的写法
const deleteAction () {// 删除选中元素
};
editor.keybindingManager.register({// Backspace 或 Delete 都可以删除key: [{ keyCode: Backspace }, { keyCode: Delete }],// 只能在没有发生拖拽的情况下下删除比如移动图形时不能删除when: (ctx) !ctx.isToolDragging,action: deleteAction,
});复制快捷键的写法
const copyHandler () {// 复制
}editor.keybindingManager.register({key: { metaKey: true, keyCode: KeyC },// Windows 环境下的快捷键winKey: { ctrlKey: true, keyCode: KeyC },action: copyHandler,
});一些优化点
如果你考虑一些非美式键盘比如法语键盘因为按键布局位置发生了变化需要做键位的重映射确保物理位置不变确保用户的肌肉记忆有效。简化快捷键描述写法使用类似 Ctrl/ 的更简洁写法。如果你需要像 VSCode 一样提供 JSON 文件给用户设置快捷键这个还是要实现的。
结尾
我是前端西瓜哥欢迎关注我学习更多图形编辑器知识。