MOBILE-3814 collapsible: Wait the page transition before calculating
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…
Reference in New Issue