大家好,我是前端西瓜哥。
快捷鍵操作在圖形編輯器中是很高頻的操作,能讓用戶快速高效地執(zhí)行特定命令。
那么今天就來(lái)學(xué)習(xí)圖形編輯器是如何做快捷鍵的管理的。
編輯器 github 地址:
https://github.com/F-star/suika
線上體驗(yàn):
https://blog.fstars.wang/app/suika/
我們先看看原生的鍵盤事件能否滿足需求。
假設(shè)我們需要判斷用戶是否按下了 Ctrl + C(需要精準(zhǔn)匹配),如果按下了就執(zhí)行 copy 方法。
用原生事件,我們要這樣寫:
window.addEventListener('keydown', (e) => { const { ctrlKey, shiftKey, altKey, metaKey } = e; if (ctrlKey && !shiftKey && !altKey && !metaKey && e.code === 'KeyC') { copy(); }})
寫法有點(diǎn)繁瑣。我們希望能簡(jiǎn)化一下寫法。
一開始我并不太在意快捷鍵綁定的管理,因?yàn)閺?fù)雜度還沒起來(lái),就找了一個(gè)輪子 hotkeys-js。
import hotkeys from 'hotkeys-js';hotkeys('ctrl+c', copy);
hotkeys-js 是原生事件的一層簡(jiǎn)單的封裝,簡(jiǎn)化了寫法并提高了可讀性。
如果你的圖形編輯器并不復(fù)雜,用一些易用性不錯(cuò)的快捷鍵庫(kù)是不錯(cuò)的選擇。
原生事件和一些常見的快捷鍵庫(kù)可以處理一些簡(jiǎn)單的場(chǎng)景,但圖形編輯器的場(chǎng)景往往更復(fù)雜。
圖形編輯器還需要的快捷鍵高級(jí)能力有:
考慮上面這些功能點(diǎn),我們來(lái)實(shí)現(xiàn)這個(gè)快捷鍵管理類 KeyBindingManager。
class KeyBindingManager { // 傳入一個(gè)入口類對(duì)象 Editor,之后需要用到它的變量 constructor(private editor: Editor) {}}
一份快捷鍵綁定(keyBinding)由下面幾個(gè)部分組成:
key,快捷鍵描述。理論上應(yīng)該用 "Ctrl+C" 這種字符串來(lái)描述,但它實(shí)現(xiàn)起來(lái)比較麻煩,要解析,要轉(zhuǎn)換(比如 / 要轉(zhuǎn)成 Slash 去匹配 event.code)。
所以我換成了一個(gè)對(duì)象:{ CtrlKey: true, keyCode: 'KeyC' }。不用解析,不用轉(zhuǎn)換,直接和 event 的屬性對(duì)比即可。這個(gè)是 精準(zhǔn) 匹配,即不能有多余的修飾鍵。
此外,key 也支持傳入數(shù)組,這種情況比較少,對(duì)應(yīng)一個(gè)行為有多個(gè)快捷鍵的情況。比如刪除操作,我們可以傳入 [{ keyCode: 'Delete' }, { keyCode: 'Backspace' }]。
winKey,快捷鍵描述(Windows 特供版)。這個(gè)參數(shù)是可選的,如果不提供,所有系統(tǒng)都會(huì)使用 key 參數(shù)。如果提供,且用戶操作系統(tǒng)為 Windows,會(huì)使用 winKey,忽略 key。
when,是否滿足上下文。也是可選的。when 是一個(gè)方法,可以通過(guò)它拿到一些上下文參數(shù),通過(guò)這些參數(shù)決定返回的布爾值。如果為 true,表示匹配到了,并執(zhí)行對(duì)應(yīng)的響應(yīng)行為;如果為 false,沒匹配到,繼續(xù)找下一個(gè)。when 可不提供,表示永遠(yuǎn)滿足條件。
action,快捷鍵匹配后要執(zhí)行的方法。
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; // 是否在拖拽中(比如移動(dòng)工具移動(dòng)圖形中)}
我們需要用有序表來(lái)根據(jù)注冊(cè)順序保存 keyBinding 的,這里我選擇用 Map 數(shù)據(jù)結(jié)構(gòu),它是一種有序數(shù)據(jù)結(jié)構(gòu)。
class KeyBindingManager { // 用 Map private keyBindingMap = new Map<number, IKeyBinding>(); private id = 0; //... // 注冊(cè)一個(gè)快捷鍵 register(keybinding: IKeyBinding) { const id = this.id; this.keyBindingMap.set(id, keybinding); this.id++; return id; } // 注銷快捷鍵 unregister(id: number) { this.keyBindingMap.delete(id); }}
注冊(cè)方法 register 會(huì)返回一個(gè)唯一 id,如果需要注銷,需要將這個(gè) id 傳給注銷方法 unregister。
事件的解綁方式有 3 種,這里選擇的是類似 setTimeout 返回一個(gè)訂閱 id 的風(fēng)格。
《事件訂閱的幾種實(shí)現(xiàn)風(fēng)格》
實(shí)際上 3 種寫法都沒啥差別,都是要把綁定事件方法返回的結(jié)果保存下來(lái),在合適的時(shí)機(jī)調(diào)用解綁方法。
哦對(duì)了,還有注冊(cè)高優(yōu)先級(jí)快捷鍵的方法:
class KeyBindingManager { // ... // 綁定一個(gè)高優(yōu)先級(jí)快捷鍵綁定(會(huì)放到 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; }}
其實(shí)就是把這個(gè)快捷鍵注冊(cè)到 Map 的開頭。
如果你需要更細(xì)的粒度,比如低優(yōu)先級(jí)、中優(yōu)先級(jí)、高優(yōu)先級(jí),那你可以考慮傳多一個(gè)優(yōu)先級(jí)枚舉值或一個(gè)數(shù)值,然后在正確的位置插入。感覺并沒有太多需要用到這種粒度的場(chǎng)景。
然后就是快捷鍵的匹配邏輯:
實(shí)現(xiàn)如下:
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,執(zhí)行其 action private handleAction = (e: KeyboardEvent) => { if ( e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement ) { return; } let isMatch = false; // 生成上下文對(duì)象,可根據(jù)需要擴(kuò)充 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 操作系統(tǒng),看看 winKey 對(duì)不對(duì) if (isWindows) { if (keyBinding.winKey && this.isKeyMatch(keyBinding.winKey, e)) { isMatch = true; } } // 其他操作系統(tǒng),看 key 是否匹配 else if (this.isKeyMatch(keyBinding.key, e)) { isMatch = true; } } // 匹配 if (isMatch) { e.preventDefault(); keyBinding.action(e); // 執(zhí)行對(duì)應(yīng) action(行為) break; // 結(jié)束,不繼續(xù)遍歷 } } }; 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' }], // 只能在沒有發(fā)生拖拽的情況下下刪除(比如移動(dòng)圖形時(shí)不能刪除) when: (ctx) => !ctx.isToolDragging, action: deleteAction,});
復(fù)制快捷鍵的寫法:
const copyHandler = () => { // 復(fù)制}editor.keybindingManager.register({ key: { metaKey: true, keyCode: 'KeyC' }, // Windows 環(huán)境下的快捷鍵 winKey: { ctrlKey: true, keyCode: 'KeyC' }, action: copyHandler,});
本文鏈接:http://www.www897cc.com/showinfo-26-12425-0.html圖形編輯器開發(fā):快捷鍵的管理
聲明:本網(wǎng)頁(yè)內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問(wèn)題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com
上一篇: HTTP協(xié)議揭秘:探尋互聯(lián)網(wǎng)的背后密碼、探秘?cái)?shù)據(jù)傳輸?shù)膴W秘