diff --git a/src/addons/mod/data/pages/entry/entry.html b/src/addons/mod/data/pages/entry/entry.html index ce14704a3..72c1598a7 100644 --- a/src/addons/mod/data/pages/entry/entry.html +++ b/src/addons/mod/data/pages/entry/entry.html @@ -60,24 +60,23 @@ [instanceId]="database.coursemodule" component="mod_data" [itemId]="entry.id" area="database_entry" [courseId]="courseId" (onLoading)="setLoadingComments($event)" [showItem]="true"> - -
- - - - - - - - - - - - - - -
+
+ + + + + + + + + + + + + + +
diff --git a/src/core/components/loading/loading.ts b/src/core/components/loading/loading.ts index a0254ee8b..dd06222c0 100644 --- a/src/core/components/loading/loading.ts +++ b/src/core/components/loading/loading.ts @@ -18,6 +18,8 @@ import { CoreEventLoadingChangedData, CoreEvents } from '@singletons/events'; import { CoreUtils } from '@services/utils/utils'; import { CoreAnimations } from '@components/animations'; import { Translate } from '@singletons'; +import { CoreComponentsRegistry } from '@singletons/components-registry'; +import { CorePromisedValue } from '@classes/promised-value'; /** * Component to show a loading spinner and message while data is being loaded. @@ -56,16 +58,18 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit { uniqueId: string; protected element: HTMLElement; // Current element. loaded = false; // Only comes true once. + protected firstLoadedPromise = new CorePromisedValue(); constructor(element: ElementRef) { this.element = element.nativeElement; + CoreComponentsRegistry.register(this.element, this); // Calculate the unique ID. this.uniqueId = 'core-loading-content-' + CoreUtils.getUniqueId('CoreLoadingComponent'); } /** - * Component being initialized. + * @inheritdoc */ ngOnInit(): void { if (!this.message) { @@ -77,50 +81,54 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit { } /** - * View has been initialized. + * @inheritdoc */ ngAfterViewInit(): void { - // Add class if loaded on init. - if (this.hideUntil) { - this.element.classList.add('core-loading-loaded'); - } - this.loaded = !!this.hideUntil; - - this.content?.nativeElement.classList.toggle('core-loading-content', !!this.hideUntil); + this.changeState(!!this.hideUntil); } /** - * Component input changed. - * - * @param changes Changes. + * @inheritdoc */ ngOnChanges(changes: { [name: string]: SimpleChange }): void { if (changes.hideUntil) { - if (!this.loaded) { - this.loaded = !!this.hideUntil; // Only comes true once. - } - - if (this.hideUntil) { - setTimeout(() => { - // Content is loaded so, center the spinner on the content itself. - this.element.classList.add('core-loading-loaded'); - // Change CSS to force calculate height. - // Removed 500ms timeout to avoid reallocating html. - this.content?.nativeElement.classList.add('core-loading-content'); - }); - } else { - this.element.classList.remove('core-loading-loaded'); - this.content?.nativeElement.classList.remove('core-loading-content'); - } - - // Trigger the event after a timeout since the elements inside ngIf haven't been added to DOM yet. - setTimeout(() => { - CoreEvents.trigger(CoreEvents.CORE_LOADING_CHANGED, { - loaded: !!this.hideUntil, - uniqueId: this.uniqueId, - }); - }); + this.changeState(!!this.hideUntil); } } + /** + * Change loaded state. + * + * @param loaded True to load, false otherwise. + * @return Promise resolved when done. + */ + async changeState(loaded: boolean): Promise { + await CoreUtils.nextTick(); + + this.element.classList.toggle('core-loading-loaded', loaded); + this.content?.nativeElement.classList.add('core-loading-content', loaded); + + await CoreUtils.nextTick(); + + // Wait for next tick before triggering the event to make sure ngIf elements have been added to the DOM. + CoreEvents.trigger(CoreEvents.CORE_LOADING_CHANGED, { + loaded: loaded, + uniqueId: this.uniqueId, + }); + + if (!this.loaded && loaded) { + this.loaded = true; // Only comes true once. + this.firstLoadedPromise.resolve(this.uniqueId); + } + } + + /** + * Wait the loading to finish. + * + * @return Promise resolved with the uniqueId when done. + */ + async whenLoaded(): Promise { + return await this.firstLoadedPromise; + } + } diff --git a/src/core/components/navbar-buttons/navbar-buttons.ts b/src/core/components/navbar-buttons/navbar-buttons.ts index 205ec43db..88e32057f 100644 --- a/src/core/components/navbar-buttons/navbar-buttons.ts +++ b/src/core/components/navbar-buttons/navbar-buttons.ts @@ -198,7 +198,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy { * @return Promise resolved with the header element. */ protected async searchHeader(retries: number = 0): Promise { - let parentPage: HTMLElement = this.element; + let parentPage: HTMLElement | null = this.element; while (parentPage) { if (!parentPage.parentElement) { @@ -207,7 +207,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy { } // Get the next parent page. - parentPage = CoreDomUtils.closest(parentPage.parentElement, '.ion-page'); + parentPage = parentPage.parentElement.closest('.ion-page'); if (parentPage) { // Check if the page has a header. If it doesn't, search the next parent page. const header = this.searchHeaderInPage(parentPage); 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 01f7b0470..0c74b1a39 100644 --- a/src/core/directives/collapsible-header.ts +++ b/src/core/directives/collapsible-header.ts @@ -13,6 +13,8 @@ // limitations under the License. import { Directive, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChange } from '@angular/core'; +import { CorePromisedValue } from '@classes/promised-value'; +import { CoreLoadingComponent } from '@components/loading/loading'; import { CoreTabsOutletComponent } from '@components/tabs-outlet/tabs-outlet'; import { ScrollDetail } from '@ionic/core'; import { CoreUtils } from '@services/utils/utils'; @@ -55,32 +57,36 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest @Input() collapsible = true; protected page?: HTMLElement; - protected collapsedHeader?: Element; + protected collapsedHeader: HTMLIonHeaderElement; protected collapsedFontStyles?: Partial; - protected expandedHeader?: Element; + protected expandedHeader?: HTMLIonItemElement; protected expandedHeaderHeight?: number; protected expandedFontStyles?: Partial; protected content?: HTMLIonContentElement; protected contentScrollListener?: EventListener; protected endContentScrollListener?: EventListener; - protected floatingTitle?: HTMLElement; + protected pageDidEnterListener?: EventListener; + protected floatingTitle?: HTMLHeadingElement; protected scrollingHeight?: number; protected subscriptions: Subscription[] = []; protected enabled = true; protected isWithinContent = false; + protected enteredPromise = new CorePromisedValue(); - constructor(protected el: ElementRef) {} + constructor(el: ElementRef) { + this.collapsedHeader = el.nativeElement; + } /** * @inheritdoc */ async ngOnInit(): Promise { - this.collapsedHeader = this.el.nativeElement; + this.initializePage(); await Promise.all([ - this.initializePage(), this.initializeCollapsedHeader(), this.initializeExpandedHeader(), + await this.enteredPromise, ]); this.initializeFloatingTitle(); @@ -111,30 +117,42 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest if (this.content && this.endContentScrollListener) { this.content.removeEventListener('ionScrollEnd', this.endContentScrollListener); } + if (this.page && this.pageDidEnterListener) { + this.page.removeEventListener('ionViewDidEnter', this.pageDidEnterListener); + } } /** * Search the page element, initialize it, and wait until it's ready for the transition to trigger on scroll. */ - protected async initializePage(): Promise { - if (!this.collapsedHeader?.parentElement) { + protected initializePage(): void { + if (!this.collapsedHeader.parentElement) { throw new Error('[collapsible-header] Couldn\'t get page'); } // Find element and prepare classes. this.page = this.collapsedHeader.parentElement; - this.page.classList.add('collapsible-header-page'); + + this.page.addEventListener( + 'ionViewDidEnter', + this.pageDidEnterListener = () => { + clearTimeout(timeout); + this.enteredPromise.resolve(); + }, + { once: true }, + ); + + // Timeout in case event is never fired. + const timeout = window.setTimeout(() => { + this.enteredPromise.reject(new Error('[collapsible-header] Waiting for ionViewDidEnter timeout reached')); + }, 5000); } /** * Search the collapsed header element, initialize it, and wait until it's ready for the transition to trigger on scroll. */ protected async initializeCollapsedHeader(): Promise { - if (!this.collapsedHeader) { - throw new Error('[collapsible-header] Couldn\'t initialize collapsed header'); - } - this.collapsedHeader.classList.add('collapsible-header-collapsed'); await this.waitFormatTextsRendered(this.collapsedHeader); @@ -144,23 +162,16 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest * Search the expanded header element, initialize it, and wait until it's ready for the transition to trigger on scroll. */ protected async initializeExpandedHeader(): Promise { - do { - await CoreUtils.wait(50); + await this.waitLoadingsDone(); - this.expandedHeader = this.page?.querySelector('ion-item[collapsible]') ?? undefined; - - if (!this.expandedHeader) { - continue; - } - - await this.waitFormatTextsRendered(this.expandedHeader); - } while ( - !this.expandedHeader || - this.expandedHeader.clientHeight === 0 || - this.expandedHeader.closest('ion-content.animating') - ); + this.expandedHeader = this.page?.querySelector('ion-item[collapsible]') ?? undefined; + if (!this.expandedHeader) { + throw new Error('[collapsible-header] Couldn\'t initialize expanded header'); + } this.expandedHeader.classList.add('collapsible-header-expanded'); + + await this.waitFormatTextsRendered(this.expandedHeader); } /** @@ -180,6 +191,8 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest this.subscriptions.push(outlet.activateEvents.subscribe(onOutletUpdated)); + onOutletUpdated(); + return; } @@ -197,15 +210,15 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest * Initialize a floating title to mimic transitioning the title from one state to the other. */ protected initializeFloatingTitle(): void { - if (!this.page || !this.collapsedHeader || !this.expandedHeader) { + if (!this.page || !this.expandedHeader) { throw new Error('[collapsible-header] Couldn\'t create floating title'); } // Add floating title and measure initial position. - const collapsedHeaderTitle = this.collapsedHeader.querySelector('h1') as HTMLElement; - const originalTitle = this.expandedHeader.querySelector('h1') as HTMLElement; + const collapsedHeaderTitle = this.collapsedHeader.querySelector('h1') as HTMLHeadingElement; + const originalTitle = this.expandedHeader.querySelector('h1') as HTMLHeadingElement; const floatingTitleWrapper = originalTitle.parentElement as HTMLElement; - const floatingTitle = originalTitle.cloneNode(true) as HTMLElement; + const floatingTitle = originalTitle.cloneNode(true) as HTMLHeadingElement; originalTitle.classList.add('collapsible-header-original-title'); floatingTitle.classList.add('collapsible-header-floating-title'); @@ -265,17 +278,31 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest this.expandedHeaderHeight = expandedHeaderHeight; } + /** + * Wait until all children inside the page. + * + * @return Promise resolved when loadings are done. + */ + protected async waitLoadingsDone(): Promise { + 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 { - 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', + ); } /** @@ -399,6 +426,10 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest return; } + if (page.classList.contains('is-frozen')) { + return; + } + const progress = parseFloat(page.style.getPropertyValue('--collapsible-header-progress')); const scrollTop = contentScroll.scrollTop; const collapse = progress > 0.5; 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/directives/external-content.ts b/src/core/directives/external-content.ts index 9d3afa58d..908f9ada7 100644 --- a/src/core/directives/external-content.ts +++ b/src/core/directives/external-content.ts @@ -27,7 +27,6 @@ import { CoreApp } from '@services/app'; import { CoreFile } from '@services/file'; import { CoreFilepool, CoreFilepoolFileActions, CoreFilepoolFileEventData } from '@services/filepool'; import { CoreSites } from '@services/sites'; -import { CoreDomUtils } from '@services/utils/dom'; import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils } from '@services/utils/utils'; import { Platform } from '@singletons'; @@ -418,13 +417,14 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O // Set events to download big files (not downloaded automatically). if (targetAttr !== 'poster' && (tagName === 'VIDEO' || tagName === 'AUDIO' || tagName === 'A' || tagName === 'SOURCE')) { const eventName = tagName == 'A' ? 'click' : 'play'; - let clickableEl = this.element; + let clickableEl: Element | null = this.element; if (tagName == 'SOURCE') { - clickableEl = CoreDomUtils.closest(this.element, 'video,audio'); - if (!clickableEl) { - return; - } + clickableEl = this.element.closest('video,audio'); + } + + if (!clickableEl) { + return; } clickableEl.addEventListener(eventName, () => { diff --git a/src/core/features/question/classes/base-question-component.ts b/src/core/features/question/classes/base-question-component.ts index 7753c9d86..86c15df55 100644 --- a/src/core/features/question/classes/base-question-component.ts +++ b/src/core/features/question/classes/base-question-component.ts @@ -458,7 +458,7 @@ export class CoreQuestionBaseComponent { name: input.name, value: input.value, readOnly: input.readOnly, - isInline: !!CoreDomUtils.closest(input, '.qtext'), // The answer can be inside the question text. + isInline: !!input.closest('.qtext'), // The answer can be inside the question text. }; // Check if question is marked as correct. diff --git a/src/core/features/question/services/question-helper.ts b/src/core/features/question/services/question-helper.ts index 27bdfe3d4..dfd095096 100644 --- a/src/core/features/question/services/question-helper.ts +++ b/src/core/features/question/services/question-helper.ts @@ -258,7 +258,7 @@ export class CoreQuestionHelperProvider { // Get the last element and check it's not in the question contents. let last = matches.pop(); while (last) { - if (!CoreDomUtils.closest(last, '.formulation')) { + if (!last.closest('.formulation')) { // Not in question contents. Add it to a separate attribute and remove it from the HTML. question[attrName] = last.innerHTML; last.parentElement?.removeChild(last); diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index a84b182df..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. @@ -99,45 +139,10 @@ export class CoreDomUtilsProvider { * @param element DOM Element. * @param selector Selector to search. * @return Closest ancestor. + * @deprecated Not needed anymore since it's supported on both Android and iOS. Use closest instead. */ closest(element: Element | undefined | null, selector: string): Element | null { - if (!element) { - return null; - } - - // Try to use closest if the browser supports it. - if (typeof element.closest == 'function') { - return element.closest(selector); - } - - if (!this.matchesFunctionName) { - // Find the matches function supported by the browser. - ['matches', 'webkitMatchesSelector', 'mozMatchesSelector', 'msMatchesSelector', 'oMatchesSelector'].some((fn) => { - if (typeof document.body[fn] == 'function') { - this.matchesFunctionName = fn; - - return true; - } - - return false; - }); - - if (!this.matchesFunctionName) { - return null; - } - } - - // Traverse parents. - let elementToTreat: Element | null = element; - - while (elementToTreat) { - if (elementToTreat[this.matchesFunctionName](selector)) { - return elementToTreat; - } - elementToTreat = elementToTreat.parentElement; - } - - return null; + return element?.closest(selector) ?? null; } /** diff --git a/src/core/singletons/components-registry.ts b/src/core/singletons/components-registry.ts index a5c7b12fb..db60b35a8 100644 --- a/src/core/singletons/components-registry.ts +++ b/src/core/singletons/components-registry.ts @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { Component } from '@angular/core'; +import { CoreUtils } from '@services/utils/utils'; + /** * Registry to keep track of component instances. */ @@ -36,7 +39,7 @@ export class CoreComponentsRegistry { * @param componentClass Component class. * @returns Component instance. */ - static resolve(element?: Element | null, componentClass?: ComponentConstructor): T | null { + static resolve(element?: Element | null, componentClass?: ComponentConstructor): T | null { const instance = (element && this.instances.get(element) as T) ?? null; return instance && (!componentClass || instance instanceof componentClass) @@ -44,6 +47,41 @@ export class CoreComponentsRegistry { : null; } + /** + * Waits all elements to be rendered. + * + * @param element Parent element where to search. + * @param selector Selector to search on parent. + * @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 finishRenderingAllElementsInside( + element: Element | undefined | null, + selector: string, + fnName: string, + params?: unknown[], + ): Promise { + if (!element) { + return; + } + + const components = Array + .from(element.querySelectorAll(selector)) + .map(element => CoreComponentsRegistry.resolve(element)); + + await Promise.all(components.map(component => { + if (!component) { + return; + } + + return component[fnName].apply(component, params); + })); + + // Wait for next tick to ensure components are completely rendered. + await CoreUtils.nextTick(); + } + } /**