diff --git a/src/addons/mod/forum/tests/behat/snapshots/test-basic-usage-of-forum-activity-in-app-reply-a-post_14.png b/src/addons/mod/forum/tests/behat/snapshots/test-basic-usage-of-forum-activity-in-app-reply-a-post_14.png index c09abb97e..2c3a9cfe5 100644 Binary files a/src/addons/mod/forum/tests/behat/snapshots/test-basic-usage-of-forum-activity-in-app-reply-a-post_14.png and b/src/addons/mod/forum/tests/behat/snapshots/test-basic-usage-of-forum-activity-in-app-reply-a-post_14.png differ 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 029b97c3d..6b476e077 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,8 +1,7 @@
+ (blur)="hideToolbar($event)" (keydown)="onKeyDown($event)">
Promise; - protected selectionChangeFunction?: () => void; + protected selectionChangeFunction = (): void => this.updateToolbarStyles(); protected languageChangedSubscription?: Subscription; protected resizeListener?: CoreEventObserver; protected domPromise?: CoreCancellablePromise; @@ -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 { - if (event.type == 'keyup' && !this.isValidKeyboardKey(event)) { + if (event.type === 'keyup' && !this.isValidKeyboardKey(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)) { - this.editorElement.innerHTML = '

'; + // Avoid loops. + if (this.editorElement.innerHTML !== '

') { + this.editorElement.innerHTML = '

'; + } 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(event)) { + if (event.type === 'keyup' && !this.isValidKeyboardKey(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(event)) { + if (event.type === 'keyup' && !this.isValidKeyboardKey(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(event)) { + if (event.type === 'keydown' && !this.isValidKeyboardKey(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 { - if (event.type == 'keyup' && !this.isValidKeyboardKey(event)) { + if (event.type === 'keyup' && !this.isValidKeyboardKey(event)) { return; } @@ -813,7 +828,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, * Method that shows the previous toolbar buttons. */ async toolbarPrev(event: Event): Promise { - if (event.type == 'keyup' && !this.isValidKeyboardKey(event)) { + if (event.type === 'keyup' && !this.isValidKeyboardKey(event)) { return; } @@ -831,7 +846,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterViewInit, * Update the number of toolbar buttons displayed. */ async updateToolbarButtons(): Promise { - 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 == '

' || draftText == '


' || draftText == '
' || - draftText == '

 

' || draftText == '


 

') { + if (draftText === '

' || draftText === '


' || draftText === '
' || + draftText === '

 

' || draftText === '


 

') { 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 { - if (event.type == 'keyup' && !this.isValidKeyboardKey(event)) { + if (event.type === 'keyup' && !this.isValidKeyboardKey(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(); } diff --git a/src/core/features/login/pages/site/site.ts b/src/core/features/login/pages/site/site.ts index 64f6eaa9d..3e93bc137 100644 --- a/src/core/features/login/pages/site/site.ts +++ b/src/core/features/login/pages/site/site.ts @@ -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 => { diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index d5c4c72df..8de6cf26d 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -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(); }