From bf7659223d4ec12a92397e547b23e14dcd496e97 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Mon, 8 May 2023 13:17:30 +0200 Subject: [PATCH] MOBILE-3748 rte: Implement keyboard shortcuts --- .../core-editor-rich-text-editor.html | 3 +- .../rich-text-editor/rich-text-editor.ts | 156 +++++++++++++++++- 2 files changed, 153 insertions(+), 6 deletions(-) diff --git a/src/core/features/editor/components/rich-text-editor/core-editor-rich-text-editor.html b/src/core/features/editor/components/rich-text-editor/core-editor-rich-text-editor.html index 40a6d2aa6..acddfb540 100644 --- a/src/core/features/editor/components/rich-text-editor/core-editor-rich-text-editor.html +++ b/src/core/features/editor/components/rich-text-editor/core-editor-rich-text-editor.html @@ -1,7 +1,8 @@
+ (blur)="hideToolbar($event)" (keydown)="onKeyDown($event)" (keyup)="onChange()" (change)="onChange()" (paste)="onChange()" + (input)="onChange()">
; protected buttonsDomPromise?: CoreCancellablePromise; + protected shortcutCommands?: Record; rteEnabled = false; isPhone = false; @@ -171,10 +172,6 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, this.setContent(this.control?.value); this.originalContent = this.control?.value; this.lastDraft = this.control?.value; - this.editorElement.onchange = () => this.onChange(); - this.editorElement.onkeyup = () => this.onChange(); - this.editorElement.onpaste = () => this.onChange(); - this.editorElement.oninput = () => this.onChange(); // Use paragraph on enter. document.execCommand('DefaultParagraphSeparator', false, 'p'); @@ -273,6 +270,26 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, }); } + /** + * Handle keydown events in the editor. + * + * @param event Event + */ + onKeyDown(event: KeyboardEvent): void { + this.onChange(); + + const shortcutId = this.getShortcutId(event); + const commands = this.getShortcutCommands(); + const command = commands[shortcutId]; + + if (!command) { + return; + } + + this.stopBubble(event); + this.executeCommand(command); + } + /** * Resize editor to maximize the space occupied. */ @@ -583,7 +600,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, } /** - * Execute an action over the selected text. + * Execute an action over the selected text when a button is activated. * API docs: https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand * * @param event Event data @@ -602,6 +619,18 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, return; } + this.executeCommand({ name: command, parameters }); + } + + /** + * Execute an action over the selected text. + * API docs: https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand + * + * @param command Editor command. + * @param command.name Command name. + * @param command.parameters Command parameters. + */ + protected executeCommand({ name: command, parameters }: EditorCommand): void { if (parameters == 'block') { document.execCommand('formatBlock', false, '<' + command + '>'); @@ -1059,4 +1088,121 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, this.buttonsDomPromise?.cancel(); } + /** + * Get commands triggered by keyboard shortcuts. + * + * @returns Commands dictionary indexed by their corresponding keyboard shortcut id. + */ + getShortcutCommands(): Record { + if (!this.shortcutCommands) { + const isIOS = CorePlatform.isIOS(); + const metaKey = isIOS ? 'metaKey' : 'ctrlKey'; + const shiftKey = isIOS ? 'ctrlKey' : 'shiftKey'; + + // Same shortcuts as TinyMCE: + // @see https://www.tiny.cloud/docs/advanced/keyboard-shortcuts/ + const shortcuts: { code: string; modifiers: (keyof KeyboardShortcut)[]; command: EditorCommand }[] = [ + { + code: 'KeyB', + modifiers: [metaKey], + command: { + name: 'bold', + parameters: 'strong', + }, + }, + { + code: 'KeyI', + modifiers: [metaKey], + command: { + name: 'italic', + parameters: 'em', + }, + }, + { + code: 'KeyU', + modifiers: [metaKey], + command: { + name: 'underline', + parameters: 'u', + }, + }, + { + code: 'Digit3', + modifiers: ['altKey', shiftKey], + command: { + name: 'h3', + parameters: 'block', + }, + }, + { + code: 'Digit4', + modifiers: ['altKey', shiftKey], + command: { + name: 'h4', + parameters: 'block', + }, + }, + { + code: 'Digit5', + modifiers: ['altKey', shiftKey], + command: { + name: 'h5', + parameters: 'block', + }, + }, + { + code: 'Digit7', + modifiers: ['altKey', shiftKey], + command: { + name: 'p', + parameters: 'block', + }, + }, + ]; + + this.shortcutCommands = shortcuts.reduce((shortcuts, { code, modifiers, command }) => { + const id = this.getShortcutId({ + code: code, + altKey: modifiers.includes('altKey'), + metaKey: modifiers.includes('metaKey'), + shiftKey: modifiers.includes('shiftKey'), + ctrlKey: modifiers.includes('ctrlKey'), + }); + + shortcuts[id] = command; + + return shortcuts; + }, {} as Record); + } + + return this.shortcutCommands; + } + + /** + * Get a unique identifier for a given keyboard shortcut. + * + * @param shortcut Shortcut. + * @returns Identifier. + */ + protected getShortcutId(shortcut: KeyboardShortcut): string { + return (shortcut.altKey ? '1' : '0') + + (shortcut.metaKey ? '1' : '0') + + (shortcut.shiftKey ? '1' : '0') + + (shortcut.ctrlKey ? '1' : '0') + + shortcut.code; + } + +} + +/** + * Combination + */ +type KeyboardShortcut = Pick; + +/** + * Editor command. + */ +interface EditorCommand { + name: string; + parameters?: string; }