GrapesJS Keymaps 模块完全指南:自定义编辑器快捷键
【免费下载链接】grapesjsFree and Open source Web Builder Framework. Next generation tool for building templates without coding项目地址: https://gitcode.com/GitHub_Trending/gr/grapesjs
导读
GrapesJS 是一个无需编码即可搭建模板的开源 Web 构建框架(Free and Open source Web Builder Framework),其 Keymaps 模块负责统一管理编辑器内所有键盘快捷键的注册、绑定、触发与解绑。本文将围绕 docs/api/keymaps.md 展开,完整讲解如何通过初始化配置预置快捷键、如何调用editor.Keymaps的六大 API 方法、如何监听keymap:add/keymap:remove/keymap:emit三类事件,并结合 packages/core/src/keymaps 源码与测试用例,深入剖析快捷键的解析规则、执行门控与内置默认快捷键表。读完本文,你将能够在 GrapesJS 中为任意命令或回调函数绑定跨平台快捷键,并理解其底层实现原理。
一、初始化配置:在编辑器启动时预置快捷键
Keymaps 模块允许你在grapesjs.init时通过keymaps.defaults配置初始快捷键集合。配置项在源码 packages/core/src/keymaps/config.ts 中定义为defaults?: Record<string, Omit<Keymap, 'id'> & { opts?: KeymapOptions }>,即每个 keymap 对象包含keys(按键序列)、handler(命令 ID 或函数)以及可选的opts。
官方文档给出如下示例(原文档中'your-namespace:keymap-name' {缺少冒号,此处为修正后的完整可运行写法):
const editor = grapesjs.init({ keymaps: { // Object of keymaps defaults: { 'your-namespace:keymap-name': { keys: '⌘+z, ctrl+z', handler: 'some-command-id' }, // ... } } })当编辑器完成加载时,模块会读取该配置并逐一注册。在源码 packages/core/src/keymaps/index.ts 的onLoad()方法中可以看到:
onLoad() { if (this.em.isHeadless) return; const defKeys = this.config.defaults; for (let id in defKeys) { const value = defKeys[id]; this.add(id, value.keys, value.handler, value.opts || {}); } }两个值得注意的实现细节:
- headless 模式不加载快捷键:当编辑器以 headless(无头/无 DOM)模式运行时,
onLoad直接返回,不会注册任何快捷键; - 默认配置可整体覆盖:源码默认配置是
defaults: { 'core:undo': ..., ... }的固定对象,如果你传入keymaps: { defaults: [] }(数组),for...in循环不会产生任何注册,等价于"清空全部默认快捷键"。这正是测试 packages/core/test/specs/keymaps/index.js 中使用new Editor({ keymaps: { defaults: [] } })来验证"No keymaps inside"的原因。
二、获取模块实例
快捷键注册完成后,可在编辑器实例上通过Keymaps属性获取模块并调用其 API。官方文档明确指出:
const keymaps = editor.Keymaps;在源码 packages/core/src/editor/model/Editor.ts 中,该属性由 getter 返回模块实例:
get Keymaps(): KeymapsModule { return this.get('Keymaps'); }KeymapsModule是Module抽象类的子类(见 packages/core/src/keymaps/index.ts),并在 Editor.ts 的依赖列表 中作为核心模块被挂载,因此只要编辑器实例存在,editor.Keymaps即可直接使用。
三、事件系统
Keymaps 模块通过编辑器的事件总线(editor.on(...))向外广播三类事件,事件名在 packages/core/src/keymaps/types.ts 的KeymapsEvents枚举中定义。
1.keymap:add— 新快捷键已添加
新 keymap 对象作为回调参数传入:
editor.on('keymap:add', (keymap) => { ... });在add()实现末尾通过em.trigger(events.add, keymap)触发(index.ts),测试中也验证了该事件会被触发(见 keymaps/index.js)。
2.keymap:remove— 快捷键已移除
被移除的 keymap 对象作为回调参数传入:
editor.on('keymap:remove', (keymap) => { ... });该事件在remove()中通过em?.trigger(events.remove, keymap)触发。
3.keymap:emit— 快捷键被触发执行
当某个快捷键被按下并成功执行时触发,回调接收三个参数:keymapId(快捷键 ID)、shortcutUsed(实际匹配的按键串)、event(原生键盘事件):
editor.on('keymap:emit', (keymapId, shortcutUsed, event) => { ... });值得展开的是:源码在触发事件时还会额外触发一个按 ID 细分的事件(index.ts):
const args = [id, h.shortcut, e]; em.trigger(events.emit, ...args); em.trigger(`${events.emitId}${id}`, ...args);其中emitId = 'keymap:emit:'(见 types.ts),因此你还可以监听形如keymap:emit:ns:my-keymap的事件,只针对某一个快捷键的触发做出响应。KeymapEvent类型联合中同样包含了keymap:emit:${string}这种模板字面量类型(types.ts)。
四、API 方法详解
官方文档列出的六个方法(getConfig/add/get/getAll/remove/removeAll)均可从keymaps实例直接调用,以下逐一说明签名、参数与示例。
1.getConfig()
获取模块配置对象(即初始化时传入的keymaps配置):
keymaps.getConfig(); // -> { defaults: { 'core:undo': {...}, ... } }返回类型为Object,其结构由 config.ts 的 KeymapsConfig 约束。
2.add(id, keys, handler, opts)
添加新的快捷键,这是模块最核心的方法。签名如下:
| 参数 | 类型 | 说明 |
|---|---|---|
id | string | Keymap 的唯一标识,建议使用命名空间:名称的格式,如'ns:my-keymap' |
keys | string | 按键组合,多个组合用逗号分隔,如'ctrl+a'、'⌘+z, ctrl+z' |
handler | Function \| string | 处理逻辑,可以是回调函数,也可以是 GrapesJS 命令 ID 字符串 |
opts | Object | 可选参数,默认{} |
opts.force | Boolean | 是否强制执行 handler(即使处于编辑模式或输入焦点),默认false |
opts.prevent | Boolean | 是否阻止原始事件的默认行为(如浏览器保存页面),默认false |
官方文档示例:
// 'ns' is just a custom namespace keymaps.add('ns:my-keymap', '⌘+j, ⌘+u, ctrl+j, alt+u', editor => { console.log('do stuff'); }); // or keymaps.add('ns:my-keymap', '⌘+s, ctrl+s', 'some-gjs-command', { // Prevent the default browser action prevent: true, }); // listen to events editor.on('keymap:emit', (id, shortcut, event) => { // ... })handler 解析规则:从源码 index.ts 可以看到,字符串形式的 handler 会被解析为命令:
const handlerRes = isString(handler) ? cmd.get(handler) : handler;最终执行时,如果 handler 是函数则直接以handlerRes(editor, 0, opt)调用;如果是命令对象,则通过cmd.runCommand(handlerRes, opt)执行,其中opt携带了原生事件event与 keymaster 的匹配句柄h。这意味着你可以把add的第三个参数直接写成一个已注册命令的 ID(如'core:undo'、'core:copy'),或者写成一个接收editor作为第一个参数的函数。
重复添加处理:如果传入的id已存在,add会先自动remove(id)再重新注册(index.ts),保证同一 ID 只会保留一个绑定。
3.get(id)
根据 ID 获取已注册的 keymap:
keymaps.get('ns:my-keymap'); // -> {keys, handler};返回值即{ id, keys, handler }对象(index.ts),测试 keymaps/index.js 验证了get与add后数据的严格相等关系。
4.getAll()
获取全部已注册的 keymap,返回以 ID 为键的对象:
keymaps.getAll(); // -> {id1: {}, id2: {}};5.remove(id)
根据 ID 移除快捷键,返回被移除的 keymap 对象:
keymaps.remove('ns:my-keymap'); // -> {keys, handler};源码实现中(index.ts),移除时会遍历keymap.keys.split(', '),逐个调用 keymaster 的unbind(k.trim())解绑底层键盘监听,并触发keymap:remove事件。测试 keymaps/index.js 验证了移除后get返回undefined、getAll()返回空对象、且事件被触发。
6.removeAll()
移除全部已绑定的 keymap,并清空 keymaster 底层全部处理器,返回模块实例本身(支持链式调用):
keymaps.removeAll();源码实现为(index.ts):
removeAll() { Object.keys(this.keymaps).forEach((keymap) => this.remove(keymap)); keymaster.handlers = {}; return this; }该实例在模块destroy()时也会被调用,用于编辑器销毁时的资源清理。
五、快捷键语法解析:keymaster 底层实现
Keymaps 模块的键盘监听基于仓库自研的 keymaster 适配实现 packages/core/src/utils/keymaster.ts(源码头部注明其初始版本源自 madrobby/keymaster 并针对 GrapesJS 做了适配)。理解其解析规则,才能写出准确可用的keys字符串。
1. 修饰键写法
keymaster 定义了一套跨平台修饰键别名(keymaster.ts):
| 修饰键 | 支持的别名 |
|---|---|
| Shift(⇧) | ⇧、shift |
| Alt / Option(⌥) | ⌥、alt、option |
| Ctrl(⌃) | ⌃、ctrl、control |
| Meta / Command(⌘) | ⌘、command |
因此'⌘+z, ctrl+z'可以在 macOS(⌘)与 Windows/Linux(ctrl)上分别生效,这也是 GrapesJS 默认快捷键同时书写两种形式的原因。
2. 特殊键与功能键
_MAP表(keymaster.ts)覆盖了常用特殊键:backspace、tab、enter/return、esc/escape、space、方向键left/up/right/down、del/delete、home/end、pageup/pagedown,以及,./`-=;'[]\等标点键;函数键f1~f19通过循环映射(keymaster.ts)。普通字母键直接取其大写字符码,例如'ctrl+a'对应 keyCode 65。
3. 多组合与空格处理
keys字符串中的空格会被去除,再按逗号拆分成多个组合分别注册(getKeys,keymaster.ts)。例如'⌘+j, ⌘+u, ctrl+j, alt+u'会被拆成四个独立绑定,任一组合按下均可触发同一 handler。
4. 默认输入过滤
keymaster 的默认filter会忽略焦点位于INPUT、SELECT、TEXTAREA元素上的按键事件(keymaster.ts),避免用户输入文字时误触快捷键。
六、内置默认快捷键表
模块默认注册了 9 组快捷键,完整定义见 packages/core/src/keymaps/config.ts,handler均指向 GrapesJS 内置命令:
| keymap ID | keys | handler | opts |
|---|---|---|---|
core:undo | ⌘+z, ctrl+z | core:undo | { prevent: true } |
core:redo | ⌘+shift+z, ctrl+shift+z | core:redo | { prevent: true } |
core:copy | ⌘+c, ctrl+c | core:copy | — |
core:paste | ⌘+v, ctrl+v | core:paste | — |
core:component-next | s | core:component-next | — |
core:component-prev | w | core:component-prev | — |
core:component-enter | d | core:component-enter | — |
core:component-exit | a | core:component-exit | — |
core:component-delete | backspace, delete | core:component-delete | { prevent: true } |
这些命令分别对应源码中的真实实现:
core:undo/core:redo在 packages/core/src/commands/index.ts 中注册,直接调用UndoManager.undo()/UndoManager.redo();core:copy/core:paste对应 packages/core/src/commands/view/CopyComponent.ts 与 packages/core/src/commands/view/PasteComponent.ts;core:component-next/prev/enter/exit分别对应 ComponentNext.ts、ComponentPrev.ts、ComponentEnter.ts、ComponentExit.ts,用于画布中组件的选择导航与进出。
undo、redo、delete之所以带prevent: true,是为了阻止浏览器原生的撤销、重做与删除页面行为(如误删网页内容)。注意默认快捷键中的字母s/w/d/a并未加组合修饰键,仅在画布处于非输入状态时生效(见下文门控机制)。
七、执行门控与 opts 语义:源码级原理
快捷键按下后并非无条件执行,add()的绑定回调包含一套完整门控逻辑(index.ts):
const ableTorun = !em.isEditing() && !editor.Canvas.isInputFocused(); if (ableTorun || opts.force) { opts.prevent && canvas.getCanvasView()?.preventDefault(e); isFunction(handlerRes) ? handlerRes(editor, 0, opt) : cmd.runCommand(handlerRes, opt); // ... }可以分解为四层语义:
- 编辑模式门控:当
em.isEditing()为真(例如正在编辑文本组件、富文本处于激活状态)时,默认不执行任何快捷键; - 输入焦点门控:当画布内存在输入焦点(
Canvas.isInputFocused())时不执行,这与 keymaster 默认过滤形成双保险; force: true:绕过上述两个门控强制执行。测试用例 keymaps/index.js 对此有精确验证——编辑模式下普通注册的 handler 不会被调用(toHaveBeenCalledTimes(0)),而带{ force: true }的 handler 会被调用;prevent: true:执行 handler 前调用canvas.getCanvasView()?.preventDefault(e)阻止原始键盘事件(如浏览器保存页面)的默认行为。
另外,em.isEditing()的底层状态来自编辑器模型的editing属性(见 Editor.ts 的isEditing()与 Editor.ts 默认值 中的editing: 0),说明该门控是全局的编辑状态开关。
八、综合实战示例
将上述知识串联起来,一个完整的快捷键增强方案如下:
const editor = grapesjs.init({ // 1. 初始化时预置:覆盖/新增默认快捷键 keymaps: { defaults: { // 覆盖内置:为删除组件追加 shift 组合(不推荐删除默认,仅演示覆盖) 'core:component-delete': { keys: 'backspace, delete, shift+backspace', handler: 'core:component-delete', opts: { prevent: true }, }, // 自定义:绑定编辑器命令 'my-app:toggle-device': { keys: '⌘+d, ctrl+d', handler: 'core:device-desktop', // 假设已注册该命令 }, }, }, }); const keymaps = editor.Keymaps; // 2. 运行时动态注册:函数式 handler + 事件监听 keymaps.add('my-app:greet', '⌘+g, ctrl+g', (ed) => { ed.Modal.open({ title: 'Hello', content: 'GrapesJS Keymaps' }); }); // 3. 监听触发事件(含按 ID 细分的事件) editor.on('keymap:emit', (id, shortcut, event) => { console.log('triggered:', id, shortcut, event); }); editor.on('keymap:emit:my-app:greet', (id, shortcut, event) => { // 只响应 my-app:greet }); // 4. 查询与清理 const km = keymaps.get('my-app:greet'); // { id, keys, handler } const all = keymaps.getAll(); // 全部 keymap keymaps.remove('my-app:greet'); // 移除单个 keymaps.removeAll(); // 清空全部(返回 this,可链式调用)需要注意:keymaps.add与事件监听的代码必须在grapesjs.init返回的editor实例上执行;在 headless 模式下模块不会注册任何快捷键,相关 API 仅适用于浏览器环境。
结语
Keymaps 模块通过defaults配置与六大方法,为 GrapesJS 提供了一套简洁而完整的快捷键管理机制:初始化时批量预置、运行时动态增删、事件总线广播、keymaster 底层解析,以及"编辑模式/输入焦点/force/prevent"四重执行控制。理解 packages/core/src/keymaps/index.ts 与 packages/core/src/utils/keymaster.ts 的源码实现后,你不仅可以熟练使用文档中的全部 API,还能像内置的core:undo、core:copy、core:component-next等默认快捷键一样,为自己的业务命令设计跨平台、防误触的键盘交互方案。
【免费下载链接】grapesjsFree and Open source Web Builder Framework. Next generation tool for building templates without coding项目地址: https://gitcode.com/GitHub_Trending/gr/grapesjs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考