commit
ab3d55529b
|
@ -60,7 +60,7 @@
|
||||||
[instanceId]="database.coursemodule" component="mod_data" [itemId]="entry.id" area="database_entry" [courseId]="courseId"
|
[instanceId]="database.coursemodule" component="mod_data" [itemId]="entry.id" area="database_entry" [courseId]="courseId"
|
||||||
(onLoading)="setLoadingComments($event)" [showItem]="true">
|
(onLoading)="setLoadingComments($event)" [showItem]="true">
|
||||||
</core-comments>
|
</core-comments>
|
||||||
|
</core-loading>
|
||||||
<div collapsible-footer *ngIf="entryLoaded && hasPrevious || hasNext" slot="fixed" appearOnBottom>
|
<div collapsible-footer *ngIf="entryLoaded && hasPrevious || hasNext" slot="fixed" appearOnBottom>
|
||||||
<ion-row class="ion-justify-content-between ion-align-items-center ion-no-padding ion-wrap">
|
<ion-row class="ion-justify-content-between ion-align-items-center ion-no-padding ion-wrap">
|
||||||
<ion-col class="ion-text-start ion-no-padding core-navigation-arrow" size="auto">
|
<ion-col class="ion-text-start ion-no-padding core-navigation-arrow" size="auto">
|
||||||
|
@ -79,5 +79,4 @@
|
||||||
</ion-col>
|
</ion-col>
|
||||||
</ion-row>
|
</ion-row>
|
||||||
</div>
|
</div>
|
||||||
</core-loading>
|
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
|
@ -18,6 +18,8 @@ import { CoreEventLoadingChangedData, CoreEvents } from '@singletons/events';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreAnimations } from '@components/animations';
|
import { CoreAnimations } from '@components/animations';
|
||||||
import { Translate } from '@singletons';
|
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.
|
* 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;
|
uniqueId: string;
|
||||||
protected element: HTMLElement; // Current element.
|
protected element: HTMLElement; // Current element.
|
||||||
loaded = false; // Only comes true once.
|
loaded = false; // Only comes true once.
|
||||||
|
protected firstLoadedPromise = new CorePromisedValue<string>();
|
||||||
|
|
||||||
constructor(element: ElementRef) {
|
constructor(element: ElementRef) {
|
||||||
this.element = element.nativeElement;
|
this.element = element.nativeElement;
|
||||||
|
CoreComponentsRegistry.register(this.element, this);
|
||||||
|
|
||||||
// Calculate the unique ID.
|
// Calculate the unique ID.
|
||||||
this.uniqueId = 'core-loading-content-' + CoreUtils.getUniqueId('CoreLoadingComponent');
|
this.uniqueId = 'core-loading-content-' + CoreUtils.getUniqueId('CoreLoadingComponent');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being initialized.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (!this.message) {
|
if (!this.message) {
|
||||||
|
@ -77,50 +81,54 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View has been initialized.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
// Add class if loaded on init.
|
this.changeState(!!this.hideUntil);
|
||||||
if (this.hideUntil) {
|
|
||||||
this.element.classList.add('core-loading-loaded');
|
|
||||||
}
|
|
||||||
this.loaded = !!this.hideUntil;
|
|
||||||
|
|
||||||
this.content?.nativeElement.classList.toggle('core-loading-content', !!this.hideUntil);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component input changed.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @param changes Changes.
|
|
||||||
*/
|
*/
|
||||||
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||||
if (changes.hideUntil) {
|
if (changes.hideUntil) {
|
||||||
if (!this.loaded) {
|
this.changeState(!!this.hideUntil);
|
||||||
this.loaded = !!this.hideUntil; // Only comes true once.
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.hideUntil) {
|
/**
|
||||||
setTimeout(() => {
|
* Change loaded state.
|
||||||
// Content is loaded so, center the spinner on the content itself.
|
*
|
||||||
this.element.classList.add('core-loading-loaded');
|
* @param loaded True to load, false otherwise.
|
||||||
// Change CSS to force calculate height.
|
* @return Promise resolved when done.
|
||||||
// Removed 500ms timeout to avoid reallocating html.
|
*/
|
||||||
this.content?.nativeElement.classList.add('core-loading-content');
|
async changeState(loaded: boolean): Promise<void> {
|
||||||
});
|
await CoreUtils.nextTick();
|
||||||
} 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.
|
this.element.classList.toggle('core-loading-loaded', loaded);
|
||||||
setTimeout(() => {
|
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> {
|
CoreEvents.trigger(CoreEvents.CORE_LOADING_CHANGED, <CoreEventLoadingChangedData> {
|
||||||
loaded: !!this.hideUntil,
|
loaded: loaded,
|
||||||
uniqueId: this.uniqueId,
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -198,7 +198,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
|
||||||
* @return Promise resolved with the header element.
|
* @return Promise resolved with the header element.
|
||||||
*/
|
*/
|
||||||
protected async searchHeader(retries: number = 0): Promise<HTMLElement> {
|
protected async searchHeader(retries: number = 0): Promise<HTMLElement> {
|
||||||
let parentPage: HTMLElement = this.element;
|
let parentPage: HTMLElement | null = this.element;
|
||||||
|
|
||||||
while (parentPage) {
|
while (parentPage) {
|
||||||
if (!parentPage.parentElement) {
|
if (!parentPage.parentElement) {
|
||||||
|
@ -207,7 +207,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the next parent page.
|
// Get the next parent page.
|
||||||
parentPage = <HTMLElement> CoreDomUtils.closest(parentPage.parentElement, '.ion-page');
|
parentPage = parentPage.parentElement.closest('.ion-page');
|
||||||
if (parentPage) {
|
if (parentPage) {
|
||||||
// Check if the page has a header. If it doesn't, search the next parent page.
|
// Check if the page has a header. If it doesn't, search the next parent page.
|
||||||
const header = this.searchHeaderInPage(parentPage);
|
const header = this.searchHeaderInPage(parentPage);
|
||||||
|
|
|
@ -19,8 +19,8 @@ import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreMath } from '@singletons/math';
|
import { CoreMath } from '@singletons/math';
|
||||||
import { CoreComponentsRegistry } from '@singletons/components-registry';
|
import { CoreComponentsRegistry } from '@singletons/components-registry';
|
||||||
import { CoreFormatTextDirective } from './format-text';
|
import { CoreFormatTextDirective } from './format-text';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreEventObserver } from '@singletons/events';
|
||||||
import { CoreEventLoadingChangedData, CoreEventObserver, CoreEvents } from '@singletons/events';
|
import { CoreLoadingComponent } from '@components/loading/loading';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Directive to make an element fixed at the bottom collapsible when scrolling.
|
* 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.
|
* @param element Element.
|
||||||
*/
|
*/
|
||||||
protected async waitFormatTextsRendered(element: Element): Promise<void> {
|
protected async waitFormatTextsRendered(element: Element): Promise<void> {
|
||||||
const formatTexts = Array
|
await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreFormatTextDirective>(
|
||||||
.from(element.querySelectorAll('core-format-text'))
|
element,
|
||||||
.map(element => CoreComponentsRegistry.resolve(element, CoreFormatTextDirective));
|
'core-format-text',
|
||||||
|
'rendered',
|
||||||
await Promise.all(formatTexts.map(formatText => formatText?.rendered()));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -184,24 +184,33 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
// 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.
|
// Only if not present or explicitly falsy it will be false.
|
||||||
this.appearOnBottom = !CoreUtils.isFalseOrZero(this.appearOnBottom);
|
this.appearOnBottom = !CoreUtils.isFalseOrZero(this.appearOnBottom);
|
||||||
|
|
||||||
// Recalculate the height if a parent core-loading displays the content.
|
await this.waitLoadingsDone();
|
||||||
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();
|
await this.calculateHeight();
|
||||||
setTimeout(() => this.calculateHeight(), 200);
|
|
||||||
|
this.listenScrollEvents();
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
/**
|
||||||
|
* 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',
|
||||||
|
),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -13,6 +13,8 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Directive, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChange } from '@angular/core';
|
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 { CoreTabsOutletComponent } from '@components/tabs-outlet/tabs-outlet';
|
||||||
import { ScrollDetail } from '@ionic/core';
|
import { ScrollDetail } from '@ionic/core';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
@ -55,32 +57,36 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
|
||||||
@Input() collapsible = true;
|
@Input() collapsible = true;
|
||||||
|
|
||||||
protected page?: HTMLElement;
|
protected page?: HTMLElement;
|
||||||
protected collapsedHeader?: Element;
|
protected collapsedHeader: HTMLIonHeaderElement;
|
||||||
protected collapsedFontStyles?: Partial<CSSStyleDeclaration>;
|
protected collapsedFontStyles?: Partial<CSSStyleDeclaration>;
|
||||||
protected expandedHeader?: Element;
|
protected expandedHeader?: HTMLIonItemElement;
|
||||||
protected expandedHeaderHeight?: number;
|
protected expandedHeaderHeight?: number;
|
||||||
protected expandedFontStyles?: Partial<CSSStyleDeclaration>;
|
protected expandedFontStyles?: Partial<CSSStyleDeclaration>;
|
||||||
protected content?: HTMLIonContentElement;
|
protected content?: HTMLIonContentElement;
|
||||||
protected contentScrollListener?: EventListener;
|
protected contentScrollListener?: EventListener;
|
||||||
protected endContentScrollListener?: EventListener;
|
protected endContentScrollListener?: EventListener;
|
||||||
protected floatingTitle?: HTMLElement;
|
protected pageDidEnterListener?: EventListener;
|
||||||
|
protected floatingTitle?: HTMLHeadingElement;
|
||||||
protected scrollingHeight?: number;
|
protected scrollingHeight?: number;
|
||||||
protected subscriptions: Subscription[] = [];
|
protected subscriptions: Subscription[] = [];
|
||||||
protected enabled = true;
|
protected enabled = true;
|
||||||
protected isWithinContent = false;
|
protected isWithinContent = false;
|
||||||
|
protected enteredPromise = new CorePromisedValue<void>();
|
||||||
|
|
||||||
constructor(protected el: ElementRef) {}
|
constructor(el: ElementRef) {
|
||||||
|
this.collapsedHeader = el.nativeElement;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
this.collapsedHeader = this.el.nativeElement;
|
this.initializePage();
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.initializePage(),
|
|
||||||
this.initializeCollapsedHeader(),
|
this.initializeCollapsedHeader(),
|
||||||
this.initializeExpandedHeader(),
|
this.initializeExpandedHeader(),
|
||||||
|
await this.enteredPromise,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.initializeFloatingTitle();
|
this.initializeFloatingTitle();
|
||||||
|
@ -111,30 +117,42 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
|
||||||
if (this.content && this.endContentScrollListener) {
|
if (this.content && this.endContentScrollListener) {
|
||||||
this.content.removeEventListener('ionScrollEnd', 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.
|
* Search the page element, initialize it, and wait until it's ready for the transition to trigger on scroll.
|
||||||
*/
|
*/
|
||||||
protected async initializePage(): Promise<void> {
|
protected initializePage(): void {
|
||||||
if (!this.collapsedHeader?.parentElement) {
|
if (!this.collapsedHeader.parentElement) {
|
||||||
throw new Error('[collapsible-header] Couldn\'t get page');
|
throw new Error('[collapsible-header] Couldn\'t get page');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find element and prepare classes.
|
// Find element and prepare classes.
|
||||||
this.page = this.collapsedHeader.parentElement;
|
this.page = this.collapsedHeader.parentElement;
|
||||||
|
|
||||||
this.page.classList.add('collapsible-header-page');
|
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.
|
* 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> {
|
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');
|
this.collapsedHeader.classList.add('collapsible-header-collapsed');
|
||||||
|
|
||||||
await this.waitFormatTextsRendered(this.collapsedHeader);
|
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.
|
* 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> {
|
protected async initializeExpandedHeader(): Promise<void> {
|
||||||
do {
|
await this.waitLoadingsDone();
|
||||||
await CoreUtils.wait(50);
|
|
||||||
|
|
||||||
this.expandedHeader = this.page?.querySelector('ion-item[collapsible]') ?? undefined;
|
this.expandedHeader = this.page?.querySelector('ion-item[collapsible]') ?? undefined;
|
||||||
|
|
||||||
if (!this.expandedHeader) {
|
if (!this.expandedHeader) {
|
||||||
continue;
|
throw new Error('[collapsible-header] Couldn\'t initialize expanded header');
|
||||||
}
|
}
|
||||||
|
this.expandedHeader.classList.add('collapsible-header-expanded');
|
||||||
|
|
||||||
await this.waitFormatTextsRendered(this.expandedHeader);
|
await this.waitFormatTextsRendered(this.expandedHeader);
|
||||||
} while (
|
|
||||||
!this.expandedHeader ||
|
|
||||||
this.expandedHeader.clientHeight === 0 ||
|
|
||||||
this.expandedHeader.closest('ion-content.animating')
|
|
||||||
);
|
|
||||||
|
|
||||||
this.expandedHeader.classList.add('collapsible-header-expanded');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -180,6 +191,8 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
|
||||||
|
|
||||||
this.subscriptions.push(outlet.activateEvents.subscribe(onOutletUpdated));
|
this.subscriptions.push(outlet.activateEvents.subscribe(onOutletUpdated));
|
||||||
|
|
||||||
|
onOutletUpdated();
|
||||||
|
|
||||||
return;
|
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.
|
* Initialize a floating title to mimic transitioning the title from one state to the other.
|
||||||
*/
|
*/
|
||||||
protected initializeFloatingTitle(): void {
|
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');
|
throw new Error('[collapsible-header] Couldn\'t create floating title');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add floating title and measure initial position.
|
// Add floating title and measure initial position.
|
||||||
const collapsedHeaderTitle = this.collapsedHeader.querySelector('h1') as HTMLElement;
|
const collapsedHeaderTitle = this.collapsedHeader.querySelector('h1') as HTMLHeadingElement;
|
||||||
const originalTitle = this.expandedHeader.querySelector('h1') as HTMLElement;
|
const originalTitle = this.expandedHeader.querySelector('h1') as HTMLHeadingElement;
|
||||||
const floatingTitleWrapper = originalTitle.parentElement as HTMLElement;
|
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');
|
originalTitle.classList.add('collapsible-header-original-title');
|
||||||
floatingTitle.classList.add('collapsible-header-floating-title');
|
floatingTitle.classList.add('collapsible-header-floating-title');
|
||||||
|
@ -265,17 +278,31 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
|
||||||
this.expandedHeaderHeight = expandedHeaderHeight;
|
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.finishRenderingAllElementsInside<CoreLoadingComponent>(
|
||||||
|
this.page,
|
||||||
|
'core-loading',
|
||||||
|
'whenLoaded',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait until all <core-format-text> children inside the element are done rendering.
|
* Wait until all <core-format-text> children inside the element are done rendering.
|
||||||
*
|
*
|
||||||
* @param element Element.
|
* @param element Element.
|
||||||
|
* @return Promise resolved when texts are rendered.
|
||||||
*/
|
*/
|
||||||
protected async waitFormatTextsRendered(element: Element): Promise<void> {
|
protected async waitFormatTextsRendered(element: Element): Promise<void> {
|
||||||
const formatTexts = Array
|
await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreFormatTextDirective>(
|
||||||
.from(element.querySelectorAll('core-format-text'))
|
element,
|
||||||
.map(element => CoreComponentsRegistry.resolve(element, CoreFormatTextDirective));
|
'core-format-text',
|
||||||
|
'rendered',
|
||||||
await Promise.all(formatTexts.map(formatText => formatText?.rendered()));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -399,6 +426,10 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (page.classList.contains('is-frozen')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const progress = parseFloat(page.style.getPropertyValue('--collapsible-header-progress'));
|
const progress = parseFloat(page.style.getPropertyValue('--collapsible-header-progress'));
|
||||||
const scrollTop = contentScroll.scrollTop;
|
const scrollTop = contentScroll.scrollTop;
|
||||||
const collapse = progress > 0.5;
|
const collapse = progress > 0.5;
|
||||||
|
|
|
@ -13,11 +13,11 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Directive, ElementRef, Input, OnInit } from '@angular/core';
|
import { Directive, ElementRef, Input, OnInit } from '@angular/core';
|
||||||
|
import { CoreLoadingComponent } from '@components/loading/loading';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { CoreComponentsRegistry } from '@singletons/components-registry';
|
import { CoreComponentsRegistry } from '@singletons/components-registry';
|
||||||
import { CoreEventLoadingChangedData, CoreEventObserver, CoreEvents } from '@singletons/events';
|
|
||||||
import { CoreFormatTextDirective } from './format-text';
|
import { CoreFormatTextDirective } from './format-text';
|
||||||
|
|
||||||
const defaultMaxHeight = 80;
|
const defaultMaxHeight = 80;
|
||||||
|
@ -47,7 +47,6 @@ export class CoreCollapsibleItemDirective implements OnInit {
|
||||||
protected expanded = false;
|
protected expanded = false;
|
||||||
protected maxHeight = defaultMaxHeight;
|
protected maxHeight = defaultMaxHeight;
|
||||||
protected expandedHeight = 0;
|
protected expandedHeight = 0;
|
||||||
protected loadingChangedListener?: CoreEventObserver;
|
|
||||||
|
|
||||||
constructor(el: ElementRef<HTMLElement>) {
|
constructor(el: ElementRef<HTMLElement>) {
|
||||||
this.element = el.nativeElement;
|
this.element = el.nativeElement;
|
||||||
|
@ -79,17 +78,22 @@ export class CoreCollapsibleItemDirective implements OnInit {
|
||||||
|
|
||||||
this.element.classList.add('collapsible-item');
|
this.element.classList.add('collapsible-item');
|
||||||
|
|
||||||
// Calculate the height now.
|
await this.waitLoadingsDone();
|
||||||
await this.calculateHeight();
|
|
||||||
|
|
||||||
// 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 <core-loading> children inside the page.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when loadings are done.
|
||||||
|
*/
|
||||||
|
protected async waitLoadingsDone(): Promise<void> {
|
||||||
|
await CoreDomUtils.waitToDom(this.element);
|
||||||
|
|
||||||
|
const page = this.element.closest('.ion-page');
|
||||||
|
|
||||||
|
await CoreComponentsRegistry.finishRenderingAllElementsInside<CoreLoadingComponent>(page, 'core-loading', 'whenLoaded');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -109,19 +113,19 @@ export class CoreCollapsibleItemDirective implements OnInit {
|
||||||
const formatTexts = formatTextElements.map(element => CoreComponentsRegistry.resolve(element, CoreFormatTextDirective));
|
const formatTexts = formatTextElements.map(element => CoreComponentsRegistry.resolve(element, CoreFormatTextDirective));
|
||||||
|
|
||||||
await Promise.all(formatTexts.map(formatText => formatText?.rendered()));
|
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.
|
* Calculate the height and check if we need to display show more or not.
|
||||||
*/
|
*/
|
||||||
protected async calculateHeight(retries = 3): Promise<void> {
|
protected async calculateHeight(): Promise<void> {
|
||||||
// Remove max-height (if any) to calculate the real height.
|
// Remove max-height (if any) to calculate the real height.
|
||||||
this.element.classList.add('collapsible-loading-height');
|
this.element.classList.add('collapsible-loading-height');
|
||||||
|
|
||||||
await this.waitFormatTextsRendered(this.element);
|
await this.waitFormatTextsRendered(this.element);
|
||||||
|
|
||||||
await CoreUtils.nextTick();
|
|
||||||
|
|
||||||
this.expandedHeight = CoreDomUtils.getElementHeight(this.element) || 0;
|
this.expandedHeight = CoreDomUtils.getElementHeight(this.element) || 0;
|
||||||
|
|
||||||
// Restore the max height now.
|
// Restore the max height now.
|
||||||
|
@ -129,10 +133,6 @@ export class CoreCollapsibleItemDirective implements OnInit {
|
||||||
|
|
||||||
// If cannot calculate height, shorten always.
|
// If cannot calculate height, shorten always.
|
||||||
this.setExpandButtonEnabled(!this.expandedHeight || this.expandedHeight >= this.maxHeight);
|
this.setExpandButtonEnabled(!this.expandedHeight || this.expandedHeight >= this.maxHeight);
|
||||||
|
|
||||||
if (this.expandedHeight == 0 && retries > 0) {
|
|
||||||
setTimeout(() => this.calculateHeight(retries - 1), 200);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -27,7 +27,6 @@ import { CoreApp } from '@services/app';
|
||||||
import { CoreFile } from '@services/file';
|
import { CoreFile } from '@services/file';
|
||||||
import { CoreFilepool, CoreFilepoolFileActions, CoreFilepoolFileEventData } from '@services/filepool';
|
import { CoreFilepool, CoreFilepoolFileActions, CoreFilepoolFileEventData } from '@services/filepool';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
|
||||||
import { CoreUrlUtils } from '@services/utils/url';
|
import { CoreUrlUtils } from '@services/utils/url';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { Platform } from '@singletons';
|
import { Platform } from '@singletons';
|
||||||
|
@ -418,14 +417,15 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O
|
||||||
// Set events to download big files (not downloaded automatically).
|
// Set events to download big files (not downloaded automatically).
|
||||||
if (targetAttr !== 'poster' && (tagName === 'VIDEO' || tagName === 'AUDIO' || tagName === 'A' || tagName === 'SOURCE')) {
|
if (targetAttr !== 'poster' && (tagName === 'VIDEO' || tagName === 'AUDIO' || tagName === 'A' || tagName === 'SOURCE')) {
|
||||||
const eventName = tagName == 'A' ? 'click' : 'play';
|
const eventName = tagName == 'A' ? 'click' : 'play';
|
||||||
let clickableEl = this.element;
|
let clickableEl: Element | null = this.element;
|
||||||
|
|
||||||
if (tagName == 'SOURCE') {
|
if (tagName == 'SOURCE') {
|
||||||
clickableEl = <HTMLElement> CoreDomUtils.closest(this.element, 'video,audio');
|
clickableEl = this.element.closest('video,audio');
|
||||||
|
}
|
||||||
|
|
||||||
if (!clickableEl) {
|
if (!clickableEl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
clickableEl.addEventListener(eventName, () => {
|
clickableEl.addEventListener(eventName, () => {
|
||||||
// User played media or opened a downloadable link.
|
// User played media or opened a downloadable link.
|
||||||
|
|
|
@ -458,7 +458,7 @@ export class CoreQuestionBaseComponent {
|
||||||
name: input.name,
|
name: input.name,
|
||||||
value: input.value,
|
value: input.value,
|
||||||
readOnly: input.readOnly,
|
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.
|
// Check if question is marked as correct.
|
||||||
|
|
|
@ -258,7 +258,7 @@ export class CoreQuestionHelperProvider {
|
||||||
// Get the last element and check it's not in the question contents.
|
// Get the last element and check it's not in the question contents.
|
||||||
let last = matches.pop();
|
let last = matches.pop();
|
||||||
while (last) {
|
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.
|
// Not in question contents. Add it to a separate attribute and remove it from the HTML.
|
||||||
question[attrName] = last.innerHTML;
|
question[attrName] = last.innerHTML;
|
||||||
last.parentElement?.removeChild(last);
|
last.parentElement?.removeChild(last);
|
||||||
|
|
|
@ -53,6 +53,7 @@ import { NavigationStart } from '@angular/router';
|
||||||
import { filter } from 'rxjs/operators';
|
import { filter } from 'rxjs/operators';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { CoreComponentsRegistry } from '@singletons/components-registry';
|
import { CoreComponentsRegistry } from '@singletons/components-registry';
|
||||||
|
import { CorePromisedValue } from '@classes/promised-value';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* "Utils" service with helper functions for UI, DOM elements and HTML code.
|
* "Utils" service with helper functions for UI, DOM elements and HTML code.
|
||||||
|
@ -91,6 +92,45 @@ export class CoreDomUtilsProvider {
|
||||||
this.debugDisplay = debugDisplay != 0;
|
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<void> {
|
||||||
|
let root = element.getRootNode({ composed: true });
|
||||||
|
const inDomPromise = new CorePromisedValue<void>();
|
||||||
|
|
||||||
|
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
|
* Equivalent to element.closest(). If the browser doesn't support element.closest, it will
|
||||||
* traverse the parents to achieve the same functionality.
|
* traverse the parents to achieve the same functionality.
|
||||||
|
@ -99,45 +139,10 @@ export class CoreDomUtilsProvider {
|
||||||
* @param element DOM Element.
|
* @param element DOM Element.
|
||||||
* @param selector Selector to search.
|
* @param selector Selector to search.
|
||||||
* @return Closest ancestor.
|
* @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 {
|
closest(element: Element | undefined | null, selector: string): Element | null {
|
||||||
if (!element) {
|
return element?.closest(selector) ?? null;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -12,6 +12,9 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registry to keep track of component instances.
|
* Registry to keep track of component instances.
|
||||||
*/
|
*/
|
||||||
|
@ -36,7 +39,7 @@ export class CoreComponentsRegistry {
|
||||||
* @param componentClass Component class.
|
* @param componentClass Component class.
|
||||||
* @returns Component instance.
|
* @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;
|
const instance = (element && this.instances.get(element) as T) ?? null;
|
||||||
|
|
||||||
return instance && (!componentClass || instance instanceof componentClass)
|
return instance && (!componentClass || instance instanceof componentClass)
|
||||||
|
@ -44,6 +47,41 @@ export class CoreComponentsRegistry {
|
||||||
: null;
|
: 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<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);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Wait for next tick to ensure components are completely rendered.
|
||||||
|
await CoreUtils.nextTick();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue