MOBILE-3905 course: Swipe between participants
parent
df92c97802
commit
429aecf3aa
|
@ -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",
|
||||
|
@ -245,4 +246,4 @@
|
|||
"optionalDependencies": {
|
||||
"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.
|
||||
*
|
||||
* @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;
|
||||
}
|
|
@ -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(),
|
||||
|
|
|
@ -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,15 +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 { 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.
|
||||
|
@ -32,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;
|
||||
|
@ -42,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);
|
||||
|
||||
|
@ -54,7 +60,6 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
|
|||
return;
|
||||
}
|
||||
|
||||
this.participants = new CoreUserParticipantsManager(CoreUserParticipantsPage, courseId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -103,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();
|
||||
}
|
||||
|
@ -118,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();
|
||||
|
||||
|
@ -133,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();
|
||||
}
|
||||
|
@ -146,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');
|
||||
|
||||
|
@ -161,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;
|
||||
}
|
||||
|
@ -202,14 +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;
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -219,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,90 +7,87 @@
|
|||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!userLoaded" (ionRefresh)="refreshUser($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="userLoaded">
|
||||
<ion-list *ngIf="user && !isDeleted && isEnrolled">
|
||||
<ion-item class="ion-text-center core-user-profile-maininfo">
|
||||
<core-user-avatar [user]="user" [userId]="user.id" [linkProfile]="false" [checkOnline]="true">
|
||||
</core-user-avatar>
|
||||
<ion-label>
|
||||
<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">
|
||||
<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>
|
||||
<core-loading [hideUntil]="userLoaded">
|
||||
<ion-list *ngIf="user && !isDeleted && isEnrolled">
|
||||
<ion-item class="ion-text-center core-user-profile-maininfo">
|
||||
<core-user-avatar [user]="user" [userId]="user.id" [linkProfile]="false" [checkOnline]="true">
|
||||
</core-user-avatar>
|
||||
<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)"
|
||||
[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>
|
||||
<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)"
|
||||
[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-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%;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -60,6 +60,9 @@ const routes: Routes = [
|
|||
{
|
||||
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),
|
||||
];
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue