MOBILE-3320 core: Support alerts on top of loading
parent
70f1bd6063
commit
027c3870fd
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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) }));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue