commit
ab3d55529b
|
@ -60,24 +60,23 @@
|
|||
[instanceId]="database.coursemodule" component="mod_data" [itemId]="entry.id" area="database_entry" [courseId]="courseId"
|
||||
(onLoading)="setLoadingComments($event)" [showItem]="true">
|
||||
</core-comments>
|
||||
|
||||
<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-col class="ion-text-start ion-no-padding core-navigation-arrow" size="auto">
|
||||
<ion-button [disabled]="!hasPrevious" fill="clear" [attr.aria-label]="'core.previous' | translate"
|
||||
(click)="gotoEntry(offset! -1)">
|
||||
<ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
<ion-col class="ion-text-center">
|
||||
</ion-col>
|
||||
<ion-col class="ion-text-end ion-no-padding core-navigation-arrow" size="auto">
|
||||
<ion-button [disabled]="!hasNext" fill="clear" [attr.aria-label]=" 'core.next' | translate"
|
||||
(click)="gotoEntry(offset! + 1)">
|
||||
<ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</div>
|
||||
</core-loading>
|
||||
<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-col class="ion-text-start ion-no-padding core-navigation-arrow" size="auto">
|
||||
<ion-button [disabled]="!hasPrevious" fill="clear" [attr.aria-label]="'core.previous' | translate"
|
||||
(click)="gotoEntry(offset! -1)">
|
||||
<ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
<ion-col class="ion-text-center">
|
||||
</ion-col>
|
||||
<ion-col class="ion-text-end ion-no-padding core-navigation-arrow" size="auto">
|
||||
<ion-button [disabled]="!hasNext" fill="clear" [attr.aria-label]=" 'core.next' | translate"
|
||||
(click)="gotoEntry(offset! + 1)">
|
||||
<ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</div>
|
||||
</ion-content>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -198,7 +198,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
|
|||
* @return Promise resolved with the header element.
|
||||
*/
|
||||
protected async searchHeader(retries: number = 0): Promise<HTMLElement> {
|
||||
let parentPage: HTMLElement = this.element;
|
||||
let parentPage: HTMLElement | null = this.element;
|
||||
|
||||
while (parentPage) {
|
||||
if (!parentPage.parentElement) {
|
||||
|
@ -207,7 +207,7 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
// Get the next parent page.
|
||||
parentPage = <HTMLElement> CoreDomUtils.closest(parentPage.parentElement, '.ion-page');
|
||||
parentPage = parentPage.parentElement.closest('.ion-page');
|
||||
if (parentPage) {
|
||||
// Check if the page has a header. If it doesn't, search the next parent page.
|
||||
const header = this.searchHeaderInPage(parentPage);
|
||||
|
|
|
@ -19,8 +19,8 @@ import { CoreUtils } from '@services/utils/utils';
|
|||
import { CoreMath } from '@singletons/math';
|
||||
import { CoreComponentsRegistry } from '@singletons/components-registry';
|
||||
import { CoreFormatTextDirective } from './format-text';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreEventLoadingChangedData, CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreEventObserver } from '@singletons/events';
|
||||
import { CoreLoadingComponent } from '@components/loading/loading';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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.finishRenderingAllElementsInside<CoreFormatTextDirective>(
|
||||
element,
|
||||
'core-format-text',
|
||||
'rendered',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -184,24 +184,33 @@ export class CoreCollapsibleFooterDirective implements OnInit, OnDestroy {
|
|||
* @inheritdoc
|
||||
*/
|
||||
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.
|
||||
this.appearOnBottom = !CoreUtils.isFalseOrZero(this.appearOnBottom);
|
||||
|
||||
// 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 format-text is inside the loading, re-calculate the height.
|
||||
await this.calculateHeight();
|
||||
setTimeout(() => this.calculateHeight(), 200);
|
||||
}
|
||||
});
|
||||
await this.waitLoadingsDone();
|
||||
|
||||
await this.calculateHeight();
|
||||
|
||||
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.
|
||||
|
||||
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,31 @@ 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.finishRenderingAllElementsInside<CoreLoadingComponent>(
|
||||
this.page,
|
||||
'core-loading',
|
||||
'whenLoaded',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until all <core-format-text> children inside the element are done rendering.
|
||||
*
|
||||
* @param element Element.
|
||||
* @return Promise resolved when texts are rendered.
|
||||
*/
|
||||
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.finishRenderingAllElementsInside<CoreFormatTextDirective>(
|
||||
element,
|
||||
'core-format-text',
|
||||
'rendered',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -399,6 +426,10 @@ export class CoreCollapsibleHeaderDirective implements OnInit, OnChanges, OnDest
|
|||
return;
|
||||
}
|
||||
|
||||
if (page.classList.contains('is-frozen')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const progress = parseFloat(page.style.getPropertyValue('--collapsible-header-progress'));
|
||||
const scrollTop = contentScroll.scrollTop;
|
||||
const collapse = progress > 0.5;
|
||||
|
|
|
@ -13,11 +13,11 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Directive, ElementRef, Input, OnInit } from '@angular/core';
|
||||
import { CoreLoadingComponent } from '@components/loading/loading';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreComponentsRegistry } from '@singletons/components-registry';
|
||||
import { CoreEventLoadingChangedData, CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreFormatTextDirective } from './format-text';
|
||||
|
||||
const defaultMaxHeight = 80;
|
||||
|
@ -47,7 +47,6 @@ export class CoreCollapsibleItemDirective implements OnInit {
|
|||
protected expanded = false;
|
||||
protected maxHeight = defaultMaxHeight;
|
||||
protected expandedHeight = 0;
|
||||
protected loadingChangedListener?: CoreEventObserver;
|
||||
|
||||
constructor(el: ElementRef<HTMLElement>) {
|
||||
this.element = el.nativeElement;
|
||||
|
@ -79,17 +78,22 @@ export class CoreCollapsibleItemDirective implements OnInit {
|
|||
|
||||
this.element.classList.add('collapsible-item');
|
||||
|
||||
// Calculate the height now.
|
||||
await this.calculateHeight();
|
||||
await this.waitLoadingsDone();
|
||||
|
||||
// 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));
|
||||
|
||||
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.
|
||||
*/
|
||||
protected async calculateHeight(retries = 3): Promise<void> {
|
||||
protected async calculateHeight(): Promise<void> {
|
||||
// Remove max-height (if any) to calculate the real height.
|
||||
this.element.classList.add('collapsible-loading-height');
|
||||
|
||||
await this.waitFormatTextsRendered(this.element);
|
||||
|
||||
await CoreUtils.nextTick();
|
||||
|
||||
this.expandedHeight = CoreDomUtils.getElementHeight(this.element) || 0;
|
||||
|
||||
// Restore the max height now.
|
||||
|
@ -129,10 +133,6 @@ export class CoreCollapsibleItemDirective implements OnInit {
|
|||
|
||||
// If cannot calculate height, shorten always.
|
||||
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 { CoreFilepool, CoreFilepoolFileActions, CoreFilepoolFileEventData } from '@services/filepool';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUrlUtils } from '@services/utils/url';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { Platform } from '@singletons';
|
||||
|
@ -418,13 +417,14 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O
|
|||
// Set events to download big files (not downloaded automatically).
|
||||
if (targetAttr !== 'poster' && (tagName === 'VIDEO' || tagName === 'AUDIO' || tagName === 'A' || tagName === 'SOURCE')) {
|
||||
const eventName = tagName == 'A' ? 'click' : 'play';
|
||||
let clickableEl = this.element;
|
||||
let clickableEl: Element | null = this.element;
|
||||
|
||||
if (tagName == 'SOURCE') {
|
||||
clickableEl = <HTMLElement> CoreDomUtils.closest(this.element, 'video,audio');
|
||||
if (!clickableEl) {
|
||||
return;
|
||||
}
|
||||
clickableEl = this.element.closest('video,audio');
|
||||
}
|
||||
|
||||
if (!clickableEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
clickableEl.addEventListener(eventName, () => {
|
||||
|
|
|
@ -458,7 +458,7 @@ export class CoreQuestionBaseComponent {
|
|||
name: input.name,
|
||||
value: input.value,
|
||||
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.
|
||||
|
|
|
@ -258,7 +258,7 @@ export class CoreQuestionHelperProvider {
|
|||
// Get the last element and check it's not in the question contents.
|
||||
let last = matches.pop();
|
||||
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.
|
||||
question[attrName] = last.innerHTML;
|
||||
last.parentElement?.removeChild(last);
|
||||
|
|
|
@ -53,6 +53,7 @@ import { NavigationStart } from '@angular/router';
|
|||
import { filter } from 'rxjs/operators';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { CoreComponentsRegistry } from '@singletons/components-registry';
|
||||
import { CorePromisedValue } from '@classes/promised-value';
|
||||
|
||||
/*
|
||||
* "Utils" service with helper functions for UI, DOM elements and HTML code.
|
||||
|
@ -91,6 +92,45 @@ export class CoreDomUtilsProvider {
|
|||
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
|
||||
* traverse the parents to achieve the same functionality.
|
||||
|
@ -99,45 +139,10 @@ export class CoreDomUtilsProvider {
|
|||
* @param element DOM Element.
|
||||
* @param selector Selector to search.
|
||||
* @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 {
|
||||
if (!element) {
|
||||
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;
|
||||
return element?.closest(selector) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,41 @@ export class CoreComponentsRegistry {
|
|||
: 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