Add keyboard shortcuts to all toolbar buttons
This commit is contained in:
parent
1f523cbc0f
commit
8bef75e59f
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user