commit
da6e3e8a20
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 },
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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(),
|
||||||
|
|
|
@ -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…
Reference in New Issue