diff --git a/src/core/directives/collapsible-footer.ts b/src/core/directives/collapsible-footer.ts index bc4505320..cbd07969d 100644 --- a/src/core/directives/collapsible-footer.ts +++ b/src/core/directives/collapsible-footer.ts @@ -19,7 +19,7 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreMath } from '@singletons/math'; import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreFormatTextDirective } from './format-text'; -import { CoreEventObserver } from '@singletons/events'; +import { CoreEventObserver, CoreSingleTimeEventObserver } from '@singletons/events'; import { CoreLoadingComponent } from '@components/loading/loading'; import { CoreDomUtils } from '@services/utils/dom'; @@ -48,6 +48,7 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { protected contentScrollListener?: EventListener; protected endContentScrollListener?: EventListener; protected resizeListener?: CoreEventObserver; + protected domListener?: CoreSingleTimeEventObserver; constructor(el: ElementRef, protected ionContent: IonContent) { this.element = el.nativeElement; @@ -61,7 +62,9 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { // Only if not present or explicitly falsy it will be false. this.appearOnBottom = !CoreUtils.isFalseOrZero(this.appearOnBottom); - await CoreDomUtils.waitToBeInDOM(this.element); + this.domListener = CoreDomUtils.waitToBeInDOM(this.element); + await this.domListener.promise; + await this.waitLoadingsDone(); await this.waitFormatTextsRendered(this.element); @@ -231,6 +234,7 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { } this.resizeListener?.off(); + this.domListener?.off(); } } diff --git a/src/core/directives/collapsible-item.ts b/src/core/directives/collapsible-item.ts index fc1730cce..d9ab8990e 100644 --- a/src/core/directives/collapsible-item.ts +++ b/src/core/directives/collapsible-item.ts @@ -18,7 +18,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { CoreComponentsRegistry } from '@singletons/components-registry'; -import { CoreEventObserver } from '@singletons/events'; +import { CoreEventObserver, CoreSingleTimeEventObserver } from '@singletons/events'; import { CoreFormatTextDirective } from './format-text'; const defaultMaxHeight = 80; @@ -49,6 +49,7 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy { protected maxHeight = defaultMaxHeight; protected expandedHeight = 0; protected resizeListener?: CoreEventObserver; + protected domListener?: CoreSingleTimeEventObserver; constructor(el: ElementRef) { this.element = el.nativeElement; @@ -95,7 +96,8 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy { * @return Promise resolved when loadings are done. */ protected async waitLoadingsDone(): Promise { - await CoreDomUtils.waitToBeInDOM(this.element); + this.domListener = CoreDomUtils.waitToBeInDOM(this.element); + await this.domListener.promise; const page = this.element.closest('.ion-page'); @@ -240,6 +242,7 @@ export class CoreCollapsibleItemDirective implements OnInit, OnDestroy { */ ngOnDestroy(): void { this.resizeListener?.off(); + this.domListener?.off(); } } diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts index c5c041f4b..12dd1f0ed 100644 --- a/src/core/directives/format-text.ts +++ b/src/core/directives/format-text.ts @@ -23,6 +23,7 @@ import { Optional, ViewContainerRef, ViewChild, + OnDestroy, } from '@angular/core'; import { IonContent } from '@ionic/angular'; @@ -41,6 +42,7 @@ import { CoreFilterHelper } from '@features/filter/services/filter-helper'; import { CoreSubscriptions } from '@singletons/subscriptions'; import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreCollapsibleItemDirective } from './collapsible-item'; +import { CoreSingleTimeEventObserver } from '@singletons/events'; /** * Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective @@ -54,7 +56,7 @@ import { CoreCollapsibleItemDirective } from './collapsible-item'; @Directive({ selector: 'core-format-text', }) -export class CoreFormatTextDirective implements OnChanges { +export class CoreFormatTextDirective implements OnChanges, OnDestroy { @ViewChild(CoreCollapsibleItemDirective) collapsible?: CoreCollapsibleItemDirective; @@ -88,6 +90,7 @@ export class CoreFormatTextDirective implements OnChanges { protected element: HTMLElement; protected emptyText = ''; protected contentSpan: HTMLElement; + protected domListener?: CoreSingleTimeEventObserver; constructor( element: ElementRef, @@ -126,6 +129,13 @@ export class CoreFormatTextDirective implements OnChanges { } } + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.domListener?.off(); + } + /** * Wait until the text is fully rendered. */ @@ -546,7 +556,8 @@ export class CoreFormatTextDirective implements OnChanges { * @return The width of the element in pixels. */ protected async getElementWidth(): Promise { - await CoreUtils.ignoreErrors(CoreDomUtils.waitToBeInDOM(this.element)); + this.domListener = CoreDomUtils.waitToBeInDOM(this.element); + await this.domListener.promise; let width = this.element.getBoundingClientRect().width; if (!width) { @@ -700,7 +711,7 @@ export class CoreFormatTextDirective implements OnChanges { let width: string | number; let height: string | number; - await CoreDomUtils.waitToBeInDOM(iframe); + await CoreDomUtils.waitToBeInDOM(iframe, 5000).promise; if (iframe.width) { width = iframe.width; 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 2a4097c55..eb4010db4 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 @@ -36,7 +36,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils } from '@services/utils/utils'; import { Platform, Translate } from '@singletons'; -import { CoreEventFormActionData, CoreEventObserver, CoreEvents } from '@singletons/events'; +import { CoreEventFormActionData, CoreEventObserver, CoreEvents, CoreSingleTimeEventObserver } from '@singletons/events'; import { CoreEditorOffline } from '../../services/editor-offline'; import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreLoadingComponent } from '@components/loading/loading'; @@ -104,6 +104,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn protected selectionChangeFunction?: () => void; protected languageChangedSubscription?: Subscription; protected resizeListener?: CoreEventObserver; + protected domListener?: CoreSingleTimeEventObserver; rteEnabled = false; isPhone = false; @@ -249,9 +250,6 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn this.windowResized(); }, 50); - // Start observing the target node for configured mutations - this.resizeObserver?.observe(this.element); - document.addEventListener('selectionchange', this.selectionChangeFunction = this.updateToolbarStyles.bind(this)); this.keyboardObserver = CoreEvents.on(CoreEvents.KEYBOARD_CHANGE, () => { @@ -289,7 +287,8 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn * @return Promise resolved when loadings are done. */ protected async waitLoadingsDone(): Promise { - await CoreDomUtils.waitToBeInDOM(this.element); + this.domListener = CoreDomUtils.waitToBeInDOM(this.element); + await this.domListener.promise; const page = this.element.closest('.ion-page'); @@ -829,7 +828,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn const length = await this.toolbarSlides.length(); - await CoreDomUtils.waitToBeInDOM(this.toolbar.nativeElement); + await CoreDomUtils.waitToBeInDOM(this.toolbar.nativeElement, 5000).promise; const width = this.toolbar.nativeElement.getBoundingClientRect().width; @@ -1075,7 +1074,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn } /** - * Component being destroyed. + * @inheritdoc */ ngOnDestroy(): void { this.valueChangeSubscription?.unsubscribe(); @@ -1088,6 +1087,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn this.keyboardObserver?.off(); this.labelObserver?.disconnect(); this.resizeListener?.off(); + this.domListener?.off(); } } diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index e02c40a90..7b568abb8 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -53,7 +53,7 @@ import { NavigationStart } from '@angular/router'; import { filter } from 'rxjs/operators'; import { Subscription } from 'rxjs'; import { CoreComponentsRegistry } from '@singletons/components-registry'; -import { CoreEventObserver } from '@singletons/events'; +import { CoreEventObserver, CoreSingleTimeEventObserver } from '@singletons/events'; /* * "Utils" service with helper functions for UI, DOM elements and HTML code. @@ -96,36 +96,52 @@ export class CoreDomUtilsProvider { * Wait an element to be in dom of another element. * * @param element Element to wait. - * @return Promise resolved when added. It will be rejected after a timeout of 5s. + * @param timeout If defined, timeout to wait before rejecting the promise. + * @return Promise CoreSingleTimeEventObserver with a promise. */ - async waitToBeInDOM( + waitToBeInDOM( element: Element, - ): Promise { + timeout?: number, + ): CoreSingleTimeEventObserver { let root = element.getRootNode({ composed: true }); if (root === document) { // Already in DOM. - return; + return { + off: (): void => { + // Nothing to do here. + }, + promise: Promise.resolve(), + }; } - return new Promise((resolve, reject) => { - // Disconnect observer for performance reasons. - const timeout = window.setTimeout(() => { - observer.disconnect(); - reject(new Error('Waiting for DOM timeout reached')); - }, 5000); + let observer: MutationObserver | undefined; + let observerTimeout: number | undefined; + if (timeout) { + observerTimeout = window.setTimeout(() => { + observer?.disconnect(); + throw new Error('Waiting for DOM timeout reached'); + }, timeout); + } - const observer = new MutationObserver(() => { - root = element.getRootNode({ composed: true }); - if (root === document) { - observer.disconnect(); - clearTimeout(timeout); - resolve(); - } - }); + return { + off: (): void => { + observer?.disconnect(); + clearTimeout(observerTimeout); + }, + promise: new Promise((resolve) => { + observer = new MutationObserver(() => { + root = element.getRootNode({ composed: true }); + if (root === document) { + observer?.disconnect(); + clearTimeout(observerTimeout); + resolve(); + } + }); - observer.observe(document.body, { subtree: true, childList: true }); - }); + observer.observe(document.body, { subtree: true, childList: true }); + }), + }; } /** diff --git a/src/core/singletons/events.ts b/src/core/singletons/events.ts index 4045c8212..f69e0ea0a 100644 --- a/src/core/singletons/events.ts +++ b/src/core/singletons/events.ts @@ -31,6 +31,21 @@ export interface CoreEventObserver { off: () => void; } +/** + * Observer instance to stop listening to an observer. + */ +export interface CoreSingleTimeEventObserver { + /** + * Stop the observer. + */ + off: () => void; + + /** + * Promise Resolved when event is done (first time). + */ + promise: Promise; +} + /** * Event payloads. */