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();
+ }
+
}
/**