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. // limitations under the License.
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { LoadingController } from '@singletons';
/**
* Dismiss listener.
*/
export type CoreIonLoadingElementDismissListener = () => unknown;
/** /**
* Class to improve the behaviour of HTMLIonLoadingElement. * 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 { export class CoreIonLoadingElement {
protected isPresented = false; protected scheduled = false;
protected isDismissed = 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> { * Dismiss the loading element.
if (!this.isPresented || this.isDismissed) { *
this.isDismissed = true; * @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> { 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. // Wait a bit before presenting the modal, to prevent it being displayed if dismiss is called fast.
this.scheduled = true;
await CoreUtils.wait(40); await CoreUtils.wait(40);
if (!this.isDismissed) { if (!this.scheduled) {
this.isPresented = true; 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; return;
} }
const contentElement = modal.loading?.querySelector('.loading-content'); modal.updateText(Translate.instant(stringKey, { $a: perc.toFixed(1) }));
if (contentElement) {
contentElement.innerHTML = 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 { CoreCanceledError } from '@classes/errors/cancelederror';
import { CoreAnyError, CoreError } from '@classes/errors/error'; import { CoreAnyError, CoreError } from '@classes/errors/error';
import { CoreSilentError } from '@classes/errors/silenterror'; import { CoreSilentError } from '@classes/errors/silenterror';
import { import { makeSingleton, Translate, AlertController, ToastController, PopoverController, ModalController } from '@singletons';
makeSingleton,
Translate,
AlertController,
LoadingController,
ToastController,
PopoverController,
ModalController,
} from '@singletons';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
import { CoreFileSizeSum } from '@services/plugin-file-delegate'; import { CoreFileSizeSum } from '@services/plugin-file-delegate';
import { CoreNetworkError } from '@classes/errors/network-error'; import { CoreNetworkError } from '@classes/errors/network-error';
@ -66,6 +58,7 @@ export class CoreDomUtilsProvider {
protected lastInstanceId = 0; protected lastInstanceId = 0;
protected debugDisplay = false; // Whether to display debug messages. Store it in a variable to make it synchronous. 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 displayedAlerts: Record<string, HTMLIonAlertElement> = {}; // To prevent duplicated alerts.
protected activeLoadingModals: CoreIonLoadingElement[] = [];
protected logger: CoreLogger; protected logger: CoreLogger;
constructor(protected domSanitizer: DomSanitizer) { constructor(protected domSanitizer: DomSanitizer) {
@ -1216,6 +1209,10 @@ export class CoreDomUtilsProvider {
const alert = await AlertController.create(options); 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 // eslint-disable-next-line promise/catch-or-return
alert.present().then(() => { alert.present().then(() => {
if (hasHTMLTags) { if (hasHTMLTags) {
@ -1231,9 +1228,14 @@ export class CoreDomUtilsProvider {
this.displayedAlerts[alertId] = alert; this.displayedAlerts[alertId] = alert;
// Set the callbacks to trigger an observable event. // Set the callbacks to trigger an observable event.
// eslint-disable-next-line promise/catch-or-return, promise/always-return // eslint-disable-next-line promise/catch-or-return
alert.onDidDismiss().then(() => { alert.onDidDismiss().then(async () => {
delete this.displayedAlerts[alertId]; 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) { if (autocloseTime && autocloseTime > 0) {
@ -1447,11 +1449,17 @@ export class CoreDomUtilsProvider {
text = Translate.instant(text); text = Translate.instant(text);
} }
const loadingElement = await LoadingController.create({ const loading = new CoreIonLoadingElement(text);
message: 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(); await loading.present();
@ -1480,19 +1488,19 @@ export class CoreDomUtilsProvider {
}); });
} }
const alert = await AlertController.create({ const alert = await this.showAlertWithOptions({
message: message, message: message,
buttons: buttons, buttons: buttons,
}); });
await alert.present();
const isDevice = CoreApp.isAndroid() || CoreApp.isIOS(); const isDevice = CoreApp.isAndroid() || CoreApp.isIOS();
if (!isDevice) { if (!isDevice) {
// Treat all anchors so they don't override the app. // Treat all anchors so they don't override the app.
const alertMessageEl: HTMLElement | null = alert.querySelector('.alert-message'); const alertMessageEl: HTMLElement | null = alert.querySelector('.alert-message');
alertMessageEl && this.treatAnchors(alertMessageEl); alertMessageEl && this.treatAnchors(alertMessageEl);
} }
await alert.onDidDismiss();
} }
/** /**