From 027c3870fd3a6260775ee09b5074af278e7b501f Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 26 May 2021 17:17:06 +0200 Subject: [PATCH] MOBILE-3320 core: Support alerts on top of loading --- src/core/classes/ion-loading.ts | 142 ++++++++++++++++-- .../services/fileuploader-helper.ts | 5 +- src/core/services/utils/dom.ts | 42 +++--- 3 files changed, 153 insertions(+), 36 deletions(-) diff --git a/src/core/classes/ion-loading.ts b/src/core/classes/ion-loading.ts index 3ec3327f5..1e831e54c 100644 --- a/src/core/classes/ion-loading.ts +++ b/src/core/classes/ion-loading.ts @@ -13,43 +13,155 @@ // limitations under the License. import { CoreUtils } from '@services/utils/utils'; +import { LoadingController } from '@singletons'; + +/** + * Dismiss listener. + */ +export type CoreIonLoadingElementDismissListener = () => unknown; /** * Class to improve the behaviour of HTMLIonLoadingElement. - * It's not a subclass of HTMLIonLoadingElement because we cannot override the dismiss function. + * + * In addition to present/dismiss, this loader can also be paused/resumed in order to allow stacking + * modals in top of one another without interfering. Conceptually, a paused loader is still + * active but will not be shown in the UI. */ export class CoreIonLoadingElement { - protected isPresented = false; - protected isDismissed = false; + protected scheduled = false; + protected paused = false; + protected listeners: CoreIonLoadingElementDismissListener[] = []; + protected asyncLoadingElement?: Promise; - constructor(public loading: HTMLIonLoadingElement) { } + constructor(protected text?: string) { } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async dismiss(data?: any, role?: string): Promise { - if (!this.isPresented || this.isDismissed) { - this.isDismissed = true; + /** + * Dismiss the loading element. + * + * @param data Dismiss data. + * @param role Dismiss role. + */ + async dismiss(data?: unknown, role?: string): Promise { + if (this.paused) { + this.paused = false; + this.listeners.forEach(listener => listener()); - return true; + return; } - this.isDismissed = true; + if (!this.asyncLoadingElement) { + if (this.scheduled) { + this.scheduled = false; + this.listeners.forEach(listener => listener()); + } - return this.loading.dismiss(data, role); + return; + } + + const asyncLoadingElement = this.asyncLoadingElement; + delete this.asyncLoadingElement; + + const loadingElement = await asyncLoadingElement; + await loadingElement.dismiss(data, role); + + this.listeners.forEach(listener => listener()); } /** - * Present the loading. + * Register dismiss listener. + * + * @param listener Listener. + */ + onDismiss(listener: CoreIonLoadingElementDismissListener): void { + this.listeners.push(listener); + } + + /** + * Hide the loading element. + */ + async pause(): Promise { + if (!this.asyncLoadingElement) { + return; + } + + this.paused = true; + + const asyncLoadingElement = this.asyncLoadingElement; + delete this.asyncLoadingElement; + + const loadingElement = await asyncLoadingElement; + loadingElement.dismiss(); + } + + /** + * Present the loading element. */ async present(): Promise { + if (this.paused || this.scheduled || this.asyncLoadingElement) { + return; + } + // Wait a bit before presenting the modal, to prevent it being displayed if dismiss is called fast. + this.scheduled = true; + await CoreUtils.wait(40); - if (!this.isDismissed) { - this.isPresented = true; + if (!this.scheduled) { + return; + } - await this.loading.present(); + // Present modal. + this.scheduled = false; + + await this.presentLoadingElement(); + } + + /** + * Show loading element. + */ + async resume(): Promise { + if (!this.paused) { + return; + } + + this.paused = false; + + await this.presentLoadingElement(); + } + + /** + * Update text in the loading element. + * + * @param text Text. + */ + async updateText(text: string): Promise { + this.text = text; + + if (!this.asyncLoadingElement) { + return; + } + + const loadingElement = await this.asyncLoadingElement; + const contentElement = loadingElement.querySelector('.loading-content'); + + if (contentElement) { + contentElement.innerHTML = text; } } + /** + * Create and present the loading element. + */ + private async presentLoadingElement(): Promise { + let resolveLoadingElement!: ((loadingElement: HTMLIonLoadingElement) => void); + this.asyncLoadingElement = new Promise(resolve => resolveLoadingElement = resolve); + + const loadingElement = await LoadingController.create({ message: this.text }); + + await loadingElement.present(); + + resolveLoadingElement(loadingElement); + } + } diff --git a/src/core/features/fileuploader/services/fileuploader-helper.ts b/src/core/features/fileuploader/services/fileuploader-helper.ts index 6a159e0fc..33c58889f 100644 --- a/src/core/features/fileuploader/services/fileuploader-helper.ts +++ b/src/core/features/fileuploader/services/fileuploader-helper.ts @@ -873,10 +873,7 @@ export class CoreFileUploaderHelperProvider { return; } - const contentElement = modal.loading?.querySelector('.loading-content'); - if (contentElement) { - contentElement.innerHTML = Translate.instant(stringKey, { $a: perc.toFixed(1) }); - } + modal.updateText(Translate.instant(stringKey, { $a: perc.toFixed(1) })); } } diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 18419faca..993a700fc 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -30,15 +30,7 @@ import { CoreIonLoadingElement } from '@classes/ion-loading'; import { CoreCanceledError } from '@classes/errors/cancelederror'; import { CoreAnyError, CoreError } from '@classes/errors/error'; import { CoreSilentError } from '@classes/errors/silenterror'; -import { - makeSingleton, - Translate, - AlertController, - LoadingController, - ToastController, - PopoverController, - ModalController, -} from '@singletons'; +import { makeSingleton, Translate, AlertController, ToastController, PopoverController, ModalController } from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { CoreFileSizeSum } from '@services/plugin-file-delegate'; import { CoreNetworkError } from '@classes/errors/network-error'; @@ -66,6 +58,7 @@ export class CoreDomUtilsProvider { protected lastInstanceId = 0; protected debugDisplay = false; // Whether to display debug messages. Store it in a variable to make it synchronous. protected displayedAlerts: Record = {}; // To prevent duplicated alerts. + protected activeLoadingModals: CoreIonLoadingElement[] = []; protected logger: CoreLogger; constructor(protected domSanitizer: DomSanitizer) { @@ -1216,6 +1209,10 @@ export class CoreDomUtilsProvider { const alert = await AlertController.create(options); + if (Object.keys(this.displayedAlerts).length === 0) { + await Promise.all(this.activeLoadingModals.slice(0).reverse().map(modal => modal.pause())); + } + // eslint-disable-next-line promise/catch-or-return alert.present().then(() => { if (hasHTMLTags) { @@ -1231,9 +1228,14 @@ export class CoreDomUtilsProvider { this.displayedAlerts[alertId] = alert; // Set the callbacks to trigger an observable event. - // eslint-disable-next-line promise/catch-or-return, promise/always-return - alert.onDidDismiss().then(() => { + // eslint-disable-next-line promise/catch-or-return + alert.onDidDismiss().then(async () => { delete this.displayedAlerts[alertId]; + + // eslint-disable-next-line promise/always-return + if (Object.keys(this.displayedAlerts).length === 0) { + await Promise.all(this.activeLoadingModals.map(modal => modal.resume())); + } }); if (autocloseTime && autocloseTime > 0) { @@ -1447,11 +1449,17 @@ export class CoreDomUtilsProvider { text = Translate.instant(text); } - const loadingElement = await LoadingController.create({ - message: text, + const loading = new CoreIonLoadingElement(text); + + loading.onDismiss(() => { + const index = this.activeLoadingModals.indexOf(loading); + + if (index !== -1) { + this.activeLoadingModals.splice(index, 1); + } }); - const loading = new CoreIonLoadingElement(loadingElement); + this.activeLoadingModals.push(loading); await loading.present(); @@ -1480,19 +1488,19 @@ export class CoreDomUtilsProvider { }); } - const alert = await AlertController.create({ + const alert = await this.showAlertWithOptions({ message: message, buttons: buttons, }); - await alert.present(); - const isDevice = CoreApp.isAndroid() || CoreApp.isIOS(); if (!isDevice) { // Treat all anchors so they don't override the app. const alertMessageEl: HTMLElement | null = alert.querySelector('.alert-message'); alertMessageEl && this.treatAnchors(alertMessageEl); } + + await alert.onDidDismiss(); } /**