大家好,我是前端西瓜哥。
快捷键操作在图形编辑器中是很高频的操作,能让用户快速高效地执行特定命令。
那么今天就来学习图形编辑器是如何做快捷键的管理的。
编辑器 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('ctrl+c', copy);
hotkeys-js 是原生事件的一层简单的封装,简化了写法并提高了可读性。
如果你的图形编辑器并不复杂,用一些易用性不错的快捷键库是不错的选择。
原生事件和一些常见的快捷键库可以处理一些简单的场景,但图形编辑器的场景往往更复杂。
图形编辑器还需要的快捷键高级能力有:
考虑上面这些功能点,我们来实现这个快捷键管理类 KeyBindingManager。
class KeyBindingManager { // 传入一个入口类对象 Editor,之后需要用到它的变量 constructor(private editor: Editor) {}}
一份快捷键绑定(keyBinding)由下面几个部分组成:
key,快捷键描述。理论上应该用 "Ctrl+C" 这种字符串来描述,但它实现起来比较麻烦,要解析,要转换(比如 / 要转成 Slash 去匹配 event.code)。
所以我换成了一个对象:{ CtrlKey: true, keyCode: 'KeyC' }。不用解析,不用转换,直接和 event 的属性对比即可。这个是 精准 匹配,即不能有多余的修饰键。
此外,key 也支持传入数组,这种情况比较少,对应一个行为有多个快捷键的情况。比如删除操作,我们可以传入 [{ keyCode: 'Delete' }, { keyCode: 'Backspace' }]。
winKey,快捷键描述(Windows 特供版)。这个参数是可选的,如果不提供,所有系统都会使用 key 参数。如果提供,且用户操作系统为 Windows,会使用 winKey,忽略 key。
when,是否满足上下文。也是可选的。when 是一个方法,可以通过它拿到一些上下文参数,通过这些参数决定返回的布尔值。如果为 true,表示匹配到了,并执行对应的响应行为;如果为 false,没匹配到,继续找下一个。when 可不提供,表示永远满足条件。
action,快捷键匹配后要执行的方法。
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 Map<number, 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 Map<number, IKeyBinding>(); map.set(id, keybinding); for (const [key, val] of this.keyBindingMap) { map.set(key, val); } this.keyBindingMap = map; this.id++; return id; }}
其实就是把这个快捷键注册到 Map 的开头。
如果你需要更细的粒度,比如低优先级、中优先级、高优先级,那你可以考虑传多一个优先级枚举值或一个数值,然后在正确的位置插入。感觉并没有太多需要用到这种粒度的场景。
然后就是快捷键的匹配逻辑:
实现如下:
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,执行其 action private 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 是否为 true(when 可不提供) 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,});
本文链接:http://www.28at.com/showinfo-26-12425-0.html图形编辑器开发:快捷键的管理
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com