forked from EVOgeek/Vmeda.Online
		
	MOBILE-4565 rte: Fix rte focus and blur problems
This commit is contained in:
		
							parent
							
								
									64ce8c78f6
								
							
						
					
					
						commit
						f9ddfb48c9
					
				
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB | 
| @ -1,8 +1,7 @@ | ||||
| <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)" (keydown)="onKeyDown($event)" (keyup)="onChange()" (change)="onChange()" (paste)="onChange()" | ||||
|         (input)="onChange()"> | ||||
|         (blur)="hideToolbar($event)" (keydown)="onKeyDown($event)"> | ||||
|     </div> | ||||
| 
 | ||||
|     <ion-textarea [hidden]="rteEnabled" #textarea class="core-textarea" role="textbox" [attr.name]="name" ngControl="control" | ||||
|  | ||||
| @ -23,6 +23,18 @@ | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     background: var(--background); | ||||
|     border: 1px solid var(--stroke); | ||||
|     border-radius: var(--mdl-shape-borderRadius-md); | ||||
| 
 | ||||
|     &:focus-within { | ||||
|         border-color: var(--a11y-focus-color); | ||||
|         border-width: 2px; | ||||
|         outline: none !important; | ||||
|     } | ||||
| 
 | ||||
|     .core-rte-editor, .core-textarea, textarea { | ||||
|         outline: none !important; | ||||
|     } | ||||
| 
 | ||||
