2021-11-19 12:20:00 +00:00
|
|
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
2022-03-04 14:42:03 +00:00
|
|
|
import { Directive, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChange } from '@angular/core';
|
2022-04-01 07:58:11 +00:00
|
|
|
import { CoreCancellablePromise } from '@classes/cancellable-promise';
|
2022-03-11 11:42:44 +00:00
|
|
|
import { CoreLoadingComponent } from '@components/loading/loading';
|
2022-02-24 16:23:22 +00:00
|
|
|
import { CoreTabsOutletComponent } from '@components/tabs-outlet/tabs-outlet';
|
2022-03-31 09:02:52 +00:00
|
|
|
import { CoreTabsComponent } from '@components/tabs/tabs';
|
2022-03-15 09:36:45 +00:00
|
|
|
import { CoreSettingsHelper } from '@features/settings/services/settings-helper';
|
2021-11-19 12:20:00 +00:00
|
|
|
import { ScrollDetail } from '@ionic/core';
|
|
|
|
import { CoreUtils } from '@services/utils/utils';
|
2022-02-24 16:23:22 +00:00
|
|
|
import { CoreComponentsRegistry } from '@singletons/components-registry';
|
2022-03-22 16:22:14 +00:00
|
|
|
import { CoreDom } from '@singletons/dom';
|
2022-03-14 12:09:41 +00:00
|
|
|
import { CoreEventObserver } from '@singletons/events';
|
2021-11-19 12:20:00 +00:00
|
|
|
import { CoreMath } from '@singletons/math';
|
2022-02-24 16:23:22 +00:00
|
|
|
import { Subscription } from 'rxjs';
|
|
|
|
import { CoreFormatTextDirective } from './format-text';
|
2021-11-19 12:20:00 +00:00
|
|
|
|
|
|
|
/**
|
2022-02-24 16:23:22 +00:00
|
|
|
* Directive to make <ion-header> collapsible.
|
|
|
|
*
|
|
|
|
* This directive expects h1 titles to be duplicated in a header and an item inside the page, and it will transition
|
|
|
|
* from one state to another listening to the scroll in the page content. The item to be used as the expanded form
|
|
|
|
* should also have the [collapsed] attribute.
|
2021-11-19 12:20:00 +00:00
|
|
|
*
|
|
|
|
* Example usage:
|
|
|
|
*
|
|
|
|
* <ion-header collapsible>
|
2022-02-24 16:23:22 +00:00
|
|
|
* <ion-toolbar>
|
|
|
|
* <ion-title>
|
|
|
|
* <h1>Title</h1>
|
|
|
|
* </ion-title>
|
|
|
|
* </ion-toolbar>
|
|
|
|
* </ion-header>
|
|
|
|
*
|
|
|
|
* <ion-content>
|
|
|
|
* <ion-item collapsible>
|
|
|
|
* <ion-label>
|
|
|
|
* <h1>Title</h1>
|
|
|
|
* </ion-label>
|
|
|
|
* </ion-item>
|
|
|
|
* ...
|
|
|
|
* </ion-content>
|
2021-11-19 12:20:00 +00:00
|
|
|
*/
|
|
|
|
@Directive({
|
|
|
|
selector: 'ion-header[collapsible]',
|
|
|
|
})
|
2022-03-04 14:42:03 +00:00
|
|
|
export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDestroy {
|
|
|
|
|
|
|
|
@Input() collapsible = true;
|
2022-02-24 16:23:22 +00:00
|
|
|
|
|
|
|
protected page?: HTMLElement;
|
2022-03-11 11:42:44 +00:00
|
|
|
protected collapsedHeader: HTMLIonHeaderElement;
|
2022-02-24 16:23:22 +00:00
|
|
|
protected collapsedFontStyles?: Partial<CSSStyleDeclaration>;
|
2022-03-11 11:42:44 +00:00
|
|
|
protected expandedHeader?: HTMLIonItemElement;
|
2022-02-24 16:23:22 +00:00
|
|
|
protected expandedHeaderHeight?: number;
|
|
|
|
protected expandedFontStyles?: Partial<CSSStyleDeclaration>;
|
|
|
|
protected content?: HTMLIonContentElement;
|
|
|
|
protected contentScrollListener?: EventListener;
|
2022-03-09 11:57:10 +00:00
|
|
|
protected endContentScrollListener?: EventListener;
|
2022-03-14 12:09:41 +00:00
|
|
|
protected resizeListener?: CoreEventObserver;
|
2022-03-11 11:42:44 +00:00
|
|
|
protected floatingTitle?: HTMLHeadingElement;
|
2022-02-24 16:23:22 +00:00
|
|
|
protected scrollingHeight?: number;
|
|
|
|
protected subscriptions: Subscription[] = [];
|
2022-03-04 14:42:03 +00:00
|
|
|
protected enabled = true;
|
2022-03-09 10:10:58 +00:00
|
|
|
protected isWithinContent = false;
|
2022-03-15 16:21:10 +00:00
|
|
|
protected mutationObserver?: MutationObserver;
|
2022-04-01 07:58:11 +00:00
|
|
|
protected loadingFloatingTitle = false;
|
|
|
|
protected visiblePromise?: CoreCancellablePromise<void>;
|
2022-02-24 16:23:22 +00:00
|
|
|
|
2022-03-11 11:42:44 +00:00
|
|
|
constructor(el: ElementRef) {
|
|
|
|
this.collapsedHeader = el.nativeElement;
|
|
|
|
}
|
2022-02-24 16:23:22 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
2022-03-31 11:09:59 +00:00
|
|
|
ngOnInit(): void {
|
2022-03-31 15:23:32 +00:00
|
|
|
this.collapsible = !CoreUtils.isFalseOrZero(this.collapsible);
|
2022-03-31 11:09:59 +00:00
|
|
|
this.init();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Init function.
|
|
|
|
*/
|
|
|
|
async init(): Promise<void> {
|
|
|
|
if (!this.collapsible || this.expandedHeader) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-03-11 11:42:44 +00:00
|
|
|
this.initializePage();
|
2021-12-01 15:19:26 +00:00
|
|
|
|
2022-02-24 16:23:22 +00:00
|
|
|
await Promise.all([
|
|
|
|
this.initializeCollapsedHeader(),
|
|
|
|
this.initializeExpandedHeader(),
|
|
|
|
]);
|
2021-11-19 12:20:00 +00:00
|
|
|
|
2022-04-01 07:58:11 +00:00
|
|
|
await this.initializeFloatingTitle();
|
2022-02-24 16:23:22 +00:00
|
|
|
this.initializeContent();
|
2021-11-19 12:20:00 +00:00
|
|
|
}
|
|
|
|
|
2022-03-04 14:42:03 +00:00
|
|
|
/**
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
2022-03-31 11:09:59 +00:00
|
|
|
async ngOnChanges(changes: {[name: string]: SimpleChange}): Promise<void> {
|
2022-04-01 07:58:11 +00:00
|
|
|
if (changes.collapsible && !changes.collapsible.firstChange) {
|
2022-03-31 15:23:32 +00:00
|
|
|
this.collapsible = !CoreUtils.isFalseOrZero(changes.collapsible.currentValue);
|
|
|
|
this.enabled = this.collapsible;
|
2022-03-31 11:09:59 +00:00
|
|
|
|
|
|
|
await this.init();
|
|
|
|
|
2022-03-04 14:42:03 +00:00
|
|
|
setTimeout(() => {
|
|
|
|
this.setEnabled(this.enabled);
|
|
|
|
}, 200);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-15 11:43:14 +00:00
|
|
|
/**
|
2022-02-24 16:23:22 +00:00
|
|
|
* @inheritdoc
|
2022-02-15 11:43:14 +00:00
|
|
|
*/
|
2022-02-24 16:23:22 +00:00
|
|
|
ngOnDestroy(): void {
|
|
|
|
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
2022-02-15 11:43:14 +00:00
|
|
|
|
2022-02-24 16:23:22 +00:00
|
|
|
if (this.content && this.contentScrollListener) {
|
|
|
|
this.content.removeEventListener('ionScroll', this.contentScrollListener);
|
2022-02-15 11:43:14 +00:00
|
|
|
}
|
2022-03-09 11:57:10 +00:00
|
|
|
if (this.content && this.endContentScrollListener) {
|
|
|
|
this.content.removeEventListener('ionScrollEnd', this.endContentScrollListener);
|
|
|
|
}
|
2022-03-14 12:09:41 +00:00
|
|
|
|
|
|
|
this.resizeListener?.off();
|
2022-03-15 16:21:10 +00:00
|
|
|
this.mutationObserver?.disconnect();
|
2022-04-01 07:58:11 +00:00
|
|
|
this.visiblePromise?.cancel();
|
2022-02-15 11:43:14 +00:00
|
|
|
}
|
|
|
|
|
2021-11-19 12:20:00 +00:00
|
|
|
/**
|
2022-04-01 07:58:11 +00:00
|
|
|
* Listen to changing events.
|
2021-11-19 12:20:00 +00:00
|
|
|
*/
|
2022-04-01 07:58:11 +00:00
|
|
|
protected listenEvents(): void {
|
2022-03-22 16:22:14 +00:00
|
|
|
this.resizeListener = CoreDom.onWindowResize(() => {
|
2022-03-14 12:09:41 +00:00
|
|
|
this.initializeFloatingTitle();
|
|
|
|
}, 50);
|
2022-03-15 09:36:45 +00:00
|
|
|
|
|
|
|
this.subscriptions.push(CoreSettingsHelper.onDarkModeChange().subscribe(() => {
|
|
|
|
this.initializeFloatingTitle();
|
|
|
|
}));
|
2022-03-15 16:21:10 +00:00
|
|
|
|
|
|
|
this.mutationObserver = new MutationObserver(() => {
|
|
|
|
if (!this.expandedHeader) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const originalTitle = this.expandedHeader.querySelector('h1.collapsible-header-original-title') ||
|
|
|
|
this.expandedHeader.querySelector('h1') as HTMLHeadingElement;
|
|
|
|
|
|
|
|
const floatingTitleWrapper = originalTitle.parentElement as HTMLElement;
|
|
|
|
const floatingTitle = floatingTitleWrapper.querySelector('.collapsible-header-floating-title') as HTMLHeadingElement;
|
|
|
|
|
|
|
|
if (!floatingTitle || !originalTitle) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Original title changed, change the contents.
|
|
|
|
const newFloatingTitle = originalTitle.cloneNode(true) as HTMLHeadingElement;
|
|
|
|
newFloatingTitle.classList.add('collapsible-header-floating-title');
|
|
|
|
newFloatingTitle.classList.remove('collapsible-header-original-title');
|
|
|
|
|
|
|
|
floatingTitleWrapper.replaceChild(newFloatingTitle, floatingTitle);
|
|
|
|
|
|
|
|
this.initializeFloatingTitle();
|
|
|
|
});
|
2022-02-24 16:23:22 +00:00
|
|
|
}
|
2022-01-24 10:19:34 +00:00
|
|
|
|
2022-04-01 07:58:11 +00:00
|
|
|
/**
|
|
|
|
* Search the page element, initialize it, and wait until it's ready for the transition to trigger on scroll.
|
|
|
|
*/
|
|
|
|
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');
|
|
|
|
}
|
|
|
|
|
2022-02-24 16:23:22 +00:00
|
|
|
/**
|
|
|
|
* 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> {
|
|
|
|
this.collapsedHeader.classList.add('collapsible-header-collapsed');
|
|
|
|
|
|
|
|
await this.waitFormatTextsRendered(this.collapsedHeader);
|
2021-11-19 12:20:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-02-24 16:23:22 +00:00
|
|
|
* Search the expanded header element, initialize it, and wait until it's ready for the transition to trigger on scroll.
|
2021-11-19 12:20:00 +00:00
|
|
|
*/
|
2022-02-24 16:23:22 +00:00
|
|
|
protected async initializeExpandedHeader(): Promise<void> {
|
2022-03-11 11:42:44 +00:00
|
|
|
await this.waitLoadingsDone();
|
2022-02-24 16:23:22 +00:00
|
|
|
|
2022-03-11 11:42:44 +00:00
|
|
|
this.expandedHeader = this.page?.querySelector('ion-item[collapsible]') ?? undefined;
|
2022-02-24 16:23:22 +00:00
|
|
|
|
2022-03-11 11:42:44 +00:00
|
|
|
if (!this.expandedHeader) {
|
2022-03-29 11:04:19 +00:00
|
|
|
this.enabled = false;
|
|
|
|
this.setEnabled(this.enabled);
|
|
|
|
|
2022-03-11 11:42:44 +00:00
|
|
|
throw new Error('[collapsible-header] Couldn\'t initialize expanded header');
|
2022-03-29 11:04:19 +00:00
|
|
|
|
2022-03-11 11:42:44 +00:00
|
|
|
}
|
2022-02-24 16:23:22 +00:00
|
|
|
this.expandedHeader.classList.add('collapsible-header-expanded');
|
2022-03-11 11:42:44 +00:00
|
|
|
|
|
|
|
await this.waitFormatTextsRendered(this.expandedHeader);
|
2021-11-19 12:20:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-02-24 16:23:22 +00:00
|
|
|
* Search the page content, initialize it, and wait until it's ready for the transition to trigger on scroll.
|
2021-11-19 12:20:00 +00:00
|
|
|
*/
|
2022-02-24 16:23:22 +00:00
|
|
|
protected async initializeContent(): Promise<void> {
|
2022-03-31 09:02:52 +00:00
|
|
|
if (!this.page) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-04-01 07:58:11 +00:00
|
|
|
this.listenEvents();
|
|
|
|
|
2022-02-24 16:23:22 +00:00
|
|
|
// Initialize from tabs.
|
2022-03-31 09:02:52 +00:00
|
|
|
const tabs = CoreComponentsRegistry.resolve(this.page.querySelector('core-tabs-outlet'), CoreTabsOutletComponent);
|
2021-11-19 12:20:00 +00:00
|
|
|
|
2022-02-24 16:23:22 +00:00
|
|
|
if (tabs) {
|
|
|
|
const outlet = tabs.getOutlet();
|
|
|
|
const onOutletUpdated = () => {
|
|
|
|
const activePage = outlet.nativeEl.querySelector('.ion-page:not(.ion-page-hidden)');
|
2021-11-19 12:20:00 +00:00
|
|
|
|
2022-02-24 16:23:22 +00:00
|
|
|
this.updateContent(activePage?.querySelector('ion-content:not(.disable-scroll-y)') as HTMLIonContentElement);
|
|
|
|
};
|
2021-11-19 12:20:00 +00:00
|
|
|
|
2022-02-24 16:23:22 +00:00
|
|
|
this.subscriptions.push(outlet.activateEvents.subscribe(onOutletUpdated));
|
2022-01-18 13:44:43 +00:00
|
|
|
|
2022-03-11 11:42:44 +00:00
|
|
|
onOutletUpdated();
|
|
|
|
|
2022-01-18 13:44:43 +00:00
|
|
|
return;
|
|
|
|
}
|
2021-11-19 12:20:00 +00:00
|
|
|
|
2022-02-24 16:23:22 +00:00
|
|
|
// Initialize from page content.
|
2022-03-31 09:02:52 +00:00
|
|
|
const content = this.page.querySelector('ion-content:not(.disable-scroll-y)');
|
2021-11-19 12:20:00 +00:00
|
|
|
|
2022-02-24 16:23:22 +00:00
|
|
|
if (!content) {
|
|
|
|
throw new Error('[collapsible-header] Couldn\'t get content');
|
2021-11-19 12:20:00 +00:00
|
|
|
}
|
|
|
|
|
2022-02-24 16:23:22 +00:00
|
|
|
this.trackContentScroll(content as HTMLIonContentElement);
|
|
|
|
}
|
2021-12-09 11:28:44 +00:00
|
|
|
|
2022-02-24 16:23:22 +00:00
|
|
|
/**
|
|
|
|
* Initialize a floating title to mimic transitioning the title from one state to the other.
|
|
|
|
*/
|
2022-04-01 07:58:11 +00:00
|
|
|
protected async initializeFloatingTitle(): Promise<void> {
|
2022-03-11 11:42:44 +00:00
|
|
|
if (!this.page || !this.expandedHeader) {
|
2022-04-01 07:58:11 +00:00
|
|
|
return;
|
2021-11-19 12:20:00 +00:00
|
|
|
}
|
|
|
|
|
2022-04-01 07:58:11 +00:00
|
|
|
if (this.loadingFloatingTitle) {
|
|
|
|
// Already calculating, return.
|
2022-03-30 11:00:30 +00:00
|
|
|
return;
|
|
|
|
}
|
2022-04-01 07:58:11 +00:00
|
|
|
this.loadingFloatingTitle = true;
|
2022-03-30 11:00:30 +00:00
|
|
|
|
2022-04-01 07:58:11 +00:00
|
|
|
this.visiblePromise = CoreDom.waitToBeVisible(this.expandedHeader);
|
|
|
|
await this.visiblePromise;
|
2022-03-30 11:00:30 +00:00
|
|
|
|
2022-03-15 10:34:03 +00:00
|
|
|
this.page.classList.remove('collapsible-header-page-is-active');
|
2022-04-01 07:58:11 +00:00
|
|
|
await CoreUtils.nextTick();
|
2022-03-14 12:09:41 +00:00
|
|
|
|
2022-02-24 16:23:22 +00:00
|
|
|
// Add floating title and measure initial position.
|
2022-03-11 11:42:44 +00:00
|
|
|
const collapsedHeaderTitle = this.collapsedHeader.querySelector('h1') as HTMLHeadingElement;
|
2022-03-14 12:09:41 +00:00
|
|
|
const originalTitle = this.expandedHeader.querySelector('h1.collapsible-header-original-title') ||
|
|
|
|
this.expandedHeader.querySelector('h1') as HTMLHeadingElement;
|
|
|
|
|
2022-02-24 16:23:22 +00:00
|
|
|
const floatingTitleWrapper = originalTitle.parentElement as HTMLElement;
|
2022-03-14 12:09:41 +00:00
|
|
|
let floatingTitle = floatingTitleWrapper.querySelector('.collapsible-header-floating-title') as HTMLHeadingElement;
|
|
|
|
if (!floatingTitle) {
|
|
|
|
// First time, create it.
|
|
|
|
floatingTitle = originalTitle.cloneNode(true) as HTMLHeadingElement;
|
|
|
|
floatingTitle.classList.add('collapsible-header-floating-title');
|
|
|
|
|
|
|
|
floatingTitleWrapper.classList.add('collapsible-header-floating-title-wrapper');
|
|
|
|
floatingTitleWrapper.insertBefore(floatingTitle, originalTitle);
|
2022-02-24 16:23:22 +00:00
|
|
|
|
2022-03-14 12:09:41 +00:00
|
|
|
originalTitle.classList.add('collapsible-header-original-title');
|
2022-03-15 16:21:10 +00:00
|
|
|
this.mutationObserver?.observe(originalTitle, { childList: true, subtree: true });
|
2022-03-14 12:09:41 +00:00
|
|
|
}
|
2022-02-24 16:23:22 +00:00
|
|
|
|
|
|
|
const floatingTitleBoundingBox = floatingTitle.getBoundingClientRect();
|
|
|
|
|
|
|
|
// Prepare styles variables.
|
|
|
|
const collapsedHeaderTitleBoundingBox = collapsedHeaderTitle.getBoundingClientRect();
|
|
|
|
const collapsedTitleStyles = getComputedStyle(collapsedHeaderTitle);
|
|
|
|
const expandedHeaderHeight = this.expandedHeader.clientHeight;
|
|
|
|
const expandedTitleStyles = getComputedStyle(originalTitle);
|
|
|
|
const originalTitleBoundingBox = originalTitle.getBoundingClientRect();
|
|
|
|
const textProperties = ['overflow', 'white-space', 'text-overflow', 'color'];
|
|
|
|
const [collapsedFontStyles, expandedFontStyles] = Array
|
|
|
|
.from(collapsedTitleStyles)
|
|
|
|
.filter(
|
|
|
|
property =>
|
|
|
|
property.startsWith('font-') ||
|
|
|
|
property.startsWith('letter-') ||
|
|
|
|
textProperties.includes(property),
|
|
|
|
)
|
|
|
|
.reduce((styles, property) => {
|
|
|
|
styles[0][property] = collapsedTitleStyles.getPropertyValue(property);
|
|
|
|
styles[1][property] = expandedTitleStyles.getPropertyValue(property);
|
|
|
|
|
|
|
|
return styles;
|
|
|
|
}, [{}, {}]);
|
|
|
|
const cssVariables = {
|
|
|
|
'--collapsible-header-collapsed-height': `${this.collapsedHeader.clientHeight}px`,
|
|
|
|
'--collapsible-header-expanded-y-delta': `-${this.collapsedHeader.clientHeight}px`,
|
|
|
|
'--collapsible-header-expanded-height': `${expandedHeaderHeight}px`,
|
|
|
|
'--collapsible-header-floating-title-top': `${originalTitleBoundingBox.top - floatingTitleBoundingBox.top}px`,
|
|
|
|
'--collapsible-header-floating-title-left': `${originalTitleBoundingBox.left - floatingTitleBoundingBox.left}px`,
|
|
|
|
'--collapsible-header-floating-title-width': `${originalTitle.clientWidth}px`,
|
|
|
|
'--collapsible-header-floating-title-x-delta':
|
|
|
|
`${collapsedHeaderTitleBoundingBox.left - originalTitleBoundingBox.left}px`,
|
|
|
|
'--collapsible-header-floating-title-width-delta': `${collapsedHeaderTitle.clientWidth - originalTitle.clientWidth}px`,
|
|
|
|
};
|
|
|
|
|
|
|
|
Object
|
|
|
|
.entries(cssVariables)
|
|
|
|
.forEach(([property, value]) => this.page?.style.setProperty(property, value));
|
|
|
|
|
|
|
|
Object
|
|
|
|
.entries(expandedFontStyles)
|
|
|
|
.forEach(([property, value]) => floatingTitle.style.setProperty(property, value as string));
|
|
|
|
|
|
|
|
// Activate styles.
|
2022-03-15 10:34:03 +00:00
|
|
|
this.page.classList.add('collapsible-header-page-is-active');
|
2022-02-24 16:23:22 +00:00
|
|
|
|
|
|
|
this.floatingTitle = floatingTitle;
|
|
|
|
this.scrollingHeight = originalTitleBoundingBox.top - collapsedHeaderTitleBoundingBox.top;
|
|
|
|
this.collapsedFontStyles = collapsedFontStyles;
|
|
|
|
this.expandedFontStyles = expandedFontStyles;
|
|
|
|
this.expandedHeaderHeight = expandedHeaderHeight;
|
2022-04-01 07:58:11 +00:00
|
|
|
|
|
|
|
this.loadingFloatingTitle = false;
|
2022-01-24 10:19:34 +00:00
|
|
|
}
|
|
|
|
|
2022-03-11 11:42:44 +00:00
|
|
|
/**
|
|
|
|
* Wait until all <core-loading> children inside the page.
|
|
|
|
*/
|
|
|
|
protected async waitLoadingsDone(): Promise<void> {
|
2022-03-16 12:49:58 +00:00
|
|
|
if (!this.page) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-03-31 09:02:52 +00:00
|
|
|
// Wait loadings to finish.
|
2022-03-16 12:49:58 +00:00
|
|
|
await CoreComponentsRegistry.waitComponentsReady(this.page, 'core-loading', CoreLoadingComponent);
|
2022-03-31 09:02:52 +00:00
|
|
|
|
|
|
|
// Wait tabs to be ready.
|
|
|
|
await CoreComponentsRegistry.waitComponentsReady(this.page, 'core-tabs', CoreTabsComponent);
|
|
|
|
await CoreComponentsRegistry.waitComponentsReady(this.page, 'core-tabs-outlet', CoreTabsOutletComponent);
|
|
|
|
|
|
|
|
// Wait loadings to finish, inside tabs (if any).
|
|
|
|
await CoreComponentsRegistry.waitComponentsReady(
|
|
|
|
this.page,
|
|
|
|
'core-tab core-loading, ion-router-outlet core-loading',
|
|
|
|
CoreLoadingComponent,
|
|
|
|
);
|
2022-03-11 11:42:44 +00:00
|
|
|
}
|
|
|
|
|
2022-01-24 10:19:34 +00:00
|
|
|
/**
|
2022-02-24 16:23:22 +00:00
|
|
|
* Wait until all <core-format-text> children inside the element are done rendering.
|
2022-01-24 10:19:34 +00:00
|
|
|
*
|
2022-02-24 16:23:22 +00:00
|
|
|
* @param element Element.
|
2022-03-11 14:58:02 +00:00
|
|
|
* @return Promise resolved when texts are rendered.
|
2022-01-24 10:19:34 +00:00
|
|
|
*/
|
2022-02-24 16:23:22 +00:00
|
|
|
protected async waitFormatTextsRendered(element: Element): Promise<void> {
|
2022-03-16 12:49:58 +00:00
|
|
|
await CoreComponentsRegistry.waitComponentsReady(element, 'core-format-text', CoreFormatTextDirective);
|
2022-02-24 16:23:22 +00:00
|
|
|
}
|
2022-01-24 10:19:34 +00:00
|
|
|
|
2022-02-24 16:23:22 +00:00
|
|
|
/**
|
|
|
|
* Update content element whos scroll is being tracked.
|
|
|
|
*
|
|
|
|
* @param content Content element.
|
|
|
|
*/
|
|
|
|
protected updateContent(content?: HTMLIonContentElement | null): void {
|
2022-03-09 08:53:12 +00:00
|
|
|
if (content === (this.content ?? null)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-03-09 11:57:10 +00:00
|
|
|
if (this.content) {
|
|
|
|
if (this.contentScrollListener) {
|
|
|
|
this.content.removeEventListener('ionScroll', this.contentScrollListener);
|
|
|
|
delete this.contentScrollListener;
|
|
|
|
}
|
2021-11-19 12:20:00 +00:00
|
|
|
|
2022-03-09 11:57:10 +00:00
|
|
|
if (this.endContentScrollListener) {
|
|
|
|
this.content.removeEventListener('ionScrollEnd', this.endContentScrollListener);
|
|
|
|
delete this.endContentScrollListener;
|
|
|
|
}
|
2022-03-09 08:53:12 +00:00
|
|
|
|
2022-02-24 16:23:22 +00:00
|
|
|
delete this.content;
|
2021-11-19 12:20:00 +00:00
|
|
|
}
|
|
|
|
|
2022-02-24 16:23:22 +00:00
|
|
|
content && this.trackContentScroll(content);
|
2021-11-19 12:20:00 +00:00
|
|
|
}
|
|
|
|
|
2022-03-04 14:42:03 +00:00
|
|
|
/**
|
|
|
|
* Set collapsed/expanded based on properties.
|
|
|
|
*
|
|
|
|
* @param enable True to enable, false otherwise
|
|
|
|
*/
|
|
|
|
async setEnabled(enable: boolean): Promise<void> {
|
2022-03-29 11:04:19 +00:00
|
|
|
if (!this.page) {
|
2022-03-04 14:42:03 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-03-29 11:04:19 +00:00
|
|
|
if (enable && this.content) {
|
2022-03-04 14:42:03 +00:00
|
|
|
const contentScroll = await this.content.getScrollElement();
|
|
|
|
|
|
|
|
// Do nothing, since scroll has already started on the page.
|
|
|
|
if (contentScroll.scrollTop > 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.page.style.setProperty('--collapsible-header-progress', enable ? '0' : '1');
|
2022-03-15 10:34:03 +00:00
|
|
|
this.page.classList.toggle('collapsible-header-page-is-collapsed', !enable);
|
2022-03-04 14:42:03 +00:00
|
|
|
}
|
|
|
|
|
2021-11-19 12:20:00 +00:00
|
|
|
/**
|
2022-02-24 16:23:22 +00:00
|
|
|
* Listen to a content element for scroll events that will control the header state transition.
|
|
|
|
*
|
|
|
|
* @param content Content element.
|
2021-11-19 12:20:00 +00:00
|
|
|
*/
|
2022-02-24 16:23:22 +00:00
|
|
|
protected async trackContentScroll(content: HTMLIonContentElement): Promise<void> {
|
|
|
|
if (content === this.content) {
|
2022-01-24 10:19:34 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-03-04 14:42:03 +00:00
|
|
|
this.content = content;
|
|
|
|
|
2022-02-24 16:23:22 +00:00
|
|
|
const page = this.page;
|
|
|
|
const scrollingHeight = this.scrollingHeight;
|
|
|
|
const expandedHeader = this.expandedHeader;
|
|
|
|
const expandedHeaderHeight = this.expandedHeaderHeight;
|
|
|
|
const expandedFontStyles = this.expandedFontStyles;
|
|
|
|
const collapsedFontStyles = this.collapsedFontStyles;
|
|
|
|
const floatingTitle = this.floatingTitle;
|
2022-03-04 14:42:03 +00:00
|
|
|
const contentScroll = await this.content.getScrollElement();
|
2022-02-24 16:23:22 +00:00
|
|
|
|
|
|
|
if (
|
|
|
|
!page ||
|
|
|
|
!scrollingHeight ||
|
|
|
|
!expandedHeader ||
|
|
|
|
!expandedHeaderHeight ||
|
|
|
|
!expandedFontStyles ||
|
|
|
|
!collapsedFontStyles ||
|
|
|
|
!floatingTitle
|
|
|
|
) {
|
2022-03-15 10:34:03 +00:00
|
|
|
page?.classList.remove('collapsible-header-page-is-active');
|
2022-02-24 16:23:22 +00:00
|
|
|
throw new Error('[collapsible-header] Couldn\'t set up scrolling');
|
2022-02-17 13:16:49 +00:00
|
|
|
}
|
2021-11-19 12:20:00 +00:00
|
|
|
|
2022-03-09 10:10:58 +00:00
|
|
|
this.isWithinContent = content.contains(expandedHeader);
|
2022-03-15 10:34:03 +00:00
|
|
|
page.classList.toggle('collapsible-header-page-is-within-content', this.isWithinContent);
|
2022-03-04 14:42:03 +00:00
|
|
|
this.setEnabled(this.enabled);
|
2022-01-24 10:19:34 +00:00
|
|
|
|
2022-02-24 16:23:22 +00:00
|
|
|
Object
|
|
|
|
.entries(expandedFontStyles)
|
|
|
|
.forEach(([property, value]) => floatingTitle.style.setProperty(property, value as string));
|
2021-11-19 12:20:00 +00:00
|
|
|
|
2022-03-09 11:57:10 +00:00
|
|
|
this.content.scrollEvents = true;
|
2022-02-24 16:23:22 +00:00
|
|
|
this.content.addEventListener('ionScroll', this.contentScrollListener = ({ target }: CustomEvent<ScrollDetail>): void => {
|
2022-03-04 14:42:03 +00:00
|
|
|
if (target !== this.content || !this.enabled) {
|
2022-02-24 16:23:22 +00:00
|
|
|
return;
|
|
|
|
}
|
2021-11-19 12:20:00 +00:00
|
|
|
|
2022-02-24 16:23:22 +00:00
|
|
|
const scrollableHeight = contentScroll.scrollHeight - contentScroll.clientHeight;
|
2022-03-09 10:10:58 +00:00
|
|
|
|
|
|
|
let frozen = false;
|
|
|
|
if (this.isWithinContent) {
|
|
|
|
frozen = scrollableHeight <= scrollingHeight;
|
|
|
|
} else {
|
|
|
|
const collapsedHeight = expandedHeaderHeight - (expandedHeader.clientHeight ?? 0);
|
|
|
|
frozen = scrollableHeight + collapsedHeight <= 2 * expandedHeaderHeight;
|
|
|
|
}
|
2022-02-24 16:23:22 +00:00
|
|
|
const progress = frozen
|
|
|
|
? 0
|
2022-03-09 10:10:58 +00:00
|
|
|
: CoreMath.clamp(contentScroll.scrollTop / scrollingHeight, 0, 1);
|
2021-11-19 12:20:00 +00:00
|
|
|
|
2022-02-24 16:23:22 +00:00
|
|
|
page.style.setProperty('--collapsible-header-progress', `${progress}`);
|
2022-03-15 10:34:03 +00:00
|
|
|
page.classList.toggle('collapsible-header-page-is-frozen', frozen);
|
|
|
|
page.classList.toggle('collapsible-header-page-is-collapsed', progress === 1);
|
2021-11-19 12:20:00 +00:00
|
|
|
|
2022-02-24 16:23:22 +00:00
|
|
|
Object
|
|
|
|
.entries(progress > .5 ? collapsedFontStyles : expandedFontStyles)
|
|
|
|
.forEach(([property, value]) => floatingTitle.style.setProperty(property, value as string));
|
|
|
|
});
|
2021-11-19 12:20:00 +00:00
|
|
|
|
2022-03-09 11:57:10 +00:00
|
|
|
this.content.addEventListener(
|
|
|
|
'ionScrollEnd',
|
|
|
|
this.endContentScrollListener = ({ target }: CustomEvent<ScrollDetail>): void => {
|
|
|
|
if (target !== this.content || !this.enabled) {
|
|
|
|
return;
|
|
|
|
}
|
2022-03-04 14:43:02 +00:00
|
|
|
|
2022-03-15 10:34:03 +00:00
|
|
|
if (page.classList.contains('collapsible-header-page-is-frozen')) {
|
2022-03-11 09:09:29 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-03-09 11:57:10 +00:00
|
|
|
const progress = parseFloat(page.style.getPropertyValue('--collapsible-header-progress'));
|
|
|
|
const scrollTop = contentScroll.scrollTop;
|
|
|
|
const collapse = progress > 0.5;
|
2022-03-04 14:43:02 +00:00
|
|
|
|
2022-03-09 11:57:10 +00:00
|
|
|
page.style.setProperty('--collapsible-header-progress', collapse ? '1' : '0');
|
2022-03-15 10:34:03 +00:00
|
|
|
page.classList.toggle('collapsible-header-page-is-collapsed', collapse);
|
2022-03-07 12:19:21 +00:00
|
|
|
|
2022-03-09 11:57:10 +00:00
|
|
|
if (collapse && this.scrollingHeight && this.scrollingHeight > 0 && scrollTop < this.scrollingHeight) {
|
|
|
|
this.content?.scrollToPoint(null, this.scrollingHeight);
|
|
|
|
}
|
2022-03-07 12:19:21 +00:00
|
|
|
|
2022-03-09 11:57:10 +00:00
|
|
|
if (!collapse && this.scrollingHeight && this.scrollingHeight > 0 && scrollTop > 0) {
|
|
|
|
this.content?.scrollToPoint(null, 0);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
2022-03-04 14:43:02 +00:00
|
|
|
}
|
|
|
|
|
2021-11-19 12:20:00 +00:00
|
|
|
}
|