forked from EVOgeek/Vmeda.Online
		
	Merge pull request #2795 from NoelDeMartin/MOBILE-3320
MOBILE-3320: Support alerts on top of loading
This commit is contained in:
		
						commit
						ad9bcc1810
					
				
							
								
								
									
										2
									
								
								.vscode/moodle.code-snippets
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.vscode/moodle.code-snippets
									
									
									
									
										vendored
									
									
								
							| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user