Merge pull request #2795 from NoelDeMartin/MOBILE-3320
MOBILE-3320: Support alerts on top of loadingmain
commit
ad9bcc1810
|
@ -89,7 +89,7 @@
|
||||||
"",
|
"",
|
||||||
"}",
|
"}",
|
||||||
"",
|
"",
|
||||||
"export class ${1:${TM_FILENAME_BASE}} extends makeSingleton(${1:${TM_FILENAME_BASE}}Service) {}",
|
"export const ${1:${TM_FILENAME_BASE}} = makeSingleton(${1:${TM_FILENAME_BASE}}Service);",
|
||||||
""
|
""
|
||||||
],
|
],
|
||||||
"description": "[Moodle] Create a Service Singleton"
|
"description": "[Moodle] Create a Service Singleton"
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue