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", | ||||
|         "core-js": "^3.9.1", | ||||
|         "es6-promise-plugin": "^4.2.2", | ||||
|         "hammerjs": "^2.0.8", | ||||
|         "jszip": "^3.5.0", | ||||
|         "mathjax": "2.7.7", | ||||
|         "moment": "^2.29.0", | ||||
| @ -15988,6 +15989,14 @@ | ||||
|         "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": { | ||||
|       "version": "2.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", | ||||
| @ -43687,6 +43696,11 @@ | ||||
|         "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": { | ||||
|       "version": "2.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", | ||||
|  | ||||
| @ -117,6 +117,7 @@ | ||||
|     "cordova.plugins.diagnostic": "^5.0.2", | ||||
|     "core-js": "^3.9.1", | ||||
|     "es6-promise-plugin": "^4.2.2", | ||||
|     "hammerjs": "^2.0.8", | ||||
|     "jszip": "^3.5.0", | ||||
|     "mathjax": "2.7.7", | ||||
|     "moment": "^2.29.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. | ||||
|  * | ||||
|  * @deprecated use CoreListItemsManager instead. | ||||
|  */ | ||||
| export abstract class CorePageItemsListManager<Item> { | ||||
| 
 | ||||
|  | ||||
| @ -50,6 +50,7 @@ import { CoreShowPasswordComponent } from './show-password/show-password'; | ||||
| import { CoreSitePickerComponent } from './site-picker/site-picker'; | ||||
| import { CoreSplitViewComponent } from './split-view/split-view'; | ||||
| import { CoreStyleComponent } from './style/style'; | ||||
| import { CoreSwipeNavigationComponent } from './swipe-navigation/swipe-navigation'; | ||||
| import { CoreTabComponent } from './tabs/tab'; | ||||
| import { CoreTabsComponent } from './tabs/tabs'; | ||||
| import { CoreTabsOutletComponent } from './tabs-outlet/tabs-outlet'; | ||||
| @ -92,6 +93,7 @@ import { CoreButtonWithSpinnerComponent } from './button-with-spinner/button-wit | ||||
|         CoreSitePickerComponent, | ||||
|         CoreSplitViewComponent, | ||||
|         CoreStyleComponent, | ||||
|         CoreSwipeNavigationComponent, | ||||
|         CoreTabComponent, | ||||
|         CoreTabsComponent, | ||||
|         CoreTabsOutletComponent, | ||||
| @ -140,6 +142,7 @@ import { CoreButtonWithSpinnerComponent } from './button-with-spinner/button-wit | ||||
|         CoreSitePickerComponent, | ||||
|         CoreSplitViewComponent, | ||||
|         CoreStyleComponent, | ||||
|         CoreSwipeNavigationComponent, | ||||
|         CoreTabComponent, | ||||
|         CoreTabsComponent, | ||||
|         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 { ApplicationInitStatus, Injector, NgModule, Type } from '@angular/core'; | ||||
| import { HammerModule, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; | ||||
| 
 | ||||
| import { CoreApplicationInitStatus } from './classes/application-init-status'; | ||||
| import { CoreFeaturesModule } from './features/features.module'; | ||||
| import { CoreHammerGestureConfig } from './classes/hammer-gesture-config'; | ||||
| import { CoreInterceptor } from './classes/interceptor'; | ||||
| import { getDatabaseProviders } from './services/database'; | ||||
| import { getInitializerProviders } from './initializers'; | ||||
| @ -84,9 +86,11 @@ export const CORE_SERVICES: Type<unknown>[] = [ | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         CoreFeaturesModule, | ||||
|         HammerModule, | ||||
|     ], | ||||
|     providers: [ | ||||
|         { provide: HTTP_INTERCEPTORS, useClass: CoreInterceptor, multi: true }, | ||||
|         { provide: HAMMER_GESTURE_CONFIG, useClass: CoreHammerGestureConfig }, | ||||
|         { provide: ApplicationInitStatus, useClass: CoreApplicationInitStatus, deps: [Injector] }, | ||||
|         ...getDatabaseProviders(), | ||||
|         ...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.
 | ||||
| 
 | ||||
| import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; | ||||
| import { Params } from '@angular/router'; | ||||
| import { IonRefresher } from '@ionic/angular'; | ||||
| 
 | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CorePageItemsListManager } from '@classes/page-items-list-manager'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; | ||||
| 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 { 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. | ||||
| @ -33,6 +35,7 @@ import { CoreUtils } from '@services/utils/utils'; | ||||
| }) | ||||
| export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestroy { | ||||
| 
 | ||||
|     courseId!: number; | ||||
|     participants!: CoreUserParticipantsManager; | ||||
|     searchQuery: string | null = null; | ||||
|     searchInProgress = false; | ||||
| @ -43,10 +46,12 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro | ||||
|     @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; | ||||
| 
 | ||||
|     constructor() { | ||||
|         let courseId: number; | ||||
| 
 | ||||
|         try { | ||||
|             courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); | ||||
|             this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); | ||||
|             this.participants = new CoreUserParticipantsManager( | ||||
|                 CoreItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId]), | ||||
|                 this, | ||||
|             ); | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.showErrorModal(error); | ||||
| 
 | ||||
| @ -55,7 +60,6 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.participants = new CoreUserParticipantsManager(CoreUserParticipantsPage, courseId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -104,9 +108,11 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const newSource = CoreItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId]); | ||||
| 
 | ||||
|         this.searchQuery = null; | ||||
|         this.searchInProgress = false; | ||||
|         this.participants.resetItems(); | ||||
|         this.participants.setSource(newSource); | ||||
| 
 | ||||
|         await this.fetchInitialParticipants(); | ||||
|     } | ||||
| @ -119,9 +125,11 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro | ||||
|     async search(query: string): Promise<void> { | ||||
|         CoreApp.closeKeyboard(); | ||||
| 
 | ||||
|         const newSource = CoreItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId, query]); | ||||
| 
 | ||||
