☰
canvas-editor 自定义快捷键开发指南:从 KeyMap 注册到全局/编辑器级事件分发
2026/10/3 2:27:13 网站建设 项目流程
  • 前端
  • UI组件
  • 富文本

【免费下载链接】canvas-editor

A Canvas/SVG-based rich text editor

项目地址:https://gitcode.com/gh_mirrors/ca/canvas-editor
点击查看免费下载

本文聚焦 canvas-editor(一个基于 Canvas/SVG 的富文本编辑器)的快捷键扩展机制,讲解如何通过instance.register.shortcutList注册自定义快捷键、理解IRegisterShortcut各字段(mod、isGlobal、disable等)的语义与平台差异,并结合 Shortcut.ts 的源码说明内部快捷键表、事件绑定与匹配分发流程,让读者能基于此为编辑器定制完整可用的快捷键方案。

如何注册自定义快捷键

基础用法

canvas-editor 的注册入口统一由Register模块暴露。创建编辑器实例后,通过instance.register.shortcutList([...])即可批量注入自定义快捷键:

import Editor from "@hufe921/canvas-editor" const instance = new Editor(container, <IElement[]>data, options) instance.register.shortcutList([ { key: KeyMap; ctrl?: boolean; meta?: boolean; mod?: boolean; // windows:ctrl || mac:command shift?: boolean; alt?: boolean; isGlobal?: boolean; callback?: (command: Command) => any; disable?: boolean; } ])

其中:

  • key为必填,取值为KeyMap枚举;
  • ctrl、meta、mod、shift、alt、isGlobal、disable、callback均为可选项;
  • 一个调用可以传入多条快捷键配置,编辑器会按注册顺序逐一匹配。

从源码结构看,Register.shortcutList只是对Shortcut.registerShortcutList的绑定转发(见 Register.ts),实际执行逻辑由 Shortcut.ts 中的registerShortcutList完成:

public registerShortcutList(payload: IRegisterShortcut[]) { this._addShortcutList(payload) }

完整可运行示例

以下示例注册一个「Ctrl/Cmd + K」插入超链接的快捷键(假设数据中已存在待选中的超链接文本),同时注册一个全局可用的「Ctrl/Cmd + H」聚焦搜索框的快捷键:

import Editor, { KeyMap } from "@hufe921/canvas-editor" const instance = new Editor(container, data, options) instance.register.shortcutList([ { key: KeyMap.K, mod: true, callback: (command) => { command.executeHyperlink() } }, { key: KeyMap.H, mod: true, isGlobal: true, callback: (command) => { command.executeFocus() } } ])

command是 Command 实例,其暴露了丰富的executeXxx命令方法(如executeBold、executeItalic、executeTitle等),自定义快捷键的回调可直接调用这些命令,或编写任意自定义逻辑。

IRegisterShortcut 配置项详解

接口定义位于 src/editor/interface/shortcut/Shortcut.ts:

export interface IRegisterShortcut { key: KeyMap ctrl?: boolean meta?: boolean mod?: boolean // windows:ctrl || mac:command shift?: boolean alt?: boolean // windows:alt || mac:option isGlobal?: boolean callback?: (command: Command) => void disable?: boolean }
字段类型必填说明
keyKeyMap是要匹配的按键,取自KeyMap枚举
ctrlboolean否要求按下 Ctrl 键
metaboolean否要求按下 Meta(Mac 的 Command)键
modboolean否跨平台修饰键:Windows 上等价于 ctrl,Mac 上等价于 command
shiftboolean否要求按下 Shift 键
altboolean否要求按下 Alt 键(Mac 上为 Option)
isGlobalboolean否是否为全局快捷键(不依赖编辑器焦点)
callback(command: Command) => void否命中快捷键后的回调,接收Command实例
disableboolean否是否禁用该快捷键

key 的取值:KeyMap 枚举

key的类型是KeyMap枚举,定义于 src/editor/dataset/enum/KeyMap.ts。它同时覆盖了功能键、方向键、标点与字母数字键:

  • 功能键:Delete、Backspace、Enter、Escape、Tab、Home、End、Alt、Meta;
  • 方向键:Left(ArrowLeft)、Right(ArrowRight)、Up(ArrowUp)、Down(ArrowDown);
  • 标点:[、]、,、.、<、>、=、-、+;
  • 字母:A~Z(小写'a'~'z'),以及A_UPPERCASE~Z_UPPERCASE(大写'A'~'Z');
  • 数字:ZERO~NINE('0'~'9')。
export enum KeyMap { Delete = 'Delete', Backspace = 'Backspace', Enter = 'Enter', Left = 'ArrowLeft', Right = 'ArrowRight', Up = 'ArrowUp', Down = 'ArrowDown', ESC = 'Escape', TAB = 'Tab', LEFT_BRACKET = '[', RIGHT_BRACKET = ']', COMMA = ',', PERIOD = '.', LEFT_ANGLE_BRACKET = '<', RIGHT_ANGLE_BRACKET = '>', A = 'a', B = 'b', // ... A_UPPERCASE ~ Z_UPPERCASE, ZERO ~ NINE }

注册时建议使用枚举成员而非裸字符串,例如KeyMap.B而不是'b',以便获得类型提示与拼写校验。

修饰键语义与平台差异

mod 与 ctrl / meta 的关系

  • mod: true表示「Windows 上按 Ctrl,macOS 上按 Command」,由 hotkey.ts 中的isMod工具函数实现:
import { isApple } from './ua' export function isMod(evt: KeyboardEvent | MouseEvent) { return isApple ? evt.metaKey : evt.ctrlKey }
  • isApple通过navigator.userAgent中的Mac OS X判断当前是否为 macOS(见 ua.ts)。
  • alt: true在 Mac 上对应 Option 键(altKey),字段注释为windows:alt || mac:option。

因此,如果希望快捷键在 Windows 与 macOS 上行为一致,应优先使用mod;如果只想监听某个平台特有的组合键,则分别使用ctrl或meta。

匹配逻辑(源码级)

Shortcut._execute是按键匹配的核心(Shortcut.ts):

private _execute(evt: KeyboardEvent, shortCutList: IRegisterShortcut[]) { for (let s = 0; s < shortCutList.length; s++) { const shortCut = shortCutList[s] if ( (shortCut.mod ? isMod(evt) === !!shortCut.mod : evt.ctrlKey === !!shortCut.ctrl && evt.metaKey === !!shortCut.meta) && evt.shiftKey === !!shortCut.shift && evt.altKey === !!shortCut.alt && evt.key.toLowerCase() === shortCut.key.toLowerCase() ) { if (!shortCut.disable) { shortCut?.callback?.(this.command) evt.preventDefault() } break } } }

匹配规则要点:

  1. 组合修饰键:shift、alt必须与事件中的shiftKey、altKey严格相等(即配置为true时要求按下,配置为false/缺省时要求未按下);
  2. mod 优先:配置了mod时走isMod(evt)判断;未配置mod时,ctrl与meta分别对应ctrlKey与metaKey;
  3. 按键大小写不敏感:evt.key.toLowerCase() === shortCut.key.toLowerCase(),因此注册KeyMap.B(大写)或KeyMap.b(小写)都能命中B键;
  4. 首个命中即中断:快捷键列表按注册顺序遍历,命中第一条即break,不再继续匹配后续项。因此,若两条配置存在冲突,先注册的优先;
  5. 命中即拦截:callback执行后调用evt.preventDefault(),阻止浏览器默认行为(例如阻止 Ctrl+B 触发浏览器加粗/收藏等);
  6. disable 语义:当disable: true时,该条快捷键被跳过不执行,但仍然会中断遍历(break依然执行)。利用这一特性,可以用disable覆盖内部快捷键的默认行为。

isGlobal:全局快捷键与编辑器快捷键

Shortcut在构造时会把快捷键表拆分为两个列表(Shortcut.ts):

private _addShortcutList(payload: IRegisterShortcut[]) { for (let s = payload.length - 1; s >= 0; s--) { const shortCut = payload[s] if (shortCut.isGlobal) { this.globalShortcutList.unshift(shortCut) } else { this.agentShortcutList.unshift(shortCut) } } }

两个列表分别绑定在两类事件源上:

  • 全局快捷键(globalShortcutList):通过document.addEventListener('keydown', this._globalKeydown)绑定在document上(Shortcut.ts)。无论焦点是否在编辑器内都会触发;
  • 编辑器快捷键(agentShortcutList):通过光标代理 DOM(draw.getCursor().getAgentDom())的keydown事件触发(Shortcut.ts)。仅当编辑器获得焦点(光标代理处于活动状态)时生效。

使用建议:

  • 需要「无论焦点在何处都响应的快捷键」(如呼出搜索框、切换主题、保存草稿)→ 设置isGlobal: true;
  • 只在编辑文本时生效的快捷键(如加粗、标题)→ 保持默认(isGlobal: false);
  • 注意全局快捷键会注册在document上,编辑器的removeEvent()会移除该监听,销毁实例时应调用清理方法以免事件泄漏。

快捷键注册机制与内部快捷键

注册转发链路

Register.ts 将快捷键注册方法绑定到实例上,形成instance.register.shortcutList(...)的调用链:

this.shortcutList = shortcut.registerShortcutList.bind(shortcut)

外部注册的快捷键通过_addShortcutList与内置快捷键一同进入全局/编辑器两个列表,无需额外配置即参与同一套匹配与分发流程。

内置快捷键表

编辑器内置了三组快捷键表,均在 Shortcut.ts 构造时注入:

this._addShortcutList([...richtextKeys, ...titleKeys, ...listKeys])
  • richtextKeys:富文本格式类快捷键,如mod + [/mod + ]增大/减小字号、mod + B加粗、mod + I斜体、mod + U下划线、mod + L/E/R/J左/中/右/两端对齐、mod + Shift + J分散对齐、Ctrl + Shift + X删除线等。其中上下标的按键做了平台适配(isApple ? KeyMap.COMMA : KeyMap.RIGHT_ANGLE_BRACKET);
  • titleKeys:标题类快捷键,Ctrl + Alt + 0~6分别对应正文与 H1~H6,回调调用command.executeTitle(...);
  • listKeys:列表类快捷键,mod + Shift + I无序列表、mod + Shift + U有序列表。

完整的内部快捷键清单可参考 shortcut-internal.md(如Ctrl/Cmd + Z撤销、Ctrl/Cmd + Y重做、Ctrl/Cmd + C/X/A复制/剪切/全选、Ctrl/Cmd + S保存、方向键与Shift + 方向键选区缩放、Esc退出格式刷、Tab/Shift + Tab缩进与控件切换等)。

测试验证

仓库为快捷键模块提供了单元测试 tests/core/shortcut/Shortcut.test.ts,其中验证了「注册自定义快捷键不抛异常」的核心行为:

it('registerShortcutList 注册自定义快捷键', () => { ctx = createTestEditor() const callback = vi.fn() const shortcutList = [ { key: 'k', mod: true, shift: false, alt: false, ctrl: false, meta: false, isGlobal: false, disable: false, callback } ] expect(() => { ctx.editor.command.executeFocus() ctx.editor.use((_editor, options) => { options?.register?.shortcutList?.(shortcutList) }) }).not.toThrow() })

测试通过createTestEditor()创建真实编辑器实例,在获得焦点后通过instance.use(...)注册快捷键,验证注册流程的健壮性。这同时也展示了另一种注册时机:在插件/扩展的use钩子中通过options.register.shortcutList注入快捷键。

实践建议

  1. 优先使用mod而不是分别指定ctrl/meta,保证跨平台体验一致;
  2. 避免与内置快捷键冲突:先查阅 shortcut-internal.md 的默认表;若确需覆盖,可注册同键位配置并利用「先注册先命中」的规则,或借助disable: true禁用内置项(注意禁用项也会中断匹配);
  3. 区分全局与编辑器内快捷键:isGlobal: true的快捷键在编辑器失焦时也会响应,请谨慎使用,避免影响页面其他交互;
  4. 回调尽量使用Command的executeXxx命令,以保持与撤销/重做历史、数据模型的一致性;
  5. 大小写统一:匹配对大小写不敏感,但建议统一使用KeyMap枚举,便于维护与类型检查。

相关文档

  • 内部快捷键完整清单:docs/en/guide/shortcut-internal.md
  • 快捷键接口定义:src/editor/interface/shortcut/Shortcut.ts
  • 快捷键核心实现:src/editor/core/shortcut/Shortcut.ts
  • 内部快捷键表:src/editor/core/shortcut/keys/richtextKeys.ts、src/editor/core/shortcut/keys/titleKeys.ts、src/editor/core/shortcut/keys/listKeys.ts
  • 快捷键模块测试:tests/core/shortcut/Shortcut.test.ts
  • 前端
  • UI组件
  • 富文本

【免费下载链接】canvas-editor

A Canvas/SVG-based rich text editor

项目地址:https://gitcode.com/gh_mirrors/ca/canvas-editor
点击查看免费下载

相关推荐

上一篇:PostHog 安全审计 Skill:面向 SaaS 多租户系统的校准式 AI 安全审计工作流
下一篇:用 GitHub 环境教学:ML-For-Beginners 机器学习课程的课堂部署与运营指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询