diff --git a/src/core/directives/collapsible-footer.ts b/src/core/directives/collapsible-footer.ts index 47fe99439..dc1e2c55a 100644 --- a/src/core/directives/collapsible-footer.ts +++ b/src/core/directives/collapsible-footer.ts @@ -19,8 +19,8 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreMath } from '@singletons/math'; import { CoreComponentsRegistry } from '@singletons/components-registry'; import { CoreFormatTextDirective } from './format-text'; -import { CoreDomUtils } from '@services/utils/dom'; -import { CoreEventLoadingChangedData, CoreEventObserver, CoreEvents } from '@singletons/events'; +import { CoreEventObserver } from '@singletons/events'; +import { CoreLoadingComponent } from '@components/loading/loading'; /** * Directive to make an element fixed at the bottom collapsible when scrolling. @@ -139,11 +139,11 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { * @param element Element. */ protected async waitFormatTextsRendered(element: Element): Promise { - const formatTexts = Array - .from(element.querySelectorAll('core-format-text')) - .map(element => CoreComponentsRegistry.resolve(element, CoreFormatTextDirective)); - - await Promise.all(formatTexts.map(formatText => formatText?.rendered())); + await CoreComponentsRegistry.finishRenderingAllElementsInside( + element, + 'core-format-text', + 'rendered', + ); } /** @@ -184,24 +184,33 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy { * @inheritdoc */ async ngOnInit(): Promise { - // Calculate the height now. - await this.calculateHeight(); - setTimeout(() => this.calculateHeight(), 200); // Try again, sometimes the first calculation is wrong. - - this.listenScrollEvents(); - // Only if not present or explicitly falsy it will be false. this.appearOnBottom = !CoreUtils.isFalseOrZero(this.appearOnBottom); - // Recalculate the height if a parent core-loading displays the content. - this.loadingChangedListener = - CoreEvents.on(CoreEvents.CORE_LOADING_CHANGED, async (data: CoreEventLoadingChangedData) => { - if (data.loaded && CoreDomUtils.closest(this.element.parentElement, '#' + data.uniqueId)) { - // The format-text is inside the loading, re-calculate the height. - await this.calculateHeight(); - setTimeout(() => this.calculateHeight(), 200); - } - }); + await this.waitLoadingsDone(); + + await this.calculateHeight(); + + this.listenScrollEvents(); + } + + /** + * Wait until all children inside the page. + * + * @return Promise resolved when loadings are done. + */ + protected async waitLoadingsDone(): Promise { + const scrollElement = await this.ionContent.getScrollElement(); + + await Promise.all([ + await CoreComponentsRegistry.finishRenderingAllElementsInside + (scrollElement, 'core-loading', 'whenLoaded'), + await CoreComponentsRegistry.finishRenderingAllElementsInside( + this.element, + 'core-loading', + 'whenLoaded', + ), + ]); } /** diff --git a/src/core/directives/collapsible-header.ts b/src/core/directives/collapsible-header.ts index cceaca12e..0c74b1a39 100644 --- a/src/core/directives/collapsible-header.ts +++ b/src/core/directives/collapsible-header.ts @@ -284,16 +284,25 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest * @return Promise resolved when loadings are done. */ protected async waitLoadingsDone(): Promise { - await CoreComponentsRegistry.resolveAllElementsInside(this.page, 'core-loading', 'whenLoaded'); + await CoreComponentsRegistry.finishRenderingAllElementsInside( + this.page, + 'core-loading', + 'whenLoaded', + ); } /** * Wait until all children inside the element are done rendering. * * @param element Element. + * @return Promise resolved when texts are rendered. */ protected async waitFormatTextsRendered(element: Element): Promise { - await CoreComponentsRegistry.resolveAllElementsInside(element, 'core-format-text', 'rendered'); + await CoreComponentsRegistry.finishRenderingAllElementsInside( + element, + 'core-format-text', + 'rendered', + ); } /** diff --git a/src/core/directives/collapsible-item.ts b/src/core/directives/collapsible-item.ts index e0c9b15b9..ba4489c4a 100644 --- a/src/core/directives/collapsible-item.ts +++ b/src/core/directives/collapsible-item.ts @@ -13,11 +13,11 @@ // limitations under the License. import { Directive, ElementRef, Input, OnInit } from '@angular/core'; +import { CoreLoadingComponent } from '@components/loading/loading'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { CoreComponentsRegistry } from '@singletons/components-registry'; -import { CoreEventLoadingChangedData, CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreFormatTextDirective } from './format-text'; const defaultMaxHeight = 80; @@ -47,7 +47,6 @@ export class CoreCollapsibleItemDirective implements OnInit { protected expanded = false; protected maxHeight = defaultMaxHeight; protected expandedHeight = 0; - protected loadingChangedListener?: CoreEventObserver; constructor(el: ElementRef) { this.element = el.nativeElement; @@ -79,17 +78,22 @@ export class CoreCollapsibleItemDirective implements OnInit { this.element.classList.add('collapsible-item'); - // Calculate the height now. - await this.calculateHeight(); + await this.waitLoadingsDone(); - // Recalculate the height if a parent core-loading displays the content. - this.loadingChangedListener = - CoreEvents.on(CoreEvents.CORE_LOADING_CHANGED, async (data: CoreEventLoadingChangedData) => { - if (data.loaded && CoreDomUtils.closest(this.element.parentElement, '#' + data.uniqueId)) { - // The element is inside the loading, re-calculate the height. - await this.calculateHeight(); - } - }); + await this.calculateHeight(); + } + + /** + * Wait until all children inside the page. + * + * @return Promise resolved when loadings are done. + */ + protected async waitLoadingsDone(): Promise { + await CoreDomUtils.waitToDom(this.element); + + const page = this.element.closest('.ion-page'); + + await CoreComponentsRegistry.finishRenderingAllElementsInside(page, 'core-loading', 'whenLoaded'); } /** @@ -109,19 +113,19 @@ export class CoreCollapsibleItemDirective implements OnInit { const formatTexts = formatTextElements.map(element => CoreComponentsRegistry.resolve(element, CoreFormatTextDirective)); await Promise.all(formatTexts.map(formatText => formatText?.rendered())); + + await CoreUtils.nextTick(); } /** * Calculate the height and check if we need to display show more or not. */ - protected async calculateHeight(retries = 3): Promise { + protected async calculateHeight(): Promise { // Remove max-height (if any) to calculate the real height. this.element.classList.add('collapsible-loading-height'); await this.waitFormatTextsRendered(this.element); - await CoreUtils.nextTick(); - this.expandedHeight = CoreDomUtils.getElementHeight(this.element) || 0; // Restore the max height now. @@ -129,10 +133,6 @@ export class CoreCollapsibleItemDirective implements OnInit { // If cannot calculate height, shorten always. this.setExpandButtonEnabled(!this.expandedHeight || this.expandedHeight >= this.maxHeight); - - if (this.expandedHeight == 0 && retries > 0) { - setTimeout(() => this.calculateHeight(retries - 1), 200); - } } /** diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index b1927edcc..b3f31119c 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -53,6 +53,7 @@ import { NavigationStart } from '@angular/router'; import { filter } from 'rxjs/operators'; import { Subscription } from 'rxjs'; import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CorePromisedValue } from '@classes/promised-value'; /* * "Utils" service with helper functions for UI, DOM elements and HTML code. @@ -91,6 +92,45 @@ export class CoreDomUtilsProvider { this.debugDisplay = debugDisplay != 0; } + /** + * 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. + */ + waitToDom( + element: Element, + ): CorePromisedValue { + let root = element.getRootNode({ composed: true }); + const inDomPromise = new CorePromisedValue(); + + if (root === document) { + // Already in DOM. + inDomPromise.resolve(); + + return inDomPromise; + } + + // Disconnect observer for performance reasons. + const timeout = window.setTimeout(() => { + inDomPromise.reject(new Error('Waiting for DOM timeout reached')); + observer.disconnect(); + }, 5000); + + const observer = new MutationObserver(() => { + root = element.getRootNode({ composed: true }); + if (root === document) { + observer.disconnect(); + clearTimeout(timeout); + inDomPromise.resolve(); + } + }); + + observer.observe(document.body, { subtree: true, childList: true }); + + return inDomPromise; + } + /** * Equivalent to element.closest(). If the browser doesn't support element.closest, it will * traverse the parents to achieve the same functionality. diff --git a/src/core/singletons/components-registry.ts b/src/core/singletons/components-registry.ts index 8087d5409..db60b35a8 100644 --- a/src/core/singletons/components-registry.ts +++ b/src/core/singletons/components-registry.ts @@ -48,15 +48,15 @@ export class CoreComponentsRegistry { } /** - * Waits all elements to be resolved. + * Waits all elements to be rendered. * * @param element Parent element where to search. * @param selector Selector to search on parent. - * @param fnName Function that needs to get resolved. - * @param params Params of function that needs to get resolved. + * @param fnName Component function that have to be resolved when rendered. + * @param params Params of function that have to be resolved when rendered. * @return Promise resolved when done. */ - static async resolveAllElementsInside( + static async finishRenderingAllElementsInside( element: Element | undefined | null, selector: string, fnName: string, @@ -77,6 +77,8 @@ export class CoreComponentsRegistry { return component[fnName].apply(component, params); })); + + // Wait for next tick to ensure components are completely rendered. await CoreUtils.nextTick(); }