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 class="core-rte-editor-container" (click)="focusRTE()" [class.toolbar-hidden]="toolbarHidden"> | ||||||
|     <div [hidden]="!rteEnabled" #editor class="core-rte-editor" role="textbox" contenteditable="true" |     <div [hidden]="!rteEnabled" #editor class="core-rte-editor" role="textbox" contenteditable="true" | ||||||
|         [attr.aria-labelledby]="ariaLabelledBy" [attr.data-placeholder-text]="placeholder" (focus)="showToolbar($event)" |         [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> |     </div> | ||||||
| 
 | 
 | ||||||
|     <ion-textarea [hidden]="rteEnabled" #textarea class="core-textarea" role="textbox" [attr.name]="name" ngControl="control" |     <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 resizeListener?: CoreEventObserver; | ||||||
|     protected domPromise?: CoreCancellablePromise<void>; |     protected domPromise?: CoreCancellablePromise<void>; | ||||||
|     protected buttonsDomPromise?: CoreCancellablePromise<void>; |     protected buttonsDomPromise?: CoreCancellablePromise<void>; | ||||||
|  |     protected shortcutCommands?: Record<string, EditorCommand>; | ||||||
| 
 | 
 | ||||||
|     rteEnabled = false; |     rteEnabled = false; | ||||||
|     isPhone = false; |     isPhone = false; | ||||||
| @ -171,11 +172,6 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | |||||||
|         this.setContent(this.control?.value); |         this.setContent(this.control?.value); | ||||||
|         this.originalContent = this.control?.value; |         this.originalContent = this.control?.value; | ||||||
|         this.lastDraft = 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.
 |         // Use paragraph on enter.
 | ||||||
|         document.execCommand('DefaultParagraphSeparator', false, 'p'); |         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. |      * Resize editor to maximize the space occupied. | ||||||
|      */ |      */ | ||||||
| @ -374,64 +390,6 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | |||||||
|         this.contentChanged.emit(this.control?.value); |         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. |      * 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 node Node where to start. | ||||||
|          * @param range Previous calculated range. |          * @param range Previous calculated range. | ||||||
|          * @param chars Object with counting of characters (input-output param). |          * @param chars Object with counting of characters (input-output param). | ||||||
|  |          * @param chars.count Count of characters. | ||||||
|          * @returns Selection range. |          * @returns Selection range. | ||||||
|          */ |          */ | ||||||
|         const setRange = (node: Node, range: Range, chars: { count: number }): 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
 |      *  API docs: https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
 | ||||||
|      * |      * | ||||||
|      * @param event Event data |      * @param event Event data | ||||||
| @ -660,6 +619,18 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | |||||||
|             return; |             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') { |         if (parameters == 'block') { | ||||||
|             document.execCommand('formatBlock', false, '<' + command + '>'); |             document.execCommand('formatBlock', false, '<' + command + '>'); | ||||||
| 
 | 
 | ||||||
| @ -1061,6 +1032,10 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | |||||||
|      * @returns Promise resolved when done. |      * @returns Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     async scanQR(event: Event): Promise<void> { |     async scanQR(event: Event): Promise<void> { | ||||||
|  |         if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         this.stopBubble(event); |         this.stopBubble(event); | ||||||
| 
 | 
 | ||||||
|         // Scan for a QR code.
 |         // Scan for a QR code.
 | ||||||
| @ -1117,4 +1092,121 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | |||||||
|         this.buttonsDomPromise?.cancel(); |         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