|     .core-rte-editor-container { | ||||
|         max-height: calc(100% - 46px); | ||||
| @ -105,7 +117,6 @@ | ||||
|         flex-grow: 0; | ||||
|         flex-shrink: 0; | ||||
|         background-color: var(--toobar-background); | ||||
|         padding-top: 5px; | ||||
|         border-top: 1px solid var(--stroke); | ||||
| 
 | ||||
|         swiper-container { | ||||
| @ -122,7 +133,7 @@ | ||||
|             height: 36px; | ||||
|             padding-right: 6px; | ||||
|             padding-left: 6px; | ||||
|             margin: 0 auto; | ||||
|             margin: 2px auto; | ||||
|             font-size: 18px; | ||||
|             background-color: var(--toobar-background); | ||||
|             border-radius: var(--mdl-shape-borderRadius-xs); | ||||
|  | ||||
| @ -115,6 +115,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|     protected keyboardObserver?: CoreEventObserver; | ||||
|     protected resetObserver?: CoreEventObserver; | ||||
|     protected labelObserver?: MutationObserver; | ||||
|     protected contentObserver?: MutationObserver; | ||||
|     protected initHeightInterval?: number; | ||||
|     protected isCurrentView = true; | ||||
|     protected toolbarButtonWidth = 44; | ||||
| @ -126,7 +127,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|     protected draftWasRestored = false; | ||||
|     protected originalContent?: string; | ||||
|     protected resizeFunction?: () => Promise<number>; | ||||
|     protected selectionChangeFunction?: () => void; | ||||
|     protected selectionChangeFunction = (): void => this.updateToolbarStyles(); | ||||
|     protected languageChangedSubscription?: Subscription; | ||||
|     protected resizeListener?: CoreEventObserver; | ||||
|     protected domPromise?: CoreCancellablePromise<void>; | ||||
| @ -226,6 +227,15 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|         } | ||||
|         ionItem.classList.add('item-rte'); | ||||
| 
 | ||||
|         if (this.editorElement) { | ||||
|             const debounceMutation = CoreUtils.debounce(() => { | ||||
|                 this.onChange(); | ||||
|             }, 20); | ||||
| 
 | ||||
|             this.contentObserver = new MutationObserver(debounceMutation); | ||||
|             this.contentObserver.observe(this.editorElement, { childList: true, subtree: true, characterData: true }); | ||||
|         } | ||||
| 
 | ||||
|         const label = ionItem.querySelector('ion-label'); | ||||
| 
 | ||||
|         if (!label) { | ||||
| @ -253,7 +263,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|     protected setListeners(): void { | ||||
|         // Listen for changes on the control to update the editor (if it is updated from outside of this component).
 | ||||
|         this.valueChangeSubscription = this.control?.valueChanges.subscribe((newValue) => { | ||||
|             if (this.draftWasRestored && this.originalContent == newValue) { | ||||
|             if (this.draftWasRestored && this.originalContent === newValue) { | ||||
|                 // A draft was restored and the content hasn't changed in the site. Use the draft value instead of this one.
 | ||||
|                 this.control?.setValue(this.lastDraft, { emitEvent: false }); | ||||
| 
 | ||||
| @ -282,7 +292,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|             this.windowResized(); | ||||
|         }, 50); | ||||
| 
 | ||||
|         document.addEventListener('selectionchange', this.selectionChangeFunction = () => this.updateToolbarStyles()); | ||||
|         document.addEventListener('selectionchange', this.selectionChangeFunction); | ||||
| 
 | ||||
|         this.keyboardObserver = CoreEvents.on(CoreEvents.KEYBOARD_CHANGE, () => { | ||||
|             // Opening or closing the keyboard also calls the resize function, but sometimes the resize is called too soon.
 | ||||
| @ -304,8 +314,6 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|      * @param event Event | ||||
|      */ | ||||
|     onKeyDown(event: KeyboardEvent): void { | ||||
|         this.onChange(); | ||||
| 
 | ||||
|         const shortcutId = this.getShortcutId(event); | ||||
|         const commands = this.getShortcutCommands(); | ||||
|         const command = commands[shortcutId]; | ||||
| @ -364,7 +372,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
| 
 | ||||
|         // Get first children with content, not fixed.
 | ||||
|         let scrollContentHeight = 0; | ||||
|         while (scrollContentHeight == 0 && content?.children) { | ||||
|         while (scrollContentHeight === 0 && content?.children) { | ||||
|             const children = Array.from(content.children) | ||||
|                 .filter((element) => element.slot !== 'fixed' && !element.classList.contains('core-loading-container')); | ||||
| 
 | ||||
| @ -489,7 +497,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|      * @param event The event. | ||||
|      */ | ||||
|     async toggleEditor(event: Event): Promise<void> { | ||||
|         if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) { | ||||
|         if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
| @ -581,7 +589,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|      * @returns If value is null only a white space. | ||||
|      */ | ||||
|     protected isNullOrWhiteSpace(value: string | null | undefined): boolean { | ||||
|         if (value == null || value === undefined) { | ||||
|         if (value === null || value === undefined) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
| @ -602,10 +610,17 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|         } | ||||
| 
 | ||||
|         if (this.isNullOrWhiteSpace(value)) { | ||||
|             // Avoid loops.
 | ||||
|             if (this.editorElement.innerHTML !== '<p></p>') { | ||||
|                 this.editorElement.innerHTML = '<p></p>'; | ||||
|             } | ||||
|             this.textarea.value = ''; | ||||
|         } else { | ||||
|             this.editorElement.innerHTML = value || ''; | ||||
|             value = value || ''; | ||||
|             // Avoid loops.
 | ||||
|             if (this.editorElement.innerHTML !== value) { | ||||
|                 this.editorElement.innerHTML = value; | ||||
|             } | ||||
|             this.textarea.value = value; | ||||
|             this.treatExternalContent(); | ||||
|         } | ||||
| @ -637,7 +652,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|      *                      toolbar styles button when set. | ||||
|      */ | ||||
|     buttonAction(event: Event, command: string, parameters?: string): void { | ||||
|         if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) { | ||||
|         if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
| @ -659,7 +674,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|      * @param command.parameters Command parameters. | ||||
|      */ | ||||
|     protected executeCommand({ name: command, parameters }: EditorCommand): void { | ||||
|         if (parameters == 'block') { | ||||
|         if (parameters === 'block') { | ||||
|             // eslint-disable-next-line deprecation/deprecation
 | ||||
|             document.execCommand('formatBlock', false, '<' + command + '>'); | ||||
| 
 | ||||
| @ -676,7 +691,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|         // Modern browsers are using non a11y tags, so replace them.
 | ||||
|         if (command === 'bold') { | ||||
|             this.replaceTags(['b'], ['strong']); | ||||
|         } else if (command == 'italic') { | ||||
|         } else if (command === 'italic') { | ||||
|             this.replaceTags(['i'], ['em']); | ||||
|         } | ||||
|     } | ||||
| @ -715,14 +730,14 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|      * @param event Event. | ||||
|      * @param force If true it will not check the target of the event. | ||||
|      */ | ||||
|     hideToolbar(event: Event, force = false): void { | ||||
|     hideToolbar(event: FocusEvent | KeyboardEvent | MouseEvent, force = false): void { | ||||
|         if (!force && event.target && this.element.contains(event.target as HTMLElement)) { | ||||
|             // Do not hide if clicked inside the editor area, except forced.
 | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) { | ||||
|         if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
| @ -748,7 +763,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|     /** | ||||
|      * Show the toolbar. | ||||
|      */ | ||||
|     showToolbar(event: Event): void { | ||||
|     showToolbar(event: FocusEvent): void { | ||||
|         this.updateToolbarButtons(); | ||||
| 
 | ||||
|         this.element.classList.add('ion-touched'); | ||||
| @ -779,14 +794,14 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|      * @param event Event. | ||||
|      */ | ||||
|     downAction(event: Event): void { | ||||
|         if (event.type == 'keydown' && !this.isValidKeyboardKey(<KeyboardEvent>event)) { | ||||
|         if (event.type === 'keydown' && !this.isValidKeyboardKey(<KeyboardEvent>event)) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const selection = window.getSelection()?.toString(); | ||||
| 
 | ||||
|         // When RTE is focused with a whole paragraph in desktop the stopBubble will not fire click.
 | ||||
|         if (CorePlatform.isMobile() || !this.rteEnabled || document.activeElement != this.editorElement || selection == '') { | ||||
|         if (CorePlatform.isMobile() || !this.rteEnabled || document.activeElement != this.editorElement || selection === '') { | ||||
|             this.stopBubble(event); | ||||
|         } | ||||
|     } | ||||
| @ -795,7 +810,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|      * Method that shows the next toolbar buttons. | ||||
|      */ | ||||
|     async toolbarNext(event: Event): Promise<void> { | ||||
|         if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) { | ||||
|         if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
| @ -813,7 +828,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|      * Method that shows the previous toolbar buttons. | ||||
|      */ | ||||
|     async toolbarPrev(event: Event): Promise<void> { | ||||
|         if (event.type == 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) { | ||||
|         if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
| @ -831,7 +846,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|      * Update the number of toolbar buttons displayed. | ||||
|      */ | ||||
|     async updateToolbarButtons(): Promise<void> { | ||||
|         if (!this.isCurrentView || !this.toolbar || !this.toolbarSlides || this.element.offsetParent == null) { | ||||
|         if (!this.isCurrentView || !this.toolbar || !this.toolbarSlides || this.element.offsetParent === null) { | ||||
|             // Don't calculate if component isn't in current view, the calculations are wrong.
 | ||||
|             return; | ||||
|         } | ||||
| @ -879,15 +894,18 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|      */ | ||||
|     updateToolbarStyles(): void { | ||||
|         const node = window.getSelection()?.focusNode; | ||||
|         if (!node) { | ||||
| 
 | ||||
|         if (!node || !this.element.contains(node)) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         let element = node.nodeType == 1 ? node as HTMLElement : node.parentElement; | ||||
|         let element = node.nodeType === 1 ? node as HTMLElement : node.parentElement; | ||||
| 
 | ||||
|         const styles = {}; | ||||
| 
 | ||||
|         while (element != null && element !== this.editorElement) { | ||||
|         while (element !== null && element !== this.editorElement) { | ||||
|             const tagName = element.tagName.toLowerCase(); | ||||
| 
 | ||||
|             if (this.toolbarStyles[tagName]) { | ||||
|                 styles[tagName] = 'true'; | ||||
|             } | ||||
| @ -906,7 +924,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|     /** | ||||
|      * Check if should auto save drafts. | ||||
|      * | ||||
|      * @returns {boolean} Whether it should auto save drafts. | ||||
|      * @returns Whether it should auto save drafts. | ||||
|      */ | ||||
|     protected shouldAutoSaveDrafts(): boolean { | ||||
|         return !!CoreSites.getCurrentSite() && | ||||
| @ -943,8 +961,8 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|             let draftText = entry.drafttext || ''; | ||||
| 
 | ||||
|             // Revert untouched editor contents to an empty string.
 | ||||
|             if (draftText == '<p></p>' || draftText == '<p><br></p>' || draftText == '<br>' || | ||||
|                     draftText == '<p> </p>' || draftText == '<p><br> </p>') { | ||||
|             if (draftText === '<p></p>' || draftText === '<p><br></p>' || draftText === '<br>' || | ||||
|                     draftText === '<p> </p>' || draftText === '<p><br> </p>') { | ||||
|                 draftText = ''; | ||||
|             } | ||||
| 
 | ||||
| @ -977,7 +995,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
| 
 | ||||
|             const newText = this.control.value ?? ''; | ||||
| 
 | ||||
|             if (this.lastDraft == newText) { | ||||
|             if (this.lastDraft === newText) { | ||||
|                 // Text hasn't changed, nothing to save.
 | ||||
|                 return; | ||||
|             } | ||||
| @ -1009,7 +1027,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|         this.resetObserver = CoreEvents.on(CoreEvents.FORM_ACTION, async (data: CoreEventFormActionData) => { | ||||
|             const form = this.element.closest('form'); | ||||
| 
 | ||||
|             if (data.form && form && data.form == form) { | ||||
|             if (data.form && form && data.form === form) { | ||||
|                 try { | ||||
|                     await CoreEditorOffline.deleteDraft( | ||||
|                         this.contextLevel || ContextLevel.SYSTEM, | ||||
| @ -1048,7 +1066,7 @@ 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)) { | ||||
|         if (event.type === 'keyup' && !this.isValidKeyboardKey(<KeyboardEvent>event)) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
| @ -1097,14 +1115,20 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, | ||||
|     ngOnDestroy(): void { | ||||
|         this.valueChangeSubscription?.unsubscribe(); | ||||
|         this.languageChangedSubscription?.unsubscribe(); | ||||
|         this.selectionChangeFunction && document.removeEventListener('selectionchange', this.selectionChangeFunction); | ||||
| 
 | ||||
|         document.removeEventListener('selectionchange', this.selectionChangeFunction); | ||||
| 
 | ||||
|         clearInterval(this.initHeightInterval); | ||||
|         clearInterval(this.autoSaveInterval); | ||||
|         clearTimeout(this.hideMessageTimeout); | ||||
| 
 | ||||
|         this.resetObserver?.off(); | ||||
|         this.keyboardObserver?.off(); | ||||
|         this.labelObserver?.disconnect(); | ||||
|         this.resizeListener?.off(); | ||||
| 
 | ||||
|         this.labelObserver?.disconnect(); | ||||
|         this.contentObserver?.disconnect(); | ||||
| 
 | ||||
|         this.domPromise?.cancel(); | ||||
|         this.buttonsDomPromise?.cancel(); | ||||
|     } | ||||
|  | ||||
| @ -235,7 +235,7 @@ export class CoreLoginSitePage implements OnInit { | ||||
|     /** | ||||
|      * Validate Url. | ||||
|      * | ||||
|      * @returns {ValidatorFn} Validation results. | ||||
|      * @returns Validation results. | ||||
|      */ | ||||
|     protected moodleUrlValidator(): ValidatorFn { | ||||
|         return (control: AbstractControl): ValidationErrors | null => { | ||||
|  | ||||
| @ -1129,7 +1129,7 @@ td { | ||||
|     @include core-focus-background(); | ||||
| } | ||||
| 
 | ||||
| :not(.hydrated):not(.native-input) { // Not an ionic component. | ||||
| :not(.hydrated):not(.native-input):not(.native-textarea) { // Not an ionic component. | ||||
|     @include core-focus-outline(); | ||||
| } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user