MOBILE-3320 core: Support alerts on top of loading

main
Noel De Martin 2021-05-26 17:17:06 +02:00
parent 70f1bd6063
commit 027c3870fd
3 changed files with 153 additions and 36 deletions

View File

@ -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<HTMLIonLoadingElement>;
constructor(public loading: HTMLIonLoadingElement) { }
constructor(protected text?: string) { }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async dismiss(data?: any, role?: string): Promise<boolean> {
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<void> {
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<void> {
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<void> {
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<void> {
if (!this.paused) {
return;
}
this.paused = false;
await this.presentLoadingElement();
}
/**
* Update text in the loading element.
*
* @param text Text.
*/
async updateText(text: string): Promise<void> {
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<void> {
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);
}
}

View File

@ -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) }));
}
}

View File

@ -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<string, HTMLIonAlertElement> = {}; // 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();
}
/**