Merge pull request #2991 from NoelDeMartin/MOBILE-3905
MOBILE-3905: Swipe navigation
This commit is contained in:
		
						commit
						da6e3e8a20
					
				
							
								
								
									
										14
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -88,6 +88,7 @@ | |||||||
|         "cordova.plugins.diagnostic": "^5.0.2", |         "cordova.plugins.diagnostic": "^5.0.2", | ||||||
|         "core-js": "^3.9.1", |         "core-js": "^3.9.1", | ||||||
|         "es6-promise-plugin": "^4.2.2", |         "es6-promise-plugin": "^4.2.2", | ||||||
|  |         "hammerjs": "^2.0.8", | ||||||
|         "jszip": "^3.5.0", |         "jszip": "^3.5.0", | ||||||
|         "mathjax": "2.7.7", |         "mathjax": "2.7.7", | ||||||
|         "moment": "^2.29.0", |         "moment": "^2.29.0", | ||||||
| @ -15988,6 +15989,14 @@ | |||||||
|         "node": ">= 0.10" |         "node": ">= 0.10" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/hammerjs": { | ||||||
|  |       "version": "2.0.8", | ||||||
|  |       "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", | ||||||
|  |       "integrity": "sha1-BO93hiz/K7edMPdpIJWTAiK/YPE=", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=0.8.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/handle-thing": { |     "node_modules/handle-thing": { | ||||||
|       "version": "2.0.1", |       "version": "2.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", |       "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", | ||||||
| @ -43687,6 +43696,11 @@ | |||||||
|         "glogg": "^1.0.0" |         "glogg": "^1.0.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "hammerjs": { | ||||||
|  |       "version": "2.0.8", | ||||||
|  |       "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", | ||||||
|  |       "integrity": "sha1-BO93hiz/K7edMPdpIJWTAiK/YPE=" | ||||||
|  |     }, | ||||||
|     "handle-thing": { |     "handle-thing": { | ||||||
|       "version": "2.0.1", |       "version": "2.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", |       "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", | ||||||
|  | |||||||
| @ -117,6 +117,7 @@ | |||||||
|     "cordova.plugins.diagnostic": "^5.0.2", |     "cordova.plugins.diagnostic": "^5.0.2", | ||||||
|     "core-js": "^3.9.1", |     "core-js": "^3.9.1", | ||||||
|     "es6-promise-plugin": "^4.2.2", |     "es6-promise-plugin": "^4.2.2", | ||||||
|  |     "hammerjs": "^2.0.8", | ||||||
|     "jszip": "^3.5.0", |     "jszip": "^3.5.0", | ||||||
|     "mathjax": "2.7.7", |     "mathjax": "2.7.7", | ||||||
|     "moment": "^2.29.0", |     "moment": "^2.29.0", | ||||||
| @ -245,4 +246,4 @@ | |||||||
|   "optionalDependencies": { |   "optionalDependencies": { | ||||||
|     "keytar": "^7.2.0" |     "keytar": "^7.2.0" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										27
									
								
								src/core/classes/hammer-gesture-config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/core/classes/hammer-gesture-config.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { HammerGestureConfig } from '@angular/platform-browser'; | ||||||
|  | import { DIRECTION_ALL } from 'hammerjs'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Application HammerJS config. | ||||||
|  |  */ | ||||||
|  | export class CoreHammerGestureConfig extends HammerGestureConfig { | ||||||
|  | 
 | ||||||
|  |     overrides = { | ||||||
|  |         swipe: { direction: DIRECTION_ALL }, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										169
									
								
								src/core/classes/items-management/items-manager-source.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								src/core/classes/items-management/items-manager-source.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,169 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Updates listener. | ||||||
|  |  */ | ||||||
|  | export interface CoreItemsListSourceListener<Item> { | ||||||
|  |     onItemsUpdated(items: Item[], hasMoreItems: boolean): void; | ||||||
|  |     onReset(): void; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Items collection source data. | ||||||
|  |  */ | ||||||
|  | export abstract class CoreItemsManagerSource<Item = unknown> { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get a string to identify instances constructed with the given arguments as being reusable. | ||||||
|  |      * | ||||||
|  |      * @param args Constructor arguments. | ||||||
|  |      * @returns Id. | ||||||
|  |      */ | ||||||
|  |     static getSourceId(...args: unknown[]): string { | ||||||
|  |         return args.map(argument => String(argument)).join('-'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private items: Item[] | null = null; | ||||||
|  |     private hasMoreItems = true; | ||||||
|  |     private listeners: CoreItemsListSourceListener<Item>[] = []; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Check whether any page has been loaded. | ||||||
|  |      * | ||||||
|  |      * @returns Whether any page has been loaded. | ||||||
|  |      */ | ||||||
|  |     isLoaded(): boolean { | ||||||
|  |         return this.items !== null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Check whether there are more pages to be loaded. | ||||||
|  |      * | ||||||
|  |      * @return Whether there are more pages to be loaded. | ||||||
|  |      */ | ||||||
|  |     isCompleted(): boolean { | ||||||
|  |         return !this.hasMoreItems; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get collection items. | ||||||
|  |      * | ||||||
|  |      * @returns Items. | ||||||
|  |      */ | ||||||
|  |     getItems(): Item[] | null { | ||||||
|  |         return this.items; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the count of pages that have been loaded. | ||||||
|  |      * | ||||||
|  |      * @returns Pages loaded. | ||||||
|  |      */ | ||||||
|  |     getPagesLoaded(): number { | ||||||
|  |         if (this.items === null) { | ||||||
|  |             return 0; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return Math.ceil(this.items.length / this.getPageLength()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Reset collection data. | ||||||
|  |      */ | ||||||
|  |     reset(): void { | ||||||
|  |         this.items = null; | ||||||
|  |         this.hasMoreItems = true; | ||||||
|  | 
 | ||||||
|  |         this.listeners.forEach(listener => listener.onReset()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Register a listener. | ||||||
|  |      * | ||||||
|  |      * @param listener Listener. | ||||||
|  |      * @returns Unsubscribe function. | ||||||
|  |      */ | ||||||
|  |     addListener(listener: CoreItemsListSourceListener<Item>): () => void { | ||||||
|  |         this.listeners.push(listener); | ||||||
|  | 
 | ||||||
|  |         return () => this.removeListener(listener); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Remove a listener. | ||||||
|  |      * | ||||||
|  |      * @param listener Listener. | ||||||
|  |      */ | ||||||
|  |     removeListener(listener: CoreItemsListSourceListener<Item>): void { | ||||||
|  |         const index = this.listeners.indexOf(listener); | ||||||
|  | 
 | ||||||
|  |         if (index === -1) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.listeners.splice(index, 1); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Reload the collection, this resets the data to the first page. | ||||||
|  |      */ | ||||||
|  |     async reload(): Promise<void> { | ||||||
|  |         const { items, hasMoreItems } = await this.loadPageItems(0); | ||||||
|  | 
 | ||||||
|  |         this.setItems(items, hasMoreItems); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Load items for the next page, if any. | ||||||
|  |      */ | ||||||
|  |     async loadNextPage(): Promise<void> { | ||||||
|  |         if (!this.hasMoreItems) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const { items, hasMoreItems } = await this.loadPageItems(this.getPagesLoaded()); | ||||||
|  | 
 | ||||||
|  |         this.setItems((this.items ?? []).concat(items), hasMoreItems); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Load page items. | ||||||
|  |      * | ||||||
|  |      * @param page Page number (starting at 0). | ||||||
|  |      * @return Page items data. | ||||||
|  |      */ | ||||||
|  |     protected abstract loadPageItems(page: number): Promise<{ items: Item[]; hasMoreItems: boolean }>; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the length of each page in the collection. | ||||||
|  |      * | ||||||
|  |      * @return Page length. | ||||||
|  |      */ | ||||||
|  |     protected abstract getPageLength(): number; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Update the collection items. | ||||||
|  |      * | ||||||
|  |      * @param items Items. | ||||||
|  |      * @param hasMoreItems Whether there are more pages to be loaded. | ||||||
|  |      */ | ||||||
|  |     protected setItems(items: Item[], hasMoreItems: boolean): void { | ||||||
|  |         this.items = items; | ||||||
|  |         this.hasMoreItems = hasMoreItems; | ||||||
|  | 
 | ||||||
|  |         this.listeners.forEach(listener => listener.onItemsUpdated(items, hasMoreItems)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,141 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { CoreItemsManagerSource } from './items-manager-source'; | ||||||
|  | 
 | ||||||
|  | type SourceConstructor<T extends CoreItemsManagerSource = CoreItemsManagerSource> = { | ||||||
|  |     getSourceId(...args: unknown[]): string; | ||||||
|  |     new (...args: unknown[]): T; | ||||||
|  | }; | ||||||
|  | type InstanceTracking = { instance: CoreItemsManagerSource; references: unknown[] }; | ||||||
|  | type Instances = Record<string, InstanceTracking>; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Tracks CoreItemsManagerSource instances to reuse between pages. | ||||||
|  |  */ | ||||||
|  | export class CoreItemsManagerSourcesTracker { | ||||||
|  | 
 | ||||||
|  |     private static instances: WeakMap<SourceConstructor, Instances> = new WeakMap(); | ||||||
|  |     private static instanceIds: WeakMap<CoreItemsManagerSource, string> = new WeakMap(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Create an instance of the given source or retrieve one if it's already in use. | ||||||
|  |      * | ||||||
|  |      * @param constructor Source constructor. | ||||||
|  |      * @param constructorArguments Arguments to create a new instance, used to find out if an instance already exists. | ||||||
|  |      * @returns Source. | ||||||
|  |      */ | ||||||
|  |     static getOrCreateSource<T extends CoreItemsManagerSource>( | ||||||
|  |         constructor: SourceConstructor<T>, | ||||||
|  |         constructorArguments: ConstructorParameters<SourceConstructor<T>>, | ||||||
|  |     ): T  { | ||||||
|  |         const id = constructor.getSourceId(...constructorArguments); | ||||||
|  |         const constructorInstances = this.getConstructorInstances(constructor); | ||||||
|  | 
 | ||||||
|  |         return constructorInstances[id]?.instance as T | ||||||
|  |             ?? this.createInstance(id, constructor, constructorArguments); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Track an object referencing a source. | ||||||
|  |      * | ||||||
|  |      * @param source Source. | ||||||
|  |      * @param reference Object referncing this source. | ||||||
|  |      */ | ||||||
|  |     static addReference(source: CoreItemsManagerSource, reference: unknown): void { | ||||||
|  |         const constructorInstances = this.getConstructorInstances(source.constructor as SourceConstructor); | ||||||
|  |         const instanceId = this.instanceIds.get(source); | ||||||
|  | 
 | ||||||
|  |         if (!instanceId) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!(instanceId in constructorInstances)) { | ||||||
|  |             constructorInstances[instanceId] = { | ||||||
|  |                 instance: source, | ||||||
|  |                 references: [], | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         constructorInstances[instanceId].references.push(reference); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Remove a reference to an existing source, freeing it from memory if it's not referenced elsewhere. | ||||||
|  |      * | ||||||
|  |      * @param source Source. | ||||||
|  |      * @param reference Object that was referncing this source. | ||||||
|  |      */ | ||||||
|  |     static removeReference(source: CoreItemsManagerSource, reference: unknown): void { | ||||||
|  |         const constructorInstances = this.instances.get(source.constructor as SourceConstructor); | ||||||
|  |         const instanceId = this.instanceIds.get(source); | ||||||
|  |         const index = constructorInstances?.[instanceId ?? '']?.references.indexOf(reference) ?? -1; | ||||||
|  | 
 | ||||||
|  |         if (!constructorInstances || !instanceId || index === -1) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         constructorInstances[instanceId].references.splice(index, 1); | ||||||
|  | 
 | ||||||
|  |         if (constructorInstances[instanceId].references.length === 0) { | ||||||
|  |             delete constructorInstances[instanceId]; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get instances for a given constructor. | ||||||
|  |      * | ||||||
|  |      * @param constructor Source constructor. | ||||||
|  |      * @returns Constructor instances. | ||||||
|  |      */ | ||||||
|  |     private static getConstructorInstances(constructor: SourceConstructor): Instances { | ||||||
|  |         return this.instances.get(constructor) | ||||||
|  |             ?? this.initialiseConstructorInstances(constructor); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Initialise instances for a given constructor. | ||||||
|  |      * | ||||||
|  |      * @param constructor Source constructor. | ||||||
|  |      * @returns Constructor instances. | ||||||
|  |      */ | ||||||
|  |     private static initialiseConstructorInstances(constructor: SourceConstructor): Instances { | ||||||
|  |         const constructorInstances = {}; | ||||||
|  | 
 | ||||||
|  |         this.instances.set(constructor, constructorInstances); | ||||||
|  | 
 | ||||||
|  |         return constructorInstances; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Create a new source instance. | ||||||
|  |      * | ||||||
|  |      * @param id Source id. | ||||||
|  |      * @param constructor Source constructor. | ||||||
|  |      * @param constructorArguments Source constructor arguments. | ||||||
|  |      * @returns Source instance. | ||||||
|  |      */ | ||||||
|  |     private static createInstance<T extends CoreItemsManagerSource>( | ||||||
|  |         id: string, | ||||||
|  |         constructor: SourceConstructor<T>, | ||||||
|  |         constructorArguments: ConstructorParameters<SourceConstructor<T>>, | ||||||
|  |     ): T { | ||||||
|  |         const instance = new constructor(...constructorArguments); | ||||||
|  | 
 | ||||||
|  |         this.instanceIds.set(instance, id); | ||||||
|  | 
 | ||||||
|  |         return instance; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										192
									
								
								src/core/classes/items-management/items-manager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								src/core/classes/items-management/items-manager.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,192 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { ActivatedRoute, ActivatedRouteSnapshot, Params } from '@angular/router'; | ||||||
|  | import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; | ||||||
|  | 
 | ||||||
|  | import { CoreItemsManagerSource } from './items-manager-source'; | ||||||
|  | import { CoreItemsManagerSourcesTracker } from './items-manager-sources-tracker'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Helper to manage a collection of items in a page. | ||||||
|  |  */ | ||||||
|  | export abstract class CoreItemsManager<Item = unknown> { | ||||||
|  | 
 | ||||||
|  |     protected source?: { instance: CoreItemsManagerSource<Item>; unsubscribe: () => void }; | ||||||
|  |     protected itemsMap: Record<string, Item> | null = null; | ||||||
|  |     protected selectedItem: Item | null = null; | ||||||
|  | 
 | ||||||
|  |     constructor(source: CoreItemsManagerSource<Item>) { | ||||||
|  |         this.setSource(source); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get source. | ||||||
|  |      * | ||||||
|  |      * @returns Source. | ||||||
|  |      */ | ||||||
|  |     getSource(): CoreItemsManagerSource<Item> { | ||||||
|  |         if (!this.source) { | ||||||
|  |             throw new Error('Source is missing from items manager'); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return this.source.instance; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Set source. | ||||||
|  |      * | ||||||
|  |      * @param newSource New source. | ||||||
|  |      */ | ||||||
|  |     setSource(newSource: CoreItemsManagerSource<Item> | null): void { | ||||||
|  |         if (this.source) { | ||||||
|  |             CoreItemsManagerSourcesTracker.removeReference(this.source.instance, this); | ||||||
|  | 
 | ||||||
|  |             this.source.unsubscribe(); | ||||||
|  |             delete this.source; | ||||||
|  | 
 | ||||||
|  |             this.onSourceReset(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (newSource) { | ||||||
|  |             CoreItemsManagerSourcesTracker.addReference(newSource, this); | ||||||
|  | 
 | ||||||
|  |             this.source = { | ||||||
|  |                 instance: newSource, | ||||||
|  |                 unsubscribe: newSource.addListener({ | ||||||
|  |                     onItemsUpdated: items => this.onSourceItemsUpdated(items), | ||||||
|  |                     onReset: () => this.onSourceReset(), | ||||||
|  |                 }), | ||||||
|  |             }; | ||||||
|  | 
 | ||||||
|  |             const items = newSource.getItems(); | ||||||
|  | 
 | ||||||
|  |             if (items) { | ||||||
|  |                 this.onSourceItemsUpdated(items); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Process page destroyed operations. | ||||||
|  |      */ | ||||||
|  |     destroy(): void { | ||||||
|  |         this.setSource(null); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get page route. | ||||||
|  |      * | ||||||
|  |      * @returns Current page route, if any. | ||||||
|  |      */ | ||||||
|  |     protected abstract getCurrentPageRoute(): ActivatedRoute | null; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the path to use when navigating to an item page. | ||||||
|  |      * | ||||||
|  |      * @param item Item. | ||||||
|  |      * @return Path to use when navigating to the item page. | ||||||
|  |      */ | ||||||
|  |     protected abstract getItemPath(item: Item): string; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the path of the selected item given the current route. | ||||||
|  |      * | ||||||
|  |      * @param route Page route. | ||||||
|  |      * @return Path of the selected item in the given route. | ||||||
|  |      */ | ||||||
|  |     protected abstract getSelectedItemPath(route?: ActivatedRouteSnapshot | null): string | null; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the query parameters to use when navigating to an item page. | ||||||
|  |      * | ||||||
|  |      * @param item Item. | ||||||
|  |      * @return Query parameters to use when navigating to the item page. | ||||||
|  |      */ | ||||||
|  |     // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||||
|  |     protected getItemQueryParams(item: Item): Params { | ||||||
|  |         return {}; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Update the selected item given the current route. | ||||||
|  |      * | ||||||
|  |      * @param route Current route. | ||||||
|  |      */ | ||||||
|  |     protected updateSelectedItem(route: ActivatedRouteSnapshot | null = null): void { | ||||||
|  |         route = route ?? this.getCurrentPageRoute()?.snapshot ?? null; | ||||||
|  | 
 | ||||||
|  |         const selectedItemPath = this.getSelectedItemPath(route); | ||||||
|  | 
 | ||||||
|  |         this.selectedItem = selectedItemPath | ||||||
|  |             ? this.itemsMap?.[selectedItemPath] ?? null | ||||||
|  |             : null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Navigate to an item in the collection. | ||||||
|  |      * | ||||||
|  |      * @param item Item. | ||||||
|  |      * @param options Navigation options. | ||||||
|  |      */ | ||||||
|  |     protected async navigateToItem( | ||||||
|  |         item: Item, | ||||||
|  |         options: Pick<CoreNavigationOptions, 'reset' | 'replace' | 'animationDirection'> = {}, | ||||||
|  |     ): Promise<void> { | ||||||
|  |         // Get current route in the page.
 | ||||||
|  |         const route = this.getCurrentPageRoute(); | ||||||
|  | 
 | ||||||
|  |         if (route === null) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // If this item is already selected, do nothing.
 | ||||||
|  |         const itemPath = this.getItemPath(item); | ||||||
|  |         const selectedItemPath = this.getSelectedItemPath(route.snapshot); | ||||||
|  | 
 | ||||||
|  |         if (selectedItemPath === itemPath) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Navigate to item.
 | ||||||
|  |         const params = this.getItemQueryParams(item); | ||||||
|  |         const pathPrefix = selectedItemPath ? selectedItemPath.split('/').fill('../').join('') : ''; | ||||||
|  | 
 | ||||||
|  |         await CoreNavigator.navigate(pathPrefix + itemPath, { params, ...options }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Called when source items have been updated. | ||||||
|  |      * | ||||||
|  |      * @param items New items. | ||||||
|  |      */ | ||||||
|  |     protected onSourceItemsUpdated(items: Item[]): void { | ||||||
|  |         this.itemsMap = items.reduce((map, item) => { | ||||||
|  |             map[this.getItemPath(item)] = item; | ||||||
|  | 
 | ||||||
|  |             return map; | ||||||
|  |         }, {}); | ||||||
|  | 
 | ||||||
|  |         this.updateSelectedItem(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Called when source has been updated. | ||||||
|  |      */ | ||||||
|  |     protected onSourceReset(): void { | ||||||
|  |         this.itemsMap = null; | ||||||
|  |         this.selectedItem = null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										237
									
								
								src/core/classes/items-management/list-items-manager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								src/core/classes/items-management/list-items-manager.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,237 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { ActivatedRoute, ActivatedRouteSnapshot, UrlSegment } from '@angular/router'; | ||||||
|  | import { Subscription } from 'rxjs'; | ||||||
|  | 
 | ||||||
|  | import { CoreSplitViewComponent } from '@components/split-view/split-view'; | ||||||
|  | import { CoreNavigator } from '@services/navigator'; | ||||||
|  | import { CoreScreen } from '@services/screen'; | ||||||
|  | import { CoreUtils } from '@services/utils/utils'; | ||||||
|  | 
 | ||||||
|  | import { CoreItemsManager } from './items-manager'; | ||||||
|  | import { CoreItemsManagerSource } from './items-manager-source'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Helper class to manage the state and routing of a list of items in a page. | ||||||
|  |  */ | ||||||
|  | export abstract class CoreListItemsManager<Item = unknown> extends CoreItemsManager<Item> { | ||||||
|  | 
 | ||||||
|  |     protected pageRouteLocator?: unknown | ActivatedRoute; | ||||||
|  |     protected splitView?: CoreSplitViewComponent; | ||||||
|  |     protected splitViewOutletSubscription?: Subscription; | ||||||
|  | 
 | ||||||
|  |     constructor(source: CoreItemsManagerSource<Item>, pageRouteLocator: unknown | ActivatedRoute) { | ||||||
|  |         super(source); | ||||||
|  | 
 | ||||||
|  |         this.pageRouteLocator = pageRouteLocator; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     get items(): Item[] { | ||||||
|  |         return this.getSource().getItems() || []; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     get loaded(): boolean { | ||||||
|  |         return this.itemsMap !== null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     get completed(): boolean { | ||||||
|  |         return this.getSource().isCompleted(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     get empty(): boolean { | ||||||
|  |         const items = this.getSource().getItems(); | ||||||
|  | 
 | ||||||
|  |         return items === null || items.length === 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Process page started operations. | ||||||
|  |      * | ||||||
|  |      * @param splitView Split view component. | ||||||
|  |      */ | ||||||
|  |     async start(splitView: CoreSplitViewComponent): Promise<void> { | ||||||
|  |         this.watchSplitViewOutlet(splitView); | ||||||
|  | 
 | ||||||
|  |         // Calculate current selected item.
 | ||||||
|  |         this.updateSelectedItem(); | ||||||
|  | 
 | ||||||
|  |         // Select default item if none is selected on a non-mobile layout.
 | ||||||
|  |         if (!CoreScreen.isMobile && this.selectedItem === null && !splitView.isNested) { | ||||||
|  |             const defaultItem = this.getDefaultItem(); | ||||||
|  | 
 | ||||||
|  |             if (defaultItem) { | ||||||
|  |                 this.select(defaultItem); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Log activity.
 | ||||||
|  |         await CoreUtils.ignoreErrors(this.logActivity()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Process page destroyed operations. | ||||||
|  |      */ | ||||||
|  |     destroy(): void { | ||||||
|  |         super.destroy(); | ||||||
|  |         this.splitViewOutletSubscription?.unsubscribe(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Watch a split view outlet to keep track of the selected item. | ||||||
|  |      * | ||||||
|  |      * @param splitView Split view component. | ||||||
|  |      */ | ||||||
|  |     watchSplitViewOutlet(splitView: CoreSplitViewComponent): void { | ||||||
|  |         this.splitView = splitView; | ||||||
|  |         this.splitViewOutletSubscription = splitView.outletRouteObservable.subscribe( | ||||||
|  |             route => this.updateSelectedItem(this.getPageRouteFromSplitViewOutlet(route)), | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         this.updateSelectedItem(this.getPageRouteFromSplitViewOutlet(splitView.outletRoute) ?? null); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Check whether the given item is selected or not. | ||||||
|  |      * | ||||||
|  |      * @param item Item. | ||||||
|  |      * @return Whether the given item is selected. | ||||||
|  |      */ | ||||||
|  |     isSelected(item: Item): boolean { | ||||||
|  |         return this.selectedItem === item; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Return the current aria value. | ||||||
|  |      * | ||||||
|  |      * @param item Item. | ||||||
|  |      * @return Will return the current value of the item if selected, false otherwise. | ||||||
|  |      */ | ||||||
|  |     getItemAriaCurrent(item: Item): string { | ||||||
|  |         return this.isSelected(item) ? 'page' : 'false'; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Select an item. | ||||||
|  |      * | ||||||
|  |      * @param item Item. | ||||||
|  |      */ | ||||||
|  |     async select(item: Item): Promise<void> { | ||||||
|  |         await this.navigateToItem(item, { reset: this.resetNavigation() }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Reset the list of items. | ||||||
|  |      */ | ||||||
|  |     reset(): void { | ||||||
|  |         this.getSource().reset(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Reload the list of items. | ||||||
|  |      */ | ||||||
|  |     async reload(): Promise<void> { | ||||||
|  |         await this.getSource().reload(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Load items for the next page, if any. | ||||||
|  |      */ | ||||||
|  |     async loadNextPage(): Promise<void> { | ||||||
|  |         await this.getSource().loadNextPage(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Log activity when the page starts. | ||||||
|  |      */ | ||||||
|  |     protected async logActivity(): Promise<void> { | ||||||
|  |         // Override to log activity.
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Check whether to reset navigation when selecting an item. | ||||||
|  |      * | ||||||
|  |      * @returns boolean Whether navigation should be reset. | ||||||
|  |      */ | ||||||
|  |     protected resetNavigation(): boolean { | ||||||
|  |         if (!CoreScreen.isTablet) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return !!this.splitView && !this.splitView?.isNested; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the item that should be selected by default. | ||||||
|  |      */ | ||||||
|  |     protected getDefaultItem(): Item | null { | ||||||
|  |         return this.items[0] || null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @inheritdoc | ||||||
|  |      */ | ||||||
|  |     protected getCurrentPageRoute(): ActivatedRoute | null { | ||||||
|  |         if (this.pageRouteLocator instanceof ActivatedRoute) { | ||||||
|  |             return CoreNavigator.isRouteActive(this.pageRouteLocator) ? this.pageRouteLocator : null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return CoreNavigator.getCurrentRoute({ pageComponent: this.pageRouteLocator }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @inheritdoc | ||||||
|  |      */ | ||||||
|  |     protected getSelectedItemPath(route?: ActivatedRouteSnapshot | null): string | null { | ||||||
|  |         const segments: UrlSegment[] = []; | ||||||
|  | 
 | ||||||
|  |         while ((route = route?.firstChild)) { | ||||||
|  |             segments.push(...route.url); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return segments.map(segment => segment.path).join('/').replace(/\/+/, '/').trim() || null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the page route given a child route on the splitview outlet. | ||||||
|  |      * | ||||||
|  |      * @param route Child route. | ||||||
|  |      * @return Page route. | ||||||
|  |      */ | ||||||
|  |     private getPageRouteFromSplitViewOutlet(route: ActivatedRouteSnapshot | null): ActivatedRouteSnapshot | null { | ||||||
|  |         const isPageRoute = this.buildRouteMatcher(); | ||||||
|  | 
 | ||||||
|  |         while (route && !isPageRoute(route)) { | ||||||
|  |             route = route.parent; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return route; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Build a function to check whether the given snapshot belongs to the page. | ||||||
|  |      * | ||||||
|  |      * @returns Route matcher. | ||||||
|  |      */ | ||||||
|  |     private buildRouteMatcher(): (route: ActivatedRouteSnapshot) => boolean { | ||||||
|  |         if (this.pageRouteLocator instanceof ActivatedRoute) { | ||||||
|  |             const pageRoutePath = CoreNavigator.getRouteFullPath(this.pageRouteLocator.snapshot); | ||||||
|  | 
 | ||||||
|  |             return route => CoreNavigator.getRouteFullPath(route) === pageRoutePath; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return route => route.component === this.pageRouteLocator; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										97
									
								
								src/core/classes/items-management/swipe-items-manager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								src/core/classes/items-management/swipe-items-manager.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,97 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { ActivatedRoute } from '@angular/router'; | ||||||
|  | 
 | ||||||
|  | import { CoreNavigator } from '@services/navigator'; | ||||||
|  | 
 | ||||||
|  | import { CoreItemsManager } from './items-manager'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Helper class to manage the state and routing of a swipeable page. | ||||||
|  |  */ | ||||||
|  | export abstract class CoreSwipeItemsManager<Item = unknown> extends CoreItemsManager<Item> { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Process page started operations. | ||||||
|  |      */ | ||||||
|  |     async start(): Promise<void> { | ||||||
|  |         this.updateSelectedItem(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Navigate to the next item. | ||||||
|  |      */ | ||||||
|  |     async navigateToNextItem(): Promise<void> { | ||||||
|  |         await this.navigateToItemBy(-1, 'back'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Navigate to the previous item. | ||||||
|  |      */ | ||||||
|  |     async navigateToPreviousItem(): Promise<void> { | ||||||
|  |         await this.navigateToItemBy(1, 'forward'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @inheritdoc | ||||||
|  |      */ | ||||||
|  |     protected getCurrentPageRoute(): ActivatedRoute | null { | ||||||
|  |         return CoreNavigator.getCurrentRoute(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Navigate to an item by an offset. | ||||||
|  |      * | ||||||
|  |      * @param delta Index offset. | ||||||
|  |      * @param animationDirection Animation direction. | ||||||
|  |      */ | ||||||
|  |     protected async navigateToItemBy(delta: number, animationDirection: 'forward' | 'back'): Promise<void> { | ||||||
|  |         const item = await this.getItemBy(delta); | ||||||
|  | 
 | ||||||
|  |         if (!item) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         await this.navigateToItem(item, { animationDirection, replace: true }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get item by an offset. | ||||||
|  |      * | ||||||
|  |      * @param delta Index offset. | ||||||
|  |      */ | ||||||
|  |     protected async getItemBy(delta: number): Promise<Item | null> { | ||||||
|  |         const items = this.getSource().getItems(); | ||||||
|  | 
 | ||||||
|  |         // Get current item.
 | ||||||
|  |         const index = (this.selectedItem && items?.indexOf(this.selectedItem)) ?? -1; | ||||||
|  | 
 | ||||||
|  |         if (index === -1) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Get item by delta.
 | ||||||
|  |         const item = items?.[index + delta] ?? null; | ||||||
|  | 
 | ||||||
|  |         if (!item && !this.getSource().isCompleted()) { | ||||||
|  |             await this.getSource().loadNextPage(); | ||||||
|  | 
 | ||||||
|  |             return this.getItemBy(delta); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return item; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -22,6 +22,8 @@ import { CoreUtils } from '@services/utils/utils'; | |||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Helper class to manage the state and routing of a list of items in a page, for example on pages using a split view. |  * Helper class to manage the state and routing of a list of items in a page, for example on pages using a split view. | ||||||
|  |  * | ||||||
|  |  * @deprecated use CoreListItemsManager instead. | ||||||
|  */ |  */ | ||||||
| export abstract class CorePageItemsListManager<Item> { | export abstract class CorePageItemsListManager<Item> { | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -50,6 +50,7 @@ import { CoreShowPasswordComponent } from './show-password/show-password'; | |||||||
| import { CoreSitePickerComponent } from './site-picker/site-picker'; | import { CoreSitePickerComponent } from './site-picker/site-picker'; | ||||||
| import { CoreSplitViewComponent } from './split-view/split-view'; | import { CoreSplitViewComponent } from './split-view/split-view'; | ||||||
| import { CoreStyleComponent } from './style/style'; | import { CoreStyleComponent } from './style/style'; | ||||||
|  | import { CoreSwipeNavigationComponent } from './swipe-navigation/swipe-navigation'; | ||||||
| import { CoreTabComponent } from './tabs/tab'; | import { CoreTabComponent } from './tabs/tab'; | ||||||
| import { CoreTabsComponent } from './tabs/tabs'; | import { CoreTabsComponent } from './tabs/tabs'; | ||||||
| import { CoreTabsOutletComponent } from './tabs-outlet/tabs-outlet'; | import { CoreTabsOutletComponent } from './tabs-outlet/tabs-outlet'; | ||||||
| @ -92,6 +93,7 @@ import { CoreButtonWithSpinnerComponent } from './button-with-spinner/button-wit | |||||||
|         CoreSitePickerComponent, |         CoreSitePickerComponent, | ||||||
|         CoreSplitViewComponent, |         CoreSplitViewComponent, | ||||||
|         CoreStyleComponent, |         CoreStyleComponent, | ||||||
|  |         CoreSwipeNavigationComponent, | ||||||
|         CoreTabComponent, |         CoreTabComponent, | ||||||
|         CoreTabsComponent, |         CoreTabsComponent, | ||||||
|         CoreTabsOutletComponent, |         CoreTabsOutletComponent, | ||||||
| @ -140,6 +142,7 @@ import { CoreButtonWithSpinnerComponent } from './button-with-spinner/button-wit | |||||||
|         CoreSitePickerComponent, |         CoreSitePickerComponent, | ||||||
|         CoreSplitViewComponent, |         CoreSplitViewComponent, | ||||||
|         CoreStyleComponent, |         CoreStyleComponent, | ||||||
|  |         CoreSwipeNavigationComponent, | ||||||
|         CoreTabComponent, |         CoreTabComponent, | ||||||
|         CoreTabsComponent, |         CoreTabsComponent, | ||||||
|         CoreTabsOutletComponent, |         CoreTabsOutletComponent, | ||||||
|  | |||||||
| @ -0,0 +1,5 @@ | |||||||
|  | <ion-slides [options]="{ allowTouchMove: !!manager }" (swipeleft)="swipeLeft()" (swiperight)="swipeRight()"> | ||||||
|  |     <ion-slide> | ||||||
|  |         <ng-content></ng-content> | ||||||
|  |     </ion-slide> | ||||||
|  | </ion-slides> | ||||||
| @ -0,0 +1,7 @@ | |||||||
|  | ion-slides { | ||||||
|  |     min-height: 100%; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | ion-slide { | ||||||
|  |     align-items: start; | ||||||
|  | } | ||||||
							
								
								
									
										41
									
								
								src/core/components/swipe-navigation/swipe-navigation.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/core/components/swipe-navigation/swipe-navigation.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { Component, Input } from '@angular/core'; | ||||||
|  | import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager'; | ||||||
|  | 
 | ||||||
|  | @Component({ | ||||||
|  |     selector: 'core-swipe-navigation', | ||||||
|  |     templateUrl: 'swipe-navigation.html', | ||||||
|  |     styleUrls: ['swipe-navigation.scss'], | ||||||
|  | }) | ||||||
|  | export class CoreSwipeNavigationComponent { | ||||||
|  | 
 | ||||||
|  |     @Input() manager?: CoreSwipeItemsManager; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Swipe to previous item. | ||||||
|  |      */ | ||||||
|  |     swipeLeft(): void { | ||||||
|  |         this.manager?.navigateToPreviousItem(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Swipe to next item. | ||||||
|  |      */ | ||||||
|  |     swipeRight(): void { | ||||||
|  |         this.manager?.navigateToNextItem(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -14,9 +14,11 @@ | |||||||
| 
 | 
 | ||||||
| import { HTTP_INTERCEPTORS } from '@angular/common/http'; | import { HTTP_INTERCEPTORS } from '@angular/common/http'; | ||||||
| import { ApplicationInitStatus, Injector, NgModule, Type } from '@angular/core'; | import { ApplicationInitStatus, Injector, NgModule, Type } from '@angular/core'; | ||||||
|  | import { HammerModule, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; | ||||||
| 
 | 
 | ||||||
| import { CoreApplicationInitStatus } from './classes/application-init-status'; | import { CoreApplicationInitStatus } from './classes/application-init-status'; | ||||||
| import { CoreFeaturesModule } from './features/features.module'; | import { CoreFeaturesModule } from './features/features.module'; | ||||||
|  | import { CoreHammerGestureConfig } from './classes/hammer-gesture-config'; | ||||||
| import { CoreInterceptor } from './classes/interceptor'; | import { CoreInterceptor } from './classes/interceptor'; | ||||||
| import { getDatabaseProviders } from './services/database'; | import { getDatabaseProviders } from './services/database'; | ||||||
| import { getInitializerProviders } from './initializers'; | import { getInitializerProviders } from './initializers'; | ||||||
| @ -84,9 +86,11 @@ export const CORE_SERVICES: Type<unknown>[] = [ | |||||||
| @NgModule({ | @NgModule({ | ||||||
|     imports: [ |     imports: [ | ||||||
|         CoreFeaturesModule, |         CoreFeaturesModule, | ||||||
|  |         HammerModule, | ||||||
|     ], |     ], | ||||||
|     providers: [ |     providers: [ | ||||||
|         { provide: HTTP_INTERCEPTORS, useClass: CoreInterceptor, multi: true }, |         { provide: HTTP_INTERCEPTORS, useClass: CoreInterceptor, multi: true }, | ||||||
|  |         { provide: HAMMER_GESTURE_CONFIG, useClass: CoreHammerGestureConfig }, | ||||||
|         { provide: ApplicationInitStatus, useClass: CoreApplicationInitStatus, deps: [Injector] }, |         { provide: ApplicationInitStatus, useClass: CoreApplicationInitStatus, deps: [Injector] }, | ||||||
|         ...getDatabaseProviders(), |         ...getDatabaseProviders(), | ||||||
|         ...getInitializerProviders(), |         ...getInitializerProviders(), | ||||||
|  | |||||||
							
								
								
									
										81
									
								
								src/core/features/user/classes/participants-source.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/core/features/user/classes/participants-source.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,81 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { CoreItemsManagerSource } from '@classes/items-management/items-manager-source'; | ||||||
|  | 
 | ||||||
|  | import { CoreUser, CoreUserData, CoreUserParticipant, CoreUserProvider } from '../services/user'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Provides a collection of course participants. | ||||||
|  |  */ | ||||||
|  | export class CoreUserParticipantsSource extends CoreItemsManagerSource<CoreUserParticipant | CoreUserData> { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @inheritdoc | ||||||
|  |      */ | ||||||
|  |     static getSourceId(courseId: number, searchQuery: string | null = null): string { | ||||||
|  |         searchQuery = searchQuery ?? '__empty__'; | ||||||
|  | 
 | ||||||
|  |         return `participants-${courseId}-${searchQuery}`; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     readonly COURSE_ID: number; | ||||||
|  |     readonly SEARCH_QUERY: string | null; | ||||||
|  | 
 | ||||||
|  |     constructor(courseId: number, searchQuery: string | null = null) { | ||||||
|  |         super(); | ||||||
|  | 
 | ||||||
|  |         this.COURSE_ID = courseId; | ||||||
|  |         this.SEARCH_QUERY = searchQuery; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @inheritdoc | ||||||
|  |      */ | ||||||
|  |     protected async loadPageItems(page: number): Promise<{ items: (CoreUserParticipant | CoreUserData)[]; hasMoreItems: boolean }> { | ||||||
|  |         if (this.SEARCH_QUERY) { | ||||||
|  |             const { participants, canLoadMore } = await CoreUser.searchParticipants( | ||||||
|  |                 this.COURSE_ID, | ||||||
|  |                 this.SEARCH_QUERY, | ||||||
|  |                 true, | ||||||
|  |                 page, | ||||||
|  |                 CoreUserProvider.PARTICIPANTS_LIST_LIMIT, | ||||||
|  |             ); | ||||||
|  | 
 | ||||||
|  |             return { | ||||||
|  |                 items: participants, | ||||||
|  |                 hasMoreItems: canLoadMore, | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const { participants, canLoadMore } = await CoreUser.getParticipants( | ||||||
|  |             this.COURSE_ID, | ||||||
|  |             page * CoreUserProvider.PARTICIPANTS_LIST_LIMIT, | ||||||
|  |             CoreUserProvider.PARTICIPANTS_LIST_LIMIT, | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         return { | ||||||
|  |             items: participants, | ||||||
|  |             hasMoreItems: canLoadMore, | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @inheritdoc | ||||||
|  |      */ | ||||||
|  |     protected getPageLength(): number { | ||||||
|  |         return CoreUserProvider.PARTICIPANTS_LIST_LIMIT; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -13,16 +13,18 @@ | |||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; | import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; | ||||||
|  | import { Params } from '@angular/router'; | ||||||
| import { IonRefresher } from '@ionic/angular'; | import { IonRefresher } from '@ionic/angular'; | ||||||
| 
 | 
 | ||||||
| import { CoreApp } from '@services/app'; | import { CoreApp } from '@services/app'; | ||||||
| import { CoreDomUtils } from '@services/utils/dom'; | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
| import { CoreNavigator } from '@services/navigator'; | import { CoreNavigator } from '@services/navigator'; | ||||||
| import { CorePageItemsListManager } from '@classes/page-items-list-manager'; | import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; | ||||||
| import { CoreScreen } from '@services/screen'; |  | ||||||
| import { CoreSplitViewComponent } from '@components/split-view/split-view'; | import { CoreSplitViewComponent } from '@components/split-view/split-view'; | ||||||
| import { CoreUser, CoreUserProvider, CoreUserParticipant, CoreUserData } from '@features/user/services/user'; | import { CoreUser, CoreUserParticipant, CoreUserData } from '@features/user/services/user'; | ||||||
| import { CoreUtils } from '@services/utils/utils'; | import { CoreUtils } from '@services/utils/utils'; | ||||||
|  | import { CoreUserParticipantsSource } from '@features/user/classes/participants-source'; | ||||||
|  | import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Page that displays the list of course participants. |  * Page that displays the list of course participants. | ||||||
| @ -33,6 +35,7 @@ import { CoreUtils } from '@services/utils/utils'; | |||||||
| }) | }) | ||||||
| export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestroy { | export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestroy { | ||||||
| 
 | 
 | ||||||
|  |     courseId!: number; | ||||||
|     participants!: CoreUserParticipantsManager; |     participants!: CoreUserParticipantsManager; | ||||||
|     searchQuery: string | null = null; |     searchQuery: string | null = null; | ||||||
|     searchInProgress = false; |     searchInProgress = false; | ||||||
| @ -43,10 +46,12 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro | |||||||
|     @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; |     @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; | ||||||
| 
 | 
 | ||||||
|     constructor() { |     constructor() { | ||||||
|         let courseId: number; |  | ||||||
| 
 |  | ||||||
|         try { |         try { | ||||||
|             courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); |             this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); | ||||||
|  |             this.participants = new CoreUserParticipantsManager( | ||||||
|  |                 CoreItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId]), | ||||||
|  |                 this, | ||||||
|  |             ); | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             CoreDomUtils.showErrorModal(error); |             CoreDomUtils.showErrorModal(error); | ||||||
| 
 | 
 | ||||||
| @ -55,7 +60,6 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro | |||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.participants = new CoreUserParticipantsManager(CoreUserParticipantsPage, courseId); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -104,9 +108,11 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro | |||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         const newSource = CoreItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId]); | ||||||
|  | 
 | ||||||
|         this.searchQuery = null; |         this.searchQuery = null; | ||||||
|         this.searchInProgress = false; |         this.searchInProgress = false; | ||||||
|         this.participants.resetItems(); |         this.participants.setSource(newSource); | ||||||
| 
 | 
 | ||||||
|         await this.fetchInitialParticipants(); |         await this.fetchInitialParticipants(); | ||||||
|     } |     } | ||||||
| @ -119,9 +125,11 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro | |||||||
|     async search(query: string): Promise<void> { |     async search(query: string): Promise<void> { | ||||||
|         CoreApp.closeKeyboard(); |         CoreApp.closeKeyboard(); | ||||||
| 
 | 
 | ||||||
|  |         const newSource = CoreItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId, query]); | ||||||
|  | 
 | ||||||
|         this.searchInProgress = true; |         this.searchInProgress = true; | ||||||
|         this.searchQuery = query; |         this.searchQuery = query; | ||||||
|         this.participants.resetItems(); |         this.participants.setSource(newSource); | ||||||
| 
 | 
 | ||||||
|         await this.fetchInitialParticipants(); |         await this.fetchInitialParticipants(); | ||||||
| 
 | 
 | ||||||
| @ -134,8 +142,8 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro | |||||||
|      * @param refresher Refresher. |      * @param refresher Refresher. | ||||||
|      */ |      */ | ||||||
|     async refreshParticipants(refresher: IonRefresher): Promise<void> { |     async refreshParticipants(refresher: IonRefresher): Promise<void> { | ||||||
|         await CoreUtils.ignoreErrors(CoreUser.invalidateParticipantsList(this.participants.courseId)); |         await CoreUtils.ignoreErrors(CoreUser.invalidateParticipantsList(this.courseId)); | ||||||
|         await CoreUtils.ignoreErrors(this.fetchParticipants()); |         await CoreUtils.ignoreErrors(this.fetchParticipants(true)); | ||||||
| 
 | 
 | ||||||
|         refresher?.complete(); |         refresher?.complete(); | ||||||
|     } |     } | ||||||
| @ -147,7 +155,7 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro | |||||||
|      */ |      */ | ||||||
|     async fetchMoreParticipants(complete: () => void): Promise<void> { |     async fetchMoreParticipants(complete: () => void): Promise<void> { | ||||||
|         try { |         try { | ||||||
|             await this.fetchParticipants(this.participants.items); |             await this.fetchParticipants(false); | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             CoreDomUtils.showErrorModalDefault(error, 'Error loading more participants'); |             CoreDomUtils.showErrorModalDefault(error, 'Error loading more participants'); | ||||||
| 
 | 
 | ||||||
| @ -162,38 +170,23 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro | |||||||
|      */ |      */ | ||||||
|     private async fetchInitialParticipants(): Promise<void> { |     private async fetchInitialParticipants(): Promise<void> { | ||||||
|         try { |         try { | ||||||
|             await this.fetchParticipants(); |             await this.fetchParticipants(true); | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             CoreDomUtils.showErrorModalDefault(error, 'Error loading participants'); |             CoreDomUtils.showErrorModalDefault(error, 'Error loading participants'); | ||||||
| 
 | 
 | ||||||
|             this.participants.setItems([]); |             this.participants.reset(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Update the list of participants. |      * Update the list of participants. | ||||||
|      * |      * | ||||||
|      * @param loadedParticipants Participants list to continue loading from. |      * @param reload Whether to reload the list or load the next page. | ||||||
|      */ |      */ | ||||||
|     private async fetchParticipants(loadedParticipants: CoreUserParticipant[] | CoreUserData[] = []): Promise<void> { |     private async fetchParticipants(reload: boolean): Promise<void> { | ||||||
|         if (this.searchQuery) { |         reload | ||||||
|             const { participants, canLoadMore } = await CoreUser.searchParticipants( |             ? await this.participants.reload() | ||||||
|                 this.participants.courseId, |             : await this.participants.loadNextPage(); | ||||||
|                 this.searchQuery, |  | ||||||
|                 true, |  | ||||||
|                 Math.ceil(loadedParticipants.length / CoreUserProvider.PARTICIPANTS_LIST_LIMIT), |  | ||||||
|                 CoreUserProvider.PARTICIPANTS_LIST_LIMIT, |  | ||||||
|             ); |  | ||||||
| 
 |  | ||||||
|             this.participants.setItems((loadedParticipants as CoreUserData[]).concat(participants), canLoadMore); |  | ||||||
|         } else { |  | ||||||
|             const { participants, canLoadMore } = await CoreUser.getParticipants( |  | ||||||
|                 this.participants.courseId, |  | ||||||
|                 loadedParticipants.length, |  | ||||||
|             ); |  | ||||||
| 
 |  | ||||||
|             this.participants.setItems((loadedParticipants as CoreUserParticipant[]).concat(participants), canLoadMore); |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         this.fetchMoreParticipantsFailed = false; |         this.fetchMoreParticipantsFailed = false; | ||||||
|     } |     } | ||||||
| @ -203,30 +196,14 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro | |||||||
| /** | /** | ||||||
|  * Helper to manage the list of participants. |  * Helper to manage the list of participants. | ||||||
|  */ |  */ | ||||||
| class CoreUserParticipantsManager extends CorePageItemsListManager<CoreUserParticipant | CoreUserData> { | class CoreUserParticipantsManager extends CoreListItemsManager<CoreUserParticipant | CoreUserData> { | ||||||
| 
 | 
 | ||||||
|     courseId: number; |     page: CoreUserParticipantsPage; | ||||||
| 
 | 
 | ||||||
|     constructor(pageComponent: unknown, courseId: number) { |     constructor(source: CoreUserParticipantsSource, page: CoreUserParticipantsPage) { | ||||||
|         super(pageComponent); |         super(source, CoreUserParticipantsPage); | ||||||
| 
 | 
 | ||||||
|         this.courseId = courseId; |         this.page = page; | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * @inheritdoc |  | ||||||
|      */ |  | ||||||
|     async select(participant: CoreUserParticipant | CoreUserData): Promise<void> { |  | ||||||
|         if (CoreScreen.isMobile) { |  | ||||||
|             await CoreNavigator.navigateToSitePath( |  | ||||||
|                 '/user/profile', |  | ||||||
|                 { params: { userId: participant.id, courseId: this.courseId } }, |  | ||||||
|             ); |  | ||||||
| 
 |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return super.select(participant); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -236,11 +213,18 @@ class CoreUserParticipantsManager extends CorePageItemsListManager<CoreUserParti | |||||||
|         return participant.id.toString(); |         return participant.id.toString(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * @inheritdoc | ||||||
|  |      */ | ||||||
|  |     protected getItemQueryParams(): Params { | ||||||
|  |         return { search: this.page.searchQuery }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * @inheritdoc |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     protected async logActivity(): Promise<void> { |     protected async logActivity(): Promise<void> { | ||||||
|         await CoreUser.logParticipantsView(this.courseId); |         await CoreUser.logParticipantsView(this.page.courseId); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -7,90 +7,87 @@ | |||||||
|     </ion-toolbar> |     </ion-toolbar> | ||||||
| </ion-header> | </ion-header> | ||||||
| <ion-content> | <ion-content> | ||||||
|     <ion-refresher slot="fixed" [disabled]="!userLoaded" (ionRefresh)="refreshUser($event.target)"> |     <core-swipe-navigation [manager]="users"> | ||||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> |         <ion-refresher slot="fixed" [disabled]="!userLoaded" (ionRefresh)="refreshUser($event.target)"> | ||||||
|     </ion-refresher> |             <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||||
|     <core-loading [hideUntil]="userLoaded"> |         </ion-refresher> | ||||||
|         <ion-list *ngIf="user && !isDeleted && isEnrolled"> |         <core-loading [hideUntil]="userLoaded"> | ||||||
|             <ion-item class="ion-text-center core-user-profile-maininfo"> |             <ion-list *ngIf="user && !isDeleted && isEnrolled"> | ||||||
|                 <core-user-avatar [user]="user" [userId]="user.id" [linkProfile]="false" [checkOnline]="true"> |                 <ion-item class="ion-text-center core-user-profile-maininfo"> | ||||||
|                 </core-user-avatar> |                     <core-user-avatar [user]="user" [userId]="user.id" [linkProfile]="false" [checkOnline]="true"> | ||||||
|                 <ion-label> |                     </core-user-avatar> | ||||||
|                     <h2>{{ user.fullname }}</h2> |  | ||||||
|                     <p *ngIf="user.address"> |  | ||||||
|                         <ion-icon name="fas-map-marker-alt" [attr.aria-hidden]="true"></ion-icon> {{ user.address }} |  | ||||||
|                     </p> |  | ||||||
|                     <p *ngIf="rolesFormatted" class="ion-text-wrap"> |  | ||||||
|                         <strong>{{ 'core.user.roles' | translate}}</strong>{{'core.labelsep' | translate}} |  | ||||||
|                         {{ rolesFormatted }} |  | ||||||
|                     </p> |  | ||||||
|                 </ion-label> |  | ||||||
|             </ion-item> |  | ||||||
| 
 |  | ||||||
|             <div class="core-user-communication-handlers" |  | ||||||
|                 *ngIf="(communicationHandlers && communicationHandlers.length) || isLoadingHandlers"> |  | ||||||
|                 <ion-item *ngIf="communicationHandlers && communicationHandlers.length"> |  | ||||||
|                     <ion-label> |                     <ion-label> | ||||||
|                         <ion-button *ngFor="let handler of communicationHandlers" expand="block" size="default" |                         <h2>{{ user.fullname }}</h2> | ||||||
|  |                         <p *ngIf="user.address"> | ||||||
|  |                             <ion-icon name="fas-map-marker-alt" [attr.aria-hidden]="true"></ion-icon> {{ user.address }} | ||||||
|  |                         </p> | ||||||
|  |                         <p *ngIf="rolesFormatted" class="ion-text-wrap"> | ||||||
|  |                             <strong>{{ 'core.user.roles' | translate}}</strong>{{'core.labelsep' | translate}} | ||||||
|  |                             {{ rolesFormatted }} | ||||||
|  |                         </p> | ||||||
|  |                     </ion-label> | ||||||
|  |                 </ion-item> | ||||||
|  | 
 | ||||||
|  |                 <div class="core-user-communication-handlers" | ||||||
|  |                     *ngIf="(communicationHandlers && communicationHandlers.length) || isLoadingHandlers"> | ||||||
|  |                     <ion-item *ngIf="communicationHandlers && communicationHandlers.length"> | ||||||
|  |                         <ion-label> | ||||||
|  |                             <ion-button *ngFor="let handler of communicationHandlers" expand="block" size="default" | ||||||
|  |                                 [ngClass]="['core-user-profile-handler', handler.class || '']" (click)="handlerClicked($event, handler)" | ||||||
|  |                                 [hidden]="handler.hidden" [attr.aria-label]="handler.title | translate" [disabled]="handler.spinner"> | ||||||
|  |                                 <ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start" aria-hidden="true"></ion-icon> | ||||||
|  |                                 {{ handler.title | translate }} | ||||||
|  |                             </ion-button> | ||||||
|  |                         </ion-label> | ||||||
|  |                     </ion-item> | ||||||
|  |                     <div *ngIf="isLoadingHandlers" class="ion-text-center core-loading-handlers"> | ||||||
|  |                         <ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |                 <ion-item button class="ion-text-wrap core-user-profile-handler" (click)="openUserDetails()" | ||||||
|  |                     [attr.aria-label]="'core.user.details' | translate" detail="true"> | ||||||
|  |                     <ion-icon name="fas-user" slot="start" aria-hidden="true"></ion-icon> | ||||||
|  |                     <ion-label> | ||||||
|  |                         <p class="item-heading">{{ 'core.user.details' | translate }}</p> | ||||||
|  |                     </ion-label> | ||||||
|  |                 </ion-item> | ||||||
|  |                 <ion-item class="ion-text-center core-loading-handlers" *ngIf="isLoadingHandlers"> | ||||||
|  |                     <ion-label> | ||||||
|  |                         <ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner> | ||||||
|  |                     </ion-label> | ||||||
|  |                 </ion-item> | ||||||
|  |                 <ion-item button *ngFor="let handler of newPageHandlers" class="ion-text-wrap" (click)="handlerClicked($event, handler)" | ||||||
|  |                     [ngClass]="['core-user-profile-handler', handler.class || '']" [hidden]="handler.hidden" | ||||||
|  |                     [attr.aria-label]="handler.title | translate" detail="true"> | ||||||
|  |                     <ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start" aria-hidden="true"></ion-icon> | ||||||
|  |                     <ion-label> | ||||||
|  |                         <p class="item-heading">{{ handler.title | translate }}</p> | ||||||
|  |                     </ion-label> | ||||||
|  |                     <ion-badge slot="end" *ngIf="handler.showBadge" [hidden]="handler.loading || !handler.badge" aria-hidden="true"> | ||||||
|  |                         {{handler.badge}} | ||||||
|  |                     </ion-badge> | ||||||
|  |                     <span *ngIf="handler.showBadge && handler.badge && handler.badgeA11yText" class="sr-only"> | ||||||
|  |                         {{ handler.badgeA11yText | translate: {$a : handler.badge } }} | ||||||
|  |                     </span> | ||||||
|  |                     <ion-spinner slot="end" *ngIf="handler.showBadge && handler.loading" [attr.aria-label]="'core.loading' | translate"> | ||||||
|  |                     </ion-spinner> | ||||||
|  |                 </ion-item> | ||||||
|  |                 <ion-item *ngIf="actionHandlers && actionHandlers.length"> | ||||||
|  |                     <ion-label> | ||||||
|  |                         <ion-button *ngFor="let handler of actionHandlers" expand="block" fill="outline" size="default" | ||||||
|                             [ngClass]="['core-user-profile-handler', handler.class || '']" (click)="handlerClicked($event, handler)" |                             [ngClass]="['core-user-profile-handler', handler.class || '']" (click)="handlerClicked($event, handler)" | ||||||
|                             [hidden]="handler.hidden" [attr.aria-label]="handler.title | translate" [disabled]="handler.spinner"> |                             [hidden]="handler.hidden" [attr.aria-label]="handler.title | translate" [disabled]="handler.spinner"> | ||||||
|                             <ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start" aria-hidden="true"></ion-icon> |                             <ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start" aria-hidden="true"></ion-icon> | ||||||
|                             {{ handler.title | translate }} |                             {{ handler.title | translate }} | ||||||
|  |                             <ion-spinner *ngIf="handler.spinner" slot="end" [attr.aria-label]="'core.loading' | translate"></ion-spinner> | ||||||
|                         </ion-button> |                         </ion-button> | ||||||
|                     </ion-label> |                     </ion-label> | ||||||
|                 </ion-item> |                 </ion-item> | ||||||
|                 <div *ngIf="isLoadingHandlers" class="ion-text-center core-loading-handlers"> |             </ion-list> | ||||||
|                     <ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner> |             <core-empty-box *ngIf="!user && !isDeleted && isEnrolled" icon="far-user" [message]=" 'core.user.detailsnotavailable' | translate"> | ||||||
|                 </div> |             </core-empty-box> | ||||||
|             </div> |             <core-empty-box *ngIf="isDeleted" icon="far-user" [message]="'core.userdeleted' | translate"></core-empty-box> | ||||||
| 
 |             <core-empty-box *ngIf="!isEnrolled" icon="far-user" [message]="'core.notenrolledprofile' | translate"></core-empty-box> | ||||||
| 
 |         </core-loading> | ||||||
|             <ion-item button class="ion-text-wrap core-user-profile-handler" (click)="openUserDetails()" |     </core-swipe-navigation> | ||||||
|                 [attr.aria-label]="'core.user.details' | translate" detail="true"> |  | ||||||
|                 <ion-icon name="fas-user" slot="start" aria-hidden="true"></ion-icon> |  | ||||||
|                 <ion-label> |  | ||||||
|                     <p class="item-heading">{{ 'core.user.details' | translate }}</p> |  | ||||||
|                 </ion-label> |  | ||||||
|             </ion-item> |  | ||||||
|             <ion-item class="ion-text-center core-loading-handlers" *ngIf="isLoadingHandlers"> |  | ||||||
|                 <ion-label> |  | ||||||
|                     <ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner> |  | ||||||
|                 </ion-label> |  | ||||||
|             </ion-item> |  | ||||||
| 
 |  | ||||||
|             <ion-item button *ngFor="let handler of newPageHandlers" class="ion-text-wrap" (click)="handlerClicked($event, handler)" |  | ||||||
|                 [ngClass]="['core-user-profile-handler', handler.class || '']" [hidden]="handler.hidden" |  | ||||||
|                 [attr.aria-label]="handler.title | translate" detail="true"> |  | ||||||
|                 <ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start" aria-hidden="true"></ion-icon> |  | ||||||
|                 <ion-label> |  | ||||||
|                     <p class="item-heading">{{ handler.title | translate }}</p> |  | ||||||
|                 </ion-label> |  | ||||||
|                 <ion-badge slot="end" *ngIf="handler.showBadge" [hidden]="handler.loading || !handler.badge" aria-hidden="true"> |  | ||||||
|                     {{handler.badge}} |  | ||||||
|                 </ion-badge> |  | ||||||
|                 <span *ngIf="handler.showBadge && handler.badge && handler.badgeA11yText" class="sr-only"> |  | ||||||
|                     {{ handler.badgeA11yText | translate: {$a : handler.badge } }} |  | ||||||
|                 </span> |  | ||||||
|                 <ion-spinner slot="end" *ngIf="handler.showBadge && handler.loading" [attr.aria-label]="'core.loading' | translate"> |  | ||||||
|                 </ion-spinner> |  | ||||||
|             </ion-item> |  | ||||||
| 
 |  | ||||||
|             <ion-item *ngIf="actionHandlers && actionHandlers.length"> |  | ||||||
|                 <ion-label> |  | ||||||
|                     <ion-button *ngFor="let handler of actionHandlers" expand="block" fill="outline" size="default" |  | ||||||
|                         [ngClass]="['core-user-profile-handler', handler.class || '']" (click)="handlerClicked($event, handler)" |  | ||||||
|                         [hidden]="handler.hidden" [attr.aria-label]="handler.title | translate" [disabled]="handler.spinner"> |  | ||||||
|                         <ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start" aria-hidden="true"></ion-icon> |  | ||||||
|                         {{ handler.title | translate }} |  | ||||||
|                         <ion-spinner *ngIf="handler.spinner" slot="end" [attr.aria-label]="'core.loading' | translate"></ion-spinner> |  | ||||||
|                     </ion-button> |  | ||||||
|                 </ion-label> |  | ||||||
|             </ion-item> |  | ||||||
|         </ion-list> |  | ||||||
| 
 |  | ||||||
|         <core-empty-box *ngIf="!user && !isDeleted && isEnrolled" icon="far-user" [message]=" 'core.user.detailsnotavailable' | translate"> |  | ||||||
|         </core-empty-box> |  | ||||||
|         <core-empty-box *ngIf="isDeleted" icon="far-user" [message]="'core.userdeleted' | translate"></core-empty-box> |  | ||||||
|         <core-empty-box *ngIf="!isEnrolled" icon="far-user" [message]="'core.notenrolledprofile' | translate"></core-empty-box> |  | ||||||
|     </core-loading> |  | ||||||
| </ion-content> | </ion-content> | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ | |||||||
| // See the License for the specific language governing permissions and
 | // See the License for the specific language governing permissions and
 | ||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
|  | import { ActivatedRoute, ActivatedRouteSnapshot, Params } from '@angular/router'; | ||||||
| import { Component, OnDestroy, OnInit } from '@angular/core'; | import { Component, OnDestroy, OnInit } from '@angular/core'; | ||||||
| import { IonRefresher } from '@ionic/angular'; | import { IonRefresher } from '@ionic/angular'; | ||||||
| import { Subscription } from 'rxjs'; | import { Subscription } from 'rxjs'; | ||||||
| @ -20,16 +21,16 @@ import { CoreSite } from '@classes/site'; | |||||||
| import { CoreSites } from '@services/sites'; | import { CoreSites } from '@services/sites'; | ||||||
| import { CoreDomUtils } from '@services/utils/dom'; | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||||
| import { | import { CoreUser, CoreUserBasicData, CoreUserProfile, CoreUserProvider } from '@features/user/services/user'; | ||||||
|     CoreUser, |  | ||||||
|     CoreUserProfile, |  | ||||||
|     CoreUserProvider, |  | ||||||
| } from '@features/user/services/user'; |  | ||||||
| import { CoreUserHelper } from '@features/user/services/user-helper'; | import { CoreUserHelper } from '@features/user/services/user-helper'; | ||||||
| import { CoreUserDelegate, CoreUserDelegateService, CoreUserProfileHandlerData } from '@features/user/services/user-delegate'; | import { CoreUserDelegate, CoreUserDelegateService, CoreUserProfileHandlerData } from '@features/user/services/user-delegate'; | ||||||
| import { CoreUtils } from '@services/utils/utils'; | import { CoreUtils } from '@services/utils/utils'; | ||||||
| import { CoreNavigator } from '@services/navigator'; | import { CoreNavigator } from '@services/navigator'; | ||||||
| import { CoreCourses } from '@features/courses/services/courses'; | import { CoreCourses } from '@features/courses/services/courses'; | ||||||
|  | import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager'; | ||||||
|  | import { CoreUserParticipantsSource } from '@features/user/classes/participants-source'; | ||||||
|  | import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker'; | ||||||
|  | import { CoreItemsManagerSource } from '@classes/items-management/items-manager-source'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|     selector: 'page-core-user-profile', |     selector: 'page-core-user-profile', | ||||||
| @ -55,7 +56,10 @@ export class CoreUserProfilePage implements OnInit, OnDestroy { | |||||||
|     newPageHandlers: CoreUserProfileHandlerData[] = []; |     newPageHandlers: CoreUserProfileHandlerData[] = []; | ||||||
|     communicationHandlers: CoreUserProfileHandlerData[] = []; |     communicationHandlers: CoreUserProfileHandlerData[] = []; | ||||||
| 
 | 
 | ||||||
|     constructor() { |     users?: CoreUserSwipeItemsManager; | ||||||
|  |     usersQueryParams: Params = {}; | ||||||
|  | 
 | ||||||
|  |     constructor(private route: ActivatedRoute) { | ||||||
|         this.obsProfileRefreshed = CoreEvents.on(CoreUserProvider.PROFILE_REFRESHED, (data) => { |         this.obsProfileRefreshed = CoreEvents.on(CoreUserProvider.PROFILE_REFRESHED, (data) => { | ||||||
|             if (!this.user || !data.user) { |             if (!this.user || !data.user) { | ||||||
|                 return; |                 return; | ||||||
| @ -86,6 +90,15 @@ export class CoreUserProfilePage implements OnInit, OnDestroy { | |||||||
|             this.courseId = undefined; |             this.courseId = undefined; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         if (this.courseId && this.route.snapshot.data.swipeManagerSource === 'participants') { | ||||||
|  |             const search = CoreNavigator.getRouteParam('search'); | ||||||
|  |             const source = CoreItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId, search]); | ||||||
|  |             this.users = new CoreUserSwipeItemsManager(source, this); | ||||||
|  | 
 | ||||||
|  |             this.usersQueryParams.search = search; | ||||||
|  |             this.users.start(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         try { |         try { | ||||||
|             await this.fetchUser(); |             await this.fetchUser(); | ||||||
| 
 | 
 | ||||||
| @ -204,8 +217,49 @@ export class CoreUserProfilePage implements OnInit, OnDestroy { | |||||||
|      * @inheritdoc |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     ngOnDestroy(): void { |     ngOnDestroy(): void { | ||||||
|  |         this.users?.destroy(); | ||||||
|         this.subscription?.unsubscribe(); |         this.subscription?.unsubscribe(); | ||||||
|         this.obsProfileRefreshed.off(); |         this.obsProfileRefreshed.off(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Helper to manage swiping within a collection of users. | ||||||
|  |  */ | ||||||
|  | class CoreUserSwipeItemsManager extends CoreSwipeItemsManager<CoreUserBasicData> { | ||||||
|  | 
 | ||||||
|  |     page: CoreUserProfilePage; | ||||||
|  | 
 | ||||||
|  |     constructor(source: CoreItemsManagerSource<CoreUserBasicData>, page: CoreUserProfilePage) { | ||||||
|  |         super(source); | ||||||
|  | 
 | ||||||
|  |         this.page = page; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @inheritdoc | ||||||
|  |      */ | ||||||
|  |     protected getItemPath(item: CoreUserBasicData): string { | ||||||
|  |         return String(item.id); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @inheritdoc | ||||||
|  |      */ | ||||||
|  |     protected getItemQueryParams(): Params { | ||||||
|  |         return this.page.usersQueryParams; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @inheritdoc | ||||||
|  |      */ | ||||||
|  |     protected getSelectedItemPath(route?: ActivatedRouteSnapshot | null): string | null { | ||||||
|  |         if (!route) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return route.params.userId; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | |||||||
| @ -19,6 +19,9 @@ | |||||||
|                 right: calc(50% - 12px -  var(--core-avatar-size) / 2) !important; |                 right: calc(50% - 12px -  var(--core-avatar-size) / 2) !important; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |         core-loading .core-loading-content { | ||||||
|  |             width: 100%; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -21,6 +21,7 @@ import { | |||||||
| } from '@features/course/services/course-options-delegate'; | } from '@features/course/services/course-options-delegate'; | ||||||
| import { CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses'; | import { CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses'; | ||||||
| import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '@features/courses/services/courses-helper'; | import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '@features/courses/services/courses-helper'; | ||||||
|  | import { PARTICIPANTS_PAGE_NAME } from '@features/user/user.module'; | ||||||
| import { makeSingleton } from '@singletons'; | import { makeSingleton } from '@singletons'; | ||||||
| import { CoreUser } from '../user'; | import { CoreUser } from '../user'; | ||||||
| 
 | 
 | ||||||
| @ -89,7 +90,7 @@ export class CoreUserCourseOptionHandlerService implements CoreCourseOptionsHand | |||||||
|         return { |         return { | ||||||
|             title: 'core.user.participants', |             title: 'core.user.participants', | ||||||
|             class: 'core-user-participants-handler', |             class: 'core-user-participants-handler', | ||||||
|             page: 'participants', |             page: PARTICIPANTS_PAGE_NAME, | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -19,17 +19,19 @@ import { CoreSharedModule } from '@/core/shared.module'; | |||||||
| import { CoreSearchComponentsModule } from '@features/search/components/components.module'; | import { CoreSearchComponentsModule } from '@features/search/components/components.module'; | ||||||
| 
 | 
 | ||||||
| import { CoreUserParticipantsPage } from './pages/participants/participants.page'; | import { CoreUserParticipantsPage } from './pages/participants/participants.page'; | ||||||
|  | import { conditionalRoutes } from '@/app/app-routing.module'; | ||||||
|  | import { CoreScreen } from '@services/screen'; | ||||||
| 
 | 
 | ||||||
| const routes: Routes = [ | const routes: Routes = [ | ||||||
|     { |     { | ||||||
|         path: '', |         path: '', | ||||||
|         component: CoreUserParticipantsPage, |         component: CoreUserParticipantsPage, | ||||||
|         children: [ |         children: conditionalRoutes([ | ||||||
|             { |             { | ||||||
|                 path: ':userId', |                 path: ':userId', | ||||||
|                 loadChildren: () => import('@features/user/pages/profile/profile.module').then(m => m.CoreUserProfilePageModule), |                 loadChildren: () => import('@features/user/pages/profile/profile.module').then(m => m.CoreUserProfilePageModule), | ||||||
|             }, |             }, | ||||||
|         ], |         ], () => CoreScreen.isTablet), | ||||||
|     }, |     }, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -35,6 +35,10 @@ import { CoreUserProvider } from './services/user'; | |||||||
| import { CoreUserHelperProvider } from './services/user-helper'; | import { CoreUserHelperProvider } from './services/user-helper'; | ||||||
| import { CoreUserOfflineProvider } from './services/user-offline'; | import { CoreUserOfflineProvider } from './services/user-offline'; | ||||||
| import { CoreUserSyncProvider } from './services/user-sync'; | import { CoreUserSyncProvider } from './services/user-sync'; | ||||||
|  | import { conditionalRoutes } from '@/app/app-routing.module'; | ||||||
|  | import { CoreScreen } from '@services/screen'; | ||||||
|  | import { COURSE_PAGE_NAME } from '@features/course/course.module'; | ||||||
|  | import { COURSE_INDEX_PATH } from '@features/course/course-lazy.module'; | ||||||
| 
 | 
 | ||||||
| export const CORE_USER_SERVICES: Type<unknown>[] = [ | export const CORE_USER_SERVICES: Type<unknown>[] = [ | ||||||
|     CoreUserDelegateService, |     CoreUserDelegateService, | ||||||
| @ -45,16 +49,27 @@ export const CORE_USER_SERVICES: Type<unknown>[] = [ | |||||||
|     CoreUserSyncProvider, |     CoreUserSyncProvider, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
|  | export const PARTICIPANTS_PAGE_NAME = 'participants'; | ||||||
|  | 
 | ||||||
| const routes: Routes = [ | const routes: Routes = [ | ||||||
|     { |     { | ||||||
|         path: 'user', |         path: 'user', | ||||||
|         loadChildren: () => import('@features/user/user-lazy.module').then(m => m.CoreUserLazyModule), |         loadChildren: () => import('@features/user/user-lazy.module').then(m => m.CoreUserLazyModule), | ||||||
|     }, |     }, | ||||||
|  |     ...conditionalRoutes([ | ||||||
|  |         { | ||||||
|  |             path: `${COURSE_PAGE_NAME}/${COURSE_INDEX_PATH}/${PARTICIPANTS_PAGE_NAME}/:userId`, | ||||||
|  |             loadChildren: () => import('@features/user/pages/profile/profile.module').then(m => m.CoreUserProfilePageModule), | ||||||
|  |             data: { | ||||||
|  |                 swipeManagerSource: 'participants', | ||||||
|  |             }, | ||||||
|  |         }, | ||||||
|  |     ], () => CoreScreen.isMobile), | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| const courseIndexRoutes: Routes = [ | const courseIndexRoutes: Routes = [ | ||||||
|     { |     { | ||||||
|         path: 'participants', |         path: PARTICIPANTS_PAGE_NAME, | ||||||
|         loadChildren: () => import('@features/user/user-course-lazy.module').then(m => m.CoreUserCourseLazyModule), |         loadChildren: () => import('@features/user/user-course-lazy.module').then(m => m.CoreUserCourseLazyModule), | ||||||
|     }, |     }, | ||||||
| ]; | ]; | ||||||
|  | |||||||
| @ -48,6 +48,7 @@ export type CoreRedirectPayload = { | |||||||
| export type CoreNavigationOptions = Pick<NavigationOptions, 'animated'|'animation'|'animationDirection'> & { | export type CoreNavigationOptions = Pick<NavigationOptions, 'animated'|'animation'|'animationDirection'> & { | ||||||
|     params?: Params; |     params?: Params; | ||||||
|     reset?: boolean; |     reset?: boolean; | ||||||
|  |     replace?: boolean; | ||||||
|     preferCurrentTab?: boolean; // Default true.
 |     preferCurrentTab?: boolean; // Default true.
 | ||||||
|     nextNavigation?: { |     nextNavigation?: { | ||||||
|         path: string; |         path: string; | ||||||
| @ -137,6 +138,7 @@ export class CoreNavigatorService { | |||||||
|             animationDirection: options.animationDirection, |             animationDirection: options.animationDirection, | ||||||
|             queryParams: CoreObject.isEmpty(options.params ?? {}) ? null : CoreObject.withoutEmpty(options.params), |             queryParams: CoreObject.isEmpty(options.params ?? {}) ? null : CoreObject.withoutEmpty(options.params), | ||||||
|             relativeTo: path.startsWith('/') ? null : this.getCurrentRoute(), |             relativeTo: path.startsWith('/') ? null : this.getCurrentRoute(), | ||||||
|  |             replaceUrl: options.replace, | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         // Remove objects from queryParams and replace them with an ID.
 |         // Remove objects from queryParams and replace them with an ID.
 | ||||||
| @ -264,14 +266,16 @@ export class CoreNavigatorService { | |||||||
|      * @return Value of the parameter, undefined if not found. |      * @return Value of the parameter, undefined if not found. | ||||||
|      */ |      */ | ||||||
|     protected getRouteSnapshotParam<T = unknown>(name: string, route?: ActivatedRoute): T | undefined { |     protected getRouteSnapshotParam<T = unknown>(name: string, route?: ActivatedRoute): T | undefined { | ||||||
|         if (!route?.snapshot) { |         if (!route) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const value = route.snapshot.queryParams[name] ?? route.snapshot.params[name]; |         if (route.snapshot) { | ||||||
|  |             const value = route.snapshot.queryParams[name] ?? route.snapshot.params[name]; | ||||||
| 
 | 
 | ||||||
|         if (value !== undefined) { |             if (value !== undefined) { | ||||||
|             return value; |                 return value; | ||||||
|  |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return this.getRouteSnapshotParam(name, route.parent || undefined); |         return this.getRouteSnapshotParam(name, route.parent || undefined); | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user