MOBILE-3814 collapsible: Wait the page transition before calculating
This commit is contained in:
		
							parent
							
								
									860c3e7483
								
							
						
					
					
						commit
						463194d526
					
				@ -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<string>();
 | 
			
		||||
 | 
			
		||||
    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, <CoreEventLoadingChangedData> {
 | 
			
		||||
                    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<void> {
 | 
			
		||||
        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, <CoreEventLoadingChangedData> {
 | 
			
		||||
            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<string> {
 | 
			
		||||
        return await this.firstLoadedPromise;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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<CSSStyleDeclaration>;
 | 
			
		||||
    protected expandedHeader?: Element;
 | 
			
		||||
    protected expandedHeader?: HTMLIonItemElement;
 | 
			
		||||
    protected expandedHeaderHeight?: number;
 | 
			
		||||
    protected expandedFontStyles?: Partial<CSSStyleDeclaration>;
 | 
			
		||||
    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<void>();
 | 
			
		||||
 | 
			
		||||
    constructor(protected el: ElementRef) {}
 | 
			
		||||
    constructor(el: ElementRef) {
 | 
			
		||||
        this.collapsedHeader = el.nativeElement;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        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<void> {
 | 
			
		||||
        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<void> {
 | 
			
		||||
        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<void> {
 | 
			
		||||
        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,22 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
 | 
			
		||||
        this.expandedHeaderHeight = expandedHeaderHeight;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Wait until all <core-loading> children inside the page.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when loadings are done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async waitLoadingsDone(): Promise<void> {
 | 
			
		||||
        await CoreComponentsRegistry.resolveAllElementsInside<CoreLoadingComponent>(this.page, 'core-loading', 'whenLoaded');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Wait until all <core-format-text> children inside the element are done rendering.
 | 
			
		||||
     *
 | 
			
		||||
     * @param element Element.
 | 
			
		||||
     */
 | 
			
		||||
    protected async waitFormatTextsRendered(element: Element): Promise<void> {
 | 
			
		||||
        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.resolveAllElementsInside<CoreFormatTextDirective>(element, 'core-format-text', 'rendered');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -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<T>(element?: Element | null, componentClass?: ComponentConstructor<T>): T | null {
 | 
			
		||||
    static resolve<T = Component>(element?: Element | null, componentClass?: ComponentConstructor<T>): T | null {
 | 
			
		||||
        const instance = (element && this.instances.get(element) as T) ?? null;
 | 
			
		||||
 | 
			
		||||
        return instance && (!componentClass || instance instanceof componentClass)
 | 
			
		||||
@ -44,6 +47,39 @@ export class CoreComponentsRegistry {
 | 
			
		||||
            : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Waits all elements to be resolved.
 | 
			
		||||
     *
 | 
			
		||||
     * @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.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    static async resolveAllElementsInside<T = Component>(
 | 
			
		||||
        element: Element | undefined | null,
 | 
			
		||||
        selector: string,
 | 
			
		||||
        fnName: string,
 | 
			
		||||
        params?: unknown[],
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        if (!element) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const components = Array
 | 
			
		||||
            .from(element.querySelectorAll(selector))
 | 
			
		||||
            .map(element => CoreComponentsRegistry.resolve<T>(element));
 | 
			
		||||
 | 
			
		||||
        await Promise.all(components.map(component => {
 | 
			
		||||
            if (!component) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return component[fnName].apply(component, params);
 | 
			
		||||
        }));
 | 
			
		||||
        await CoreUtils.nextTick();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user