forked from EVOgeek/Vmeda.Online
		
	Merge pull request #3662 from NoelDeMartin/MOBILE-3748
MOBILE-3748: RTE a11y improvements
This commit is contained in:
		
						commit
						aaac4fbea7
					
				| @ -1,7 +1,8 @@ | ||||
| <div class="core-rte-editor-container" (click)="focusRTE()" [class.toolbar-hidden]="toolbarHidden"> | ||||
|     <div [hidden]="!rteEnabled" #editor class="core-rte-editor" role="textbox" contenteditable="true" | ||||
|         [attr.aria-labelledby]="ariaLabelledBy" [attr.data-placeholder-text]="placeholder" (focus)="showToolbar($event)" | ||||
|         (blur)="hideToolbar($event)"> | ||||
|         (blur)="hideToolbar($event)" (keydown)="onKeyDown($event)" (keyup)="onChange()" (change)="onChange()" (paste)="onChange()" | ||||
|         (input)="onChange()"> | ||||
|     </div> | ||||
| 
 | ||||
|     <ion-textarea [hidden]="rteEnabled" #textarea class="core-textarea" role="textbox" [attr.name]="name" ngControl="control" | ||||
|  | ||||
| @ -108,6 +108,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|     protected resizeListener?: CoreEventObserver; | ||||
|     protected domPromise?: CoreCancellablePromise<void>; | ||||
|     protected buttonsDomPromise?: CoreCancellablePromise<void>; | ||||
|     protected shortcutCommands?: Record<string, EditorCommand>; | ||||
| 
 | ||||
|     rteEnabled = false; | ||||
|     isPhone = false; | ||||
| @ -171,11 +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(); | ||||
|         this.editorElement.onkeydown = event => this.moveCursor(event); | ||||
| 
 | ||||
|         // Use paragraph on enter.
 | ||||
|         document.execCommand('DefaultParagraphSeparator', false, 'p'); | ||||
| @ -274,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. | ||||
|      */ | ||||
| @ -374,64 +390,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 +404,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 => { | ||||
| @ -641,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 | ||||
| @ -660,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 + '>'); | ||||
| 
 | ||||
| @ -1061,6 +1032,10 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|      * @returns Promise resolved when done. | ||||
|      */ | ||||
|     async scanQR(event: Event): Promise<void> { | ||||
|         if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.stopBubble(event); | ||||
| 
 | ||||
|         // Scan for a QR code.
 | ||||
| @ -1117,4 +1092,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<string, EditorCommand> { | ||||
|         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<string, EditorCommand>); | ||||
|         } | ||||
| 
 | ||||
|         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<KeyboardEvent, 'code' | 'altKey' | 'metaKey' | 'ctrlKey' | 'shiftKey'>; | ||||
| 
 | ||||
| /** | ||||
|  * Editor command. | ||||
|  */ | ||||
| interface EditorCommand { | ||||
|     name: string; | ||||
|     parameters?: string; | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user