From dee6c915f86f666b7f29387c0ffec909b20d0552 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Mon, 8 May 2023 12:24:04 +0200 Subject: [PATCH 1/3] MOBILE-3748 rte: Remove arrow keys override --- .../rich-text-editor/rich-text-editor.ts | 60 +------------------ 1 file changed, 1 insertion(+), 59 deletions(-) diff --git a/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts b/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts index 5b32415bb..98f5c750a 100644 --- a/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts +++ b/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts @@ -175,7 +175,6 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, this.editorElement.onkeyup = () => this.onChange(); this.editorElement.onpaste = () => this.onChange(); this.editorElement.oninput = () => this.onChange(); - this.editorElement.onkeydown = event => this.moveCursor(event); // Use paragraph on enter. document.execCommand('DefaultParagraphSeparator', false, 'p'); @@ -374,64 +373,6 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, this.contentChanged.emit(this.control?.value); } - /** - * On key down function to move the cursor. - * https://stackoverflow.com/questions/6249095/how-to-set-caretcursor-position-in-contenteditable-element-div - * - * @param event The event. - */ - moveCursor(event: KeyboardEvent): void { - if (!this.rteEnabled || !this.editorElement) { - return; - } - - if (event.key != 'ArrowLeft' && event.key != 'ArrowRight') { - return; - } - - this.stopBubble(event); - - const move = event.key === 'ArrowLeft' ? -1 : +1; - const cursor = this.getCurrentCursorPosition(this.editorElement); - - this.setCurrentCursorPosition(this.editorElement, cursor + move); - } - - /** - * Returns the number of chars from the beggining where is placed the cursor. - * - * @param parent Parent where to get the position from. - * @returns Position in chars. - */ - protected getCurrentCursorPosition(parent: Node): number { - const selection = window.getSelection(); - - let charCount = -1; - - if (selection?.focusNode && parent.contains(selection.focusNode)) { - let node: Node | null = selection.focusNode; - charCount = selection.focusOffset; - - while (node) { - if (node.isSameNode(parent)) { - break; - } - - if (node.previousSibling) { - node = node.previousSibling; - charCount += (node.textContent || '').length; - } else { - node = node.parentNode; - if (node === null) { - break; - } - } - } - } - - return charCount; - } - /** * Set the caret position on the character number. * @@ -446,6 +387,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, * @param node Node where to start. * @param range Previous calculated range. * @param chars Object with counting of characters (input-output param). + * @param chars.count Count of characters. * @returns Selection range. */ const setRange = (node: Node, range: Range, chars: { count: number }): Range => { From bf7659223d4ec12a92397e547b23e14dcd496e97 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Mon, 8 May 2023 13:17:30 +0200 Subject: [PATCH 2/3] 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; } From 007a4f76564b7ba24a237bf8ad0b76a093d1a410 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Mon, 8 May 2023 13:18:25 +0200 Subject: [PATCH 3/3] MOBILE-3748 rte: Fix Scan QR keyboard interaction --- .../editor/components/rich-text-editor/rich-text-editor.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts b/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts index f039f7191..fde9e250e 100644 --- a/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts +++ b/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts @@ -1032,6 +1032,10 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, * @returns Promise resolved when done. */ async scanQR(event: Event): Promise { + if (event.type == 'keyup' && !this.isValidKeyboardKey(event)) { + return; + } + this.stopBubble(event); // Scan for a QR code.