|         this.searchInProgress = true; | ||||
|         this.searchQuery = query; | ||||
|         this.participants.resetItems(); | ||||
|         this.participants.setSource(newSource); | ||||
| 
 | ||||
|         await this.fetchInitialParticipants(); | ||||
| 
 | ||||
| @ -134,8 +142,8 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro | ||||
|      * @param refresher Refresher. | ||||
|      */ | ||||
|     async refreshParticipants(refresher: IonRefresher): Promise<void> { | ||||
|         await CoreUtils.ignoreErrors(CoreUser.invalidateParticipantsList(this.participants.courseId)); | ||||
|         await CoreUtils.ignoreErrors(this.fetchParticipants()); | ||||
|         await CoreUtils.ignoreErrors(CoreUser.invalidateParticipantsList(this.courseId)); | ||||
|         await CoreUtils.ignoreErrors(this.fetchParticipants(true)); | ||||
| 
 | ||||
|         refresher?.complete(); | ||||
|     } | ||||
| @ -147,7 +155,7 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro | ||||
|      */ | ||||
|     async fetchMoreParticipants(complete: () => void): Promise<void> { | ||||
|         try { | ||||
|             await this.fetchParticipants(this.participants.items); | ||||
|             await this.fetchParticipants(false); | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.showErrorModalDefault(error, 'Error loading more participants'); | ||||
| 
 | ||||
| @ -162,38 +170,23 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro | ||||
|      */ | ||||
|     private async fetchInitialParticipants(): Promise<void> { | ||||
|         try { | ||||
|             await this.fetchParticipants(); | ||||
|             await this.fetchParticipants(true); | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.showErrorModalDefault(error, 'Error loading participants'); | ||||
| 
 | ||||
|             this.participants.setItems([]); | ||||
|             this.participants.reset(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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> { | ||||
|         if (this.searchQuery) { | ||||
|             const { participants, canLoadMore } = await CoreUser.searchParticipants( | ||||
|                 this.participants.courseId, | ||||
|                 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); | ||||
|         } | ||||
|     private async fetchParticipants(reload: boolean): Promise<void> { | ||||
|         reload | ||||
|             ? await this.participants.reload() | ||||
|             : await this.participants.loadNextPage(); | ||||
| 
 | ||||
|         this.fetchMoreParticipantsFailed = false; | ||||
|     } | ||||
| @ -203,30 +196,14 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro | ||||
| /** | ||||
|  * 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) { | ||||
|         super(pageComponent); | ||||
|     constructor(source: CoreUserParticipantsSource, page: CoreUserParticipantsPage) { | ||||
|         super(source, CoreUserParticipantsPage); | ||||
| 
 | ||||
|         this.courseId = courseId; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @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); | ||||
|         this.page = page; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -236,11 +213,18 @@ class CoreUserParticipantsManager extends CorePageItemsListManager<CoreUserParti | ||||
|         return participant.id.toString(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected getItemQueryParams(): Params { | ||||
|         return { search: this.page.searchQuery }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async logActivity(): Promise<void> { | ||||
|         await CoreUser.logParticipantsView(this.courseId); | ||||
|         await CoreUser.logParticipantsView(this.page.courseId); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -7,6 +7,7 @@ | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <core-swipe-navigation [manager]="users"> | ||||
|         <ion-refresher slot="fixed" [disabled]="!userLoaded" (ionRefresh)="refreshUser($event.target)"> | ||||
|             <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|         </ion-refresher> | ||||
| @ -43,8 +44,6 @@ | ||||
|                         <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> | ||||
| @ -57,7 +56,6 @@ | ||||
|                         <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"> | ||||
| @ -74,7 +72,6 @@ | ||||
|                     <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" | ||||
| @ -87,10 +84,10 @@ | ||||
|                     </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> | ||||
|     </core-swipe-navigation> | ||||
| </ion-content> | ||||
|  | ||||
| @ -12,6 +12,7 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { ActivatedRoute, ActivatedRouteSnapshot, Params } from '@angular/router'; | ||||
| import { Component, OnDestroy, OnInit } from '@angular/core'; | ||||
| import { IonRefresher } from '@ionic/angular'; | ||||
| import { Subscription } from 'rxjs'; | ||||
| @ -20,16 +21,16 @@ import { CoreSite } from '@classes/site'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||
| import { | ||||
|     CoreUser, | ||||
|     CoreUserProfile, | ||||
|     CoreUserProvider, | ||||
| } from '@features/user/services/user'; | ||||
| import { CoreUser, CoreUserBasicData, CoreUserProfile, CoreUserProvider } from '@features/user/services/user'; | ||||
| import { CoreUserHelper } from '@features/user/services/user-helper'; | ||||
| import { CoreUserDelegate, CoreUserDelegateService, CoreUserProfileHandlerData } from '@features/user/services/user-delegate'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| 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({ | ||||
|     selector: 'page-core-user-profile', | ||||
| @ -55,7 +56,10 @@ export class CoreUserProfilePage implements OnInit, OnDestroy { | ||||
|     newPageHandlers: CoreUserProfileHandlerData[] = []; | ||||
|     communicationHandlers: CoreUserProfileHandlerData[] = []; | ||||
| 
 | ||||
|     constructor() { | ||||
|     users?: CoreUserSwipeItemsManager; | ||||
|     usersQueryParams: Params = {}; | ||||
| 
 | ||||
|     constructor(private route: ActivatedRoute) { | ||||
|         this.obsProfileRefreshed = CoreEvents.on(CoreUserProvider.PROFILE_REFRESHED, (data) => { | ||||
|             if (!this.user || !data.user) { | ||||
|                 return; | ||||
| @ -86,6 +90,15 @@ export class CoreUserProfilePage implements OnInit, OnDestroy { | ||||
|             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 { | ||||
|             await this.fetchUser(); | ||||
| 
 | ||||
| @ -204,8 +217,49 @@ export class CoreUserProfilePage implements OnInit, OnDestroy { | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.users?.destroy(); | ||||
|         this.subscription?.unsubscribe(); | ||||
|         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; | ||||
|             } | ||||
|         } | ||||
|         core-loading .core-loading-content { | ||||
|             width: 100%; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -21,6 +21,7 @@ import { | ||||
| } from '@features/course/services/course-options-delegate'; | ||||
| import { CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses'; | ||||
| import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '@features/courses/services/courses-helper'; | ||||
| import { PARTICIPANTS_PAGE_NAME } from '@features/user/user.module'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { CoreUser } from '../user'; | ||||
| 
 | ||||
| @ -89,7 +90,7 @@ export class CoreUserCourseOptionHandlerService implements CoreCourseOptionsHand | ||||
|         return { | ||||
|             title: 'core.user.participants', | ||||
|             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 { CoreUserParticipantsPage } from './pages/participants/participants.page'; | ||||
| import { conditionalRoutes } from '@/app/app-routing.module'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: '', | ||||
|         component: CoreUserParticipantsPage, | ||||
|         children: [ | ||||
|         children: conditionalRoutes([ | ||||
|             { | ||||
|                 path: ':userId', | ||||
|                 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 { CoreUserOfflineProvider } from './services/user-offline'; | ||||
| 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>[] = [ | ||||
|     CoreUserDelegateService, | ||||
| @ -45,16 +49,27 @@ export const CORE_USER_SERVICES: Type<unknown>[] = [ | ||||
|     CoreUserSyncProvider, | ||||
| ]; | ||||
| 
 | ||||
| export const PARTICIPANTS_PAGE_NAME = 'participants'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: 'user', | ||||
|         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 = [ | ||||
|     { | ||||
|         path: 'participants', | ||||
|         path: PARTICIPANTS_PAGE_NAME, | ||||
|         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'> & { | ||||
|     params?: Params; | ||||
|     reset?: boolean; | ||||
|     replace?: boolean; | ||||
|     preferCurrentTab?: boolean; // Default true.
 | ||||
|     nextNavigation?: { | ||||
|         path: string; | ||||
| @ -137,6 +138,7 @@ export class CoreNavigatorService { | ||||
|             animationDirection: options.animationDirection, | ||||
|             queryParams: CoreObject.isEmpty(options.params ?? {}) ? null : CoreObject.withoutEmpty(options.params), | ||||
|             relativeTo: path.startsWith('/') ? null : this.getCurrentRoute(), | ||||
|             replaceUrl: options.replace, | ||||
|         }); | ||||
| 
 | ||||
|         // Remove objects from queryParams and replace them with an ID.
 | ||||
| @ -264,15 +266,17 @@ export class CoreNavigatorService { | ||||
|      * @return Value of the parameter, undefined if not found. | ||||
|      */ | ||||
|     protected getRouteSnapshotParam<T = unknown>(name: string, route?: ActivatedRoute): T | undefined { | ||||
|         if (!route?.snapshot) { | ||||
|         if (!route) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (route.snapshot) { | ||||
|             const value = route.snapshot.queryParams[name] ?? route.snapshot.params[name]; | ||||
| 
 | ||||
|             if (value !== undefined) { | ||||
|                 return value; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return this.getRouteSnapshotParam(name, route.parent || undefined); | ||||
|     } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user