From 8bef75e59f227fbbffa8bcf2f05a50c5a17335ae Mon Sep 17 00:00:00 2001 From: gsb Date: Wed, 29 Apr 2026 07:22:00 +0000 Subject: [PATCH] Add keyboard shortcuts to all toolbar buttons --- src/ts/tags.ts | 8 +++--- src/ts/toolbar.ts | 65 ++++++++++++++++++++++++++++++++++++++++++-- test/toolbar.test.ts | 48 ++++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 6 deletions(-) diff --git a/src/ts/tags.ts b/src/ts/tags.ts index 4938438..1e2b209 100644 --- a/src/ts/tags.ts +++ b/src/ts/tags.ts @@ -187,7 +187,7 @@ export const defaultBlockTags: Record = { * ``` */ name: 'fencedCode', - button: { show: true, label: 'Code Block' }, + button: { show: true, label: 'Code Block', shortcut: 'Ctrl+Shift+E' }, template: '```\ncode\n```', replaceSelection: true, match: (context) => { @@ -225,7 +225,7 @@ export const defaultBlockTags: Record = { * ___ */ name: 'hr', - button: { show: true, label: 'Divider' }, + button: { show: true, label: 'Divider', shortcut: 'Ctrl+Shift+-' }, template: '---', replaceSelection: false, match: (context) => { @@ -276,7 +276,7 @@ export const defaultBlockTags: Record = { * > more quoted text */ name: 'blockquote', - button: { show: true, label: 'Quote' }, + button: { show: true, label: 'Quote', shortcut: 'Ctrl+Shift+.' }, template: '> Quote\n> continues here', replaceSelection: true, match: (context) => { @@ -330,7 +330,7 @@ export const defaultBlockTags: Record = { * | cell 1 | cell 2 | */ name: 'table', - button: { show: true, label: 'Table' }, + button: { show: true, label: 'Table', shortcut: 'Ctrl+Shift+T' }, template: '| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |', replaceSelection: false, match: (context) => { diff --git a/src/ts/toolbar.ts b/src/ts/toolbar.ts index ed7e5b6..8d4bfdf 100644 --- a/src/ts/toolbar.ts +++ b/src/ts/toolbar.ts @@ -83,6 +83,31 @@ export class ToolbarManager { }); } + // Heading and list variants (derived from their parent tags) + for (let i = 1; i <= 6; i++) { + this.register(`h${i}`, { + label: `H${i}`, + shortcut: `Ctrl+${i}`, + action: 'prefix', + delimiter: '#'.repeat(i) + ' ', + replaceSelection: true, + }); + } + this.register('ul', { + label: 'Bullet List', + shortcut: 'Ctrl+Shift+8', + action: 'insert', + template: '- Item 1\n- Item 2\n- Item 3', + replaceSelection: false, + }); + this.register('ol', { + label: 'Numbered List', + shortcut: 'Ctrl+Shift+7', + action: 'insert', + template: '1. Item 1\n2. Item 2\n3. Item 3', + replaceSelection: false, + }); + for (const macro of macros) { if (macro.button === false) { continue; @@ -102,7 +127,7 @@ export class ToolbarManager { handler: () => this.editor.save(), }); this.register('toggle', { - label: 'Edit', action: 'custom', + label: 'Edit', shortcut: 'Ctrl+Shift+V', action: 'custom', handler: () => { this.editor.getState() === 'view' ? this.editor.wysiwyg() @@ -110,7 +135,7 @@ export class ToolbarManager { }, }); this.register('markdown', { - label: 'Source', action: 'custom', + label: 'Source', shortcut: 'Ctrl+/', action: 'custom', handler: () => { this.editor.getState() === 'edit' ? this.editor.wysiwyg() @@ -119,6 +144,42 @@ export class ToolbarManager { }); this.layout = layout || this.defaultLayout(); + this.bindShortcuts(); + } + + /** + * Listen for keyboard shortcuts on the document and dispatch + * to the matching toolbar button. + */ + private bindShortcuts(): void { + const shortcutMap = new Map(); + for (const button of this.buttons.values()) { + if (button.shortcut) { + shortcutMap.set(button.shortcut.toLowerCase(), button); + } + } + + document.addEventListener('keydown', (event: KeyboardEvent) => { + const parts: string[] = []; + if (event.ctrlKey || event.metaKey) parts.push('ctrl'); + if (event.shiftKey) parts.push('shift'); + if (event.altKey) parts.push('alt'); + + let key = event.key; + if (key === '/') key = '/'; + else if (key === '.') key = '.'; + else if (key === '-') key = '-'; + else key = key.toLowerCase(); + + parts.push(key); + const combo = parts.join('+'); + + const button = shortcutMap.get(combo); + if (button) { + event.preventDefault(); + this.executeAction(button); + } + }); } private register(id: string, def: Partial