Add keyboard shortcuts to all toolbar buttons

This commit is contained in:
gsb 2026-04-29 07:22:00 +00:00
parent 1f523cbc0f
commit 8bef75e59f
3 changed files with 115 additions and 6 deletions

View File

@ -187,7 +187,7 @@ export const defaultBlockTags: Record<string, Tag> = {
* ``` * ```
*/ */
name: 'fencedCode', name: 'fencedCode',
button: { show: true, label: 'Code Block' }, button: { show: true, label: 'Code Block', shortcut: 'Ctrl+Shift+E' },
template: '```\ncode\n```', template: '```\ncode\n```',
replaceSelection: true, replaceSelection: true,
match: (context) => { match: (context) => {
@ -225,7 +225,7 @@ export const defaultBlockTags: Record<string, Tag> = {
* ___ * ___
*/ */
name: 'hr', name: 'hr',
button: { show: true, label: 'Divider' }, button: { show: true, label: 'Divider', shortcut: 'Ctrl+Shift+-' },
template: '---', template: '---',
replaceSelection: false, replaceSelection: false,
match: (context) => { match: (context) => {
@ -276,7 +276,7 @@ export const defaultBlockTags: Record<string, Tag> = {
* > more quoted text * > more quoted text
*/ */
name: 'blockquote', name: 'blockquote',
button: { show: true, label: 'Quote' }, button: { show: true, label: 'Quote', shortcut: 'Ctrl+Shift+.' },
template: '> Quote\n> continues here', template: '> Quote\n> continues here',
replaceSelection: true, replaceSelection: true,
match: (context) => { match: (context) => {
@ -330,7 +330,7 @@ export const defaultBlockTags: Record<string, Tag> = {
* | cell 1 | cell 2 | * | cell 1 | cell 2 |
*/ */
name: 'table', 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 |', template: '| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |',
replaceSelection: false, replaceSelection: false,
match: (context) => { match: (context) => {

View File

@ -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) { for (const macro of macros) {
if (macro.button === false) { if (macro.button === false) {
continue; continue;
@ -102,7 +127,7 @@ export class ToolbarManager {
handler: () => this.editor.save(), handler: () => this.editor.save(),
}); });
this.register('toggle', { this.register('toggle', {
label: 'Edit', action: 'custom', label: 'Edit', shortcut: 'Ctrl+Shift+V', action: 'custom',
handler: () => { handler: () => {
this.editor.getState() === 'view' this.editor.getState() === 'view'
? this.editor.wysiwyg() ? this.editor.wysiwyg()
@ -110,7 +135,7 @@ export class ToolbarManager {
}, },
}); });
this.register('markdown', { this.register('markdown', {
label: 'Source', action: 'custom', label: 'Source', shortcut: 'Ctrl+/', action: 'custom',
handler: () => { handler: () => {
this.editor.getState() === 'edit' this.editor.getState() === 'edit'
? this.editor.wysiwyg() ? this.editor.wysiwyg()
@ -119,6 +144,42 @@ export class ToolbarManager {
}); });
this.layout = layout || this.defaultLayout(); 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<string, Button>();
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<Button>): void { private register(id: string, def: Partial<Button>): void {

View File

@ -239,6 +239,54 @@ describe('ToolbarManager', () => {
}); });
}); });
describe('heading and list buttons', () => {
it('registers h1-h6', () => {
const editor = new r.Editor({ autoToolbar: false });
editor.run();
for (let i = 1; i <= 6; i++) {
const btn = editor.toolbar.buttons.get(`h${i}`);
expect(btn).toBeDefined();
expect(btn!.label).toBe(`H${i}`);
expect(btn!.shortcut).toBe(`Ctrl+${i}`);
expect(btn!.action).toBe('prefix');
}
});
it('registers ul and ol', () => {
const editor = new r.Editor({ autoToolbar: false });
editor.run();
expect(editor.toolbar.buttons.get('ul')!.shortcut).toBe('Ctrl+Shift+8');
expect(editor.toolbar.buttons.get('ol')!.shortcut).toBe('Ctrl+Shift+7');
});
});
describe('keyboard shortcuts', () => {
it('all formatting buttons have shortcuts', () => {
const editor = new r.Editor({ autoToolbar: false });
editor.run();
const expected = ['bold', 'italic', 'code', 'link', 'save'];
for (const id of expected) {
expect(editor.toolbar.buttons.get(id)!.shortcut).toBeDefined();
}
});
it('block buttons have shortcuts', () => {
const editor = new r.Editor({ autoToolbar: false });
editor.run();
expect(editor.toolbar.buttons.get('fencedCode')!.shortcut).toBe('Ctrl+Shift+E');
expect(editor.toolbar.buttons.get('blockquote')!.shortcut).toBe('Ctrl+Shift+.');
expect(editor.toolbar.buttons.get('table')!.shortcut).toBe('Ctrl+Shift+T');
expect(editor.toolbar.buttons.get('hr')!.shortcut).toBe('Ctrl+Shift+-');
});
it('editor actions have shortcuts', () => {
const editor = new r.Editor({ autoToolbar: false });
editor.run();
expect(editor.toolbar.buttons.get('toggle')!.shortcut).toBe('Ctrl+Shift+V');
expect(editor.toolbar.buttons.get('markdown')!.shortcut).toBe('Ctrl+/');
});
});
describe('save button', () => { describe('save button', () => {
it('triggers editor.save()', () => { it('triggers editor.save()', () => {
resetDOM(); resetDOM();