2022-02-24 13:27:39 +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-07 11:04:32 +00:00
|
|
|
import { Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core';
|
2022-02-24 13:27:39 +00:00
|
|
|
import { ScrollDetail } from '@ionic/core';
|
|
|
|
import { IonContent } from '@ionic/angular';
|
|
|
|
import { CoreUtils } from '@services/utils/utils';
|
|
|
|
import { CoreMath } from '@singletons/math';
|
2022-03-01 13:23:29 +00:00
|
|
|
import { CoreComponentsRegistry } from '@singletons/components-registry';
|
|
|
|
import { CoreFormatTextDirective } from './format-text';
|
2022-03-14 21:56:05 +00:00
|
|
|
import { CoreEventObserver, CoreSingleTimeEventObserver } from '@singletons/events';
|
2022-03-11 14:58:02 +00:00
|
|
|
import { CoreLoadingComponent } from '@components/loading/loading';
|
2022-03-14 12:09:41 +00:00
|
|
|
import { CoreDomUtils } from '@services/utils/dom';
|
2022-02-24 13:27:39 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Directive to make an element fixed at the bottom collapsible when scrolling.
|
|
|
|
*
|
|
|
|
* Example usage:
|
|
|
|
*
|
|
|
|
* <div collapsible-footer>
|
|
|
|
*/
|
|
|
|
@Directive({
|
|
|
|
selector: '[collapsible-footer]',
|
|
|
|
})
|
|
|
|
export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
|
|
|
|
|
2022-03-07 11:04:32 +00:00
|
|
|
@Input() appearOnBottom = false;
|
|
|
|
|
2022-02-24 13:27:39 +00:00
|
|
|
protected element: HTMLElement;
|
2022-03-14 12:09:41 +00:00
|
|
|
protected initialHeight = 48;
|
2022-03-07 14:36:56 +00:00
|
|
|
protected finalHeight = 0;
|
2022-03-01 13:23:29 +00:00
|
|
|
protected initialPaddingBottom = '0px';
|
2022-02-24 13:27:39 +00:00
|
|
|
protected previousTop = 0;
|
|
|
|
protected previousHeight = 0;
|
|
|
|
protected content?: HTMLIonContentElement | null;
|
2022-03-01 13:23:29 +00:00
|
|
|
protected loadingChangedListener?: CoreEventObserver;
|
2022-03-09 11:57:10 +00:00
|
|
|
protected contentScrollListener?: EventListener;
|
|
|
|
protected endContentScrollListener?: EventListener;
|
2022-03-14 12:09:41 +00:00
|
|
|
protected resizeListener?: CoreEventObserver;
|
2022-03-14 21:56:05 +00:00
|
|
|
protected domListener?: CoreSingleTimeEventObserver;
|
2022-02-24 13:27:39 +00:00
|
|
|
|
|
|
|
constructor(el: ElementRef, protected ionContent: IonContent) {
|
|
|
|
this.element = el.nativeElement;
|
|
|
|
this.element.setAttribute('slot', 'fixed'); // Just in case somebody forgets to add it.
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-03-14 12:09:41 +00:00
|
|
|
* @inheritdoc
|
2022-02-24 13:27:39 +00:00
|
|
|
*/
|
2022-03-14 12:09:41 +00:00
|
|
|
async ngOnInit(): Promise<void> {
|
|
|
|
// Only if not present or explicitly falsy it will be false.
|
|
|
|
this.appearOnBottom = !CoreUtils.isFalseOrZero(this.appearOnBottom);
|
|
|
|
|
2022-03-14 21:56:05 +00:00
|
|
|
this.domListener = CoreDomUtils.waitToBeInDOM(this.element);
|
|
|
|
await this.domListener.promise;
|
|
|
|
|
2022-03-14 12:09:41 +00:00
|
|
|
await this.waitLoadingsDone();
|
2022-03-01 13:23:29 +00:00
|
|
|
await this.waitFormatTextsRendered(this.element);
|
2022-02-24 13:27:39 +00:00
|
|
|
|
2022-03-14 13:05:44 +00:00
|
|
|
this.content = this.element.closest('ion-content');
|
|
|
|
|
2022-03-14 12:09:41 +00:00
|
|
|
await this.calculateHeight();
|
|
|
|
|
|
|
|
this.listenScrollEvents();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Calculate the height of the footer.
|
|
|
|
*/
|
|
|
|
protected async calculateHeight(): Promise<void> {
|
|
|
|
this.element.classList.remove('is-active');
|
2022-03-01 13:23:29 +00:00
|
|
|
await CoreUtils.nextTick();
|
2022-02-24 13:27:39 +00:00
|
|
|
|
2022-03-01 13:23:29 +00:00
|
|
|
// Set a minimum height value.
|
2022-03-14 12:09:41 +00:00
|
|
|
this.initialHeight = this.element.getBoundingClientRect().height || this.initialHeight;
|
2022-03-07 14:36:56 +00:00
|
|
|
const moduleNav = this.element.querySelector('core-course-module-navigation');
|
|
|
|
if (moduleNav) {
|
|
|
|
this.element.classList.add('has-module-nav');
|
|
|
|
this.finalHeight = this.initialHeight - (moduleNav.getBoundingClientRect().height);
|
|
|
|
}
|
|
|
|
|
2022-03-01 13:23:29 +00:00
|
|
|
this.previousHeight = this.initialHeight;
|
2022-02-24 13:27:39 +00:00
|
|
|
|
2022-03-04 13:33:19 +00:00
|
|
|
this.content?.style.setProperty('--core-collapsible-footer-max-height', this.initialHeight + 'px');
|
2022-03-14 12:09:41 +00:00
|
|
|
this.element.classList.add('is-active');
|
2022-03-04 13:33:19 +00:00
|
|
|
|
2022-03-01 13:23:29 +00:00
|
|
|
this.setBarHeight(this.initialHeight);
|
|
|
|
}
|
2022-02-24 13:27:39 +00:00
|
|
|
|
2022-03-01 13:23:29 +00:00
|
|
|
/**
|
|
|
|
* Setup scroll event listener.
|
|
|
|
*/
|
|
|
|
protected async listenScrollEvents(): Promise<void> {
|
2022-03-14 13:05:44 +00:00
|
|
|
if (!this.content || this.content?.classList.contains('has-collapsible-footer')) {
|
2022-02-24 13:27:39 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.content.classList.add('has-collapsible-footer');
|
|
|
|
|
|
|
|
// Move element to the nearest ion-content if it's not the parent.
|
|
|
|
if (this.element.parentElement?.nodeName != 'ION-CONTENT') {
|
|
|
|
this.content.appendChild(this.element);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set a padding to not overlap elements.
|
2022-03-01 13:23:29 +00:00
|
|
|
this.initialPaddingBottom = this.content.style.getPropertyValue('--padding-bottom') || this.initialPaddingBottom;
|
|
|
|
this.content.style.setProperty(
|
|
|
|
'--padding-bottom',
|
2022-03-04 13:33:19 +00:00
|
|
|
`calc(${this.initialPaddingBottom} + var(--core-collapsible-footer-max-height, 0px))`,
|
2022-03-01 13:23:29 +00:00
|
|
|
);
|
|
|
|
|
2022-02-24 13:27:39 +00:00
|
|
|
const scroll = await this.content.getScrollElement();
|
|
|
|
this.content.scrollEvents = true;
|
|
|
|
|
2022-03-09 11:57:10 +00:00
|
|
|
this.content.addEventListener('ionScroll', this.contentScrollListener = (e: CustomEvent<ScrollDetail>): void => {
|
2022-02-24 13:27:39 +00:00
|
|
|
if (!this.content) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.onScroll(e.detail, scroll);
|
|
|
|
});
|
|
|
|
|
2022-03-09 11:57:10 +00:00
|
|
|
this.content.addEventListener('ionScrollEnd', this.endContentScrollListener = (): void => {
|
|
|
|
if (!this.content) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const height = this.previousHeight;
|
|
|
|
const collapsed = height <= this.finalHeight;
|
|
|
|
const expanded = height >= this.initialHeight;
|
|
|
|
|
|
|
|
if (!collapsed && !expanded) {
|
|
|
|
// Finish opening or closing the bar.
|
|
|
|
const newHeight = (height - this.finalHeight) < (this.initialHeight - this.finalHeight) / 2
|
|
|
|
? this.finalHeight
|
|
|
|
: this.initialHeight;
|
|
|
|
|
|
|
|
this.setBarHeight(newHeight); }
|
|
|
|
});
|
2022-03-14 12:09:41 +00:00
|
|
|
|
|
|
|
this.resizeListener = CoreDomUtils.onWindowResize(() => {
|
|
|
|
this.calculateHeight();
|
|
|
|
}, 50);
|
2022-02-24 13:27:39 +00:00
|
|
|
}
|
|
|
|
|
2022-03-01 13:23:29 +00:00
|
|
|
/**
|
|
|
|
* Wait until all <core-format-text> children inside the element are done rendering.
|
|
|
|
*
|
|
|
|
* @param element Element.
|
|
|
|
*/
|
|
|
|
protected async waitFormatTextsRendered(element: Element): Promise<void> {
|
2022-03-11 14:58:02 +00:00
|
|
|
await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreFormatTextDirective>(
|
|
|
|
element,
|
|
|
|
'core-format-text',
|
|
|
|
'rendered',
|
|
|
|
);
|
2022-03-01 13:23:29 +00:00
|
|
|
}
|
|
|
|
|
2022-02-24 13:27:39 +00:00
|
|
|
/**
|
|
|
|
* On scroll function.
|
|
|
|
*
|
|
|
|
* @param scrollDetail Scroll detail object.
|
|
|
|
* @param scrollElement Scroll element to calculate maxScroll.
|
|
|
|
*/
|
|
|
|
protected onScroll(scrollDetail: ScrollDetail, scrollElement: HTMLElement): void {
|
|
|
|
const maxScroll = scrollElement.scrollHeight - scrollElement.offsetHeight;
|
2022-03-07 11:04:32 +00:00
|
|
|
if (scrollDetail.scrollTop <= 0 || (this.appearOnBottom && scrollDetail.scrollTop >= maxScroll)) {
|
2022-02-24 13:27:39 +00:00
|
|
|
// Reset.
|
|
|
|
this.setBarHeight(this.initialHeight);
|
|
|
|
} else {
|
|
|
|
let newHeight = this.previousHeight - (scrollDetail.scrollTop - this.previousTop);
|
2022-03-07 14:36:56 +00:00
|
|
|
newHeight = CoreMath.clamp(newHeight, this.finalHeight, this.initialHeight);
|
2022-02-24 13:27:39 +00:00
|
|
|
|
|
|
|
this.setBarHeight(newHeight);
|
|
|
|
}
|
|
|
|
this.previousTop = scrollDetail.scrollTop;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the bar height.
|
|
|
|
*
|
|
|
|
* @param height The new bar height.
|
|
|
|
*/
|
|
|
|
protected setBarHeight(height: number): void {
|
2022-03-07 14:36:56 +00:00
|
|
|
const collapsed = height <= this.finalHeight;
|
|
|
|
const expanded = height >= this.initialHeight;
|
|
|
|
this.element.classList.toggle('footer-collapsed', collapsed);
|
|
|
|
this.element.classList.toggle('footer-expanded', expanded);
|
2022-02-24 13:27:39 +00:00
|
|
|
this.content?.style.setProperty('--core-collapsible-footer-height', height + 'px');
|
|
|
|
this.previousHeight = height;
|
2022-03-04 13:33:19 +00:00
|
|
|
}
|
|
|
|
|
2022-03-11 14:58:02 +00:00
|
|
|
/**
|
|
|
|
* Wait until all <core-loading> children inside the page.
|
|
|
|
*
|
|
|
|
* @return Promise resolved when loadings are done.
|
|
|
|
*/
|
|
|
|
protected async waitLoadingsDone(): Promise<void> {
|
|
|
|
const scrollElement = await this.ionContent.getScrollElement();
|
|
|
|
|
|
|
|
await Promise.all([
|
|
|
|
await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent>
|
|
|
|
(scrollElement, 'core-loading', 'whenLoaded'),
|
|
|
|
await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent>(
|
|
|
|
this.element,
|
|
|
|
'core-loading',
|
|
|
|
'whenLoaded',
|
|
|
|
),
|
|
|
|
]);
|
2022-02-24 13:27:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
|
|
|
async ngOnDestroy(): Promise<void> {
|
2022-03-01 13:23:29 +00:00
|
|
|
this.content?.style.setProperty('--padding-bottom', this.initialPaddingBottom);
|
2022-03-09 11:57:10 +00:00
|
|
|
|
|
|
|
if (this.content && this.contentScrollListener) {
|
|
|
|
this.content.removeEventListener('ionScroll', this.contentScrollListener);
|
|
|
|
}
|
|
|
|
if (this.content && this.endContentScrollListener) {
|
|
|
|
this.content.removeEventListener('ionScrollEnd', this.endContentScrollListener);
|
|
|
|
}
|
2022-03-14 12:09:41 +00:00
|
|
|
|
|
|
|
this.resizeListener?.off();
|
2022-03-14 21:56:05 +00:00
|
|
|
this.domListener?.off();
|
2022-02-24 13:27:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|