Extract list items management from settings
parent
6f54e0eb06
commit
98910cf465
|
@ -0,0 +1,202 @@
|
||||||
|
// (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 { ActivatedRouteSnapshot, Params } 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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class to manage the state and routing of a list of items in a page, for example on pages using a split view.
|
||||||
|
*/
|
||||||
|
export abstract class CorePageItemsListManager<Item> {
|
||||||
|
|
||||||
|
protected itemsList: Item[] | null = null;
|
||||||
|
protected itemsMap: Record<string, Item> | null = null;
|
||||||
|
protected selectedItem: Item | null = null;
|
||||||
|
protected pageComponent: unknown;
|
||||||
|
protected splitView?: CoreSplitViewComponent;
|
||||||
|
protected splitViewOutletSubscription?: Subscription;
|
||||||
|
|
||||||
|
constructor(pageComponent: unknown) {
|
||||||
|
this.pageComponent = pageComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
get items(): Item[] {
|
||||||
|
return this.itemsList || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get loaded(): boolean {
|
||||||
|
return this.itemsMap !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get empty(): boolean {
|
||||||
|
return this.itemsList === null || this.itemsList.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process page started operations.
|
||||||
|
*/
|
||||||
|
async start(): Promise<void> {
|
||||||
|
// Calculate current selected item.
|
||||||
|
const route = CoreNavigator.instance.getCurrentRoute({ pageComponent: this.pageComponent });
|
||||||
|
if (route !== null && route.firstChild) {
|
||||||
|
this.updateSelectedItem(route.firstChild.snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select default item if none is selected on a non-mobile layout.
|
||||||
|
if (!CoreScreen.instance.isMobile && this.selectedItem === null) {
|
||||||
|
const defaultItem = this.getDefaultItem();
|
||||||
|
|
||||||
|
if (defaultItem) {
|
||||||
|
this.select(defaultItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log activity.
|
||||||
|
await CoreUtils.instance.ignoreErrors(this.logActivity());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process page destroyed operations.
|
||||||
|
*/
|
||||||
|
destroy(): void {
|
||||||
|
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(route));
|
||||||
|
|
||||||
|
this.updateSelectedItem(splitView.outletRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @todo Implement watchResize.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select an item.
|
||||||
|
*
|
||||||
|
* @param item Item.
|
||||||
|
*/
|
||||||
|
async select(item: Item): Promise<void> {
|
||||||
|
// Get current route in the page.
|
||||||
|
const route = CoreNavigator.instance.getCurrentRoute({ pageComponent: this.pageComponent });
|
||||||
|
|
||||||
|
if (route === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this item is already selected, do nothing.
|
||||||
|
const itemPath = this.getItemPath(item);
|
||||||
|
|
||||||
|
if (route.firstChild?.routeConfig?.path === itemPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to item.
|
||||||
|
const path = route.firstChild ? `../${itemPath}` : itemPath;
|
||||||
|
const params = this.getItemQueryParams(item);
|
||||||
|
|
||||||
|
await CoreNavigator.instance.navigate(path, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the list of items.
|
||||||
|
*
|
||||||
|
* @param items Items.
|
||||||
|
*/
|
||||||
|
setItems(items: Item[]): void {
|
||||||
|
this.itemsList = items.slice(0);
|
||||||
|
this.itemsMap = items.reduce((map, item) => {
|
||||||
|
map[this.getItemPath(item)] = item;
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
this.updateSelectedItem(this.splitView?.outletRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log activity when the page starts.
|
||||||
|
*/
|
||||||
|
protected async logActivity(): Promise<void> {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the selected item given the current route.
|
||||||
|
*
|
||||||
|
* @param route Current route.
|
||||||
|
*/
|
||||||
|
protected updateSelectedItem(route?: ActivatedRouteSnapshot | null): void {
|
||||||
|
const selectedItemPath = route ? this.getSelectedItemPath(route) : null;
|
||||||
|
|
||||||
|
this.selectedItem = selectedItemPath
|
||||||
|
? this.itemsMap?.[selectedItemPath] ?? null
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the item that should be selected by default.
|
||||||
|
*/
|
||||||
|
protected getDefaultItem(): Item | null {
|
||||||
|
return this.itemsList?.[0] || 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 {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 Current route.
|
||||||
|
* @return Path of the selected item in the given route.
|
||||||
|
*/
|
||||||
|
protected abstract getSelectedItemPath(route: ActivatedRouteSnapshot): string | null;
|
||||||
|
|
||||||
|
}
|
|
@ -13,9 +13,10 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { AfterViewInit, Component, ElementRef, HostBinding, Input, OnDestroy, ViewChild } from '@angular/core';
|
import { AfterViewInit, Component, ElementRef, HostBinding, Input, OnDestroy, ViewChild } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot } from '@angular/router';
|
||||||
import { IonRouterOutlet } from '@ionic/angular';
|
import { IonRouterOutlet } from '@ionic/angular';
|
||||||
import { CoreScreen } from '@services/screen';
|
import { CoreScreen } from '@services/screen';
|
||||||
import { Subscription } from 'rxjs';
|
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||||
|
|
||||||
enum CoreSplitViewMode {
|
enum CoreSplitViewMode {
|
||||||
MenuOnly = 'menu-only', // Hides content.
|
MenuOnly = 'menu-only', // Hides content.
|
||||||
|
@ -35,18 +36,33 @@ export class CoreSplitViewComponent implements AfterViewInit, OnDestroy {
|
||||||
@Input() placeholderText = 'core.emptysplit';
|
@Input() placeholderText = 'core.emptysplit';
|
||||||
isNested = false;
|
isNested = false;
|
||||||
|
|
||||||
|
private outletRouteSubject: BehaviorSubject<ActivatedRouteSnapshot | null> = new BehaviorSubject(null);
|
||||||
private subscriptions?: Subscription[];
|
private subscriptions?: Subscription[];
|
||||||
|
|
||||||
constructor(private element: ElementRef<HTMLElement>) {}
|
constructor(private element: ElementRef<HTMLElement>) {}
|
||||||
|
|
||||||
|
get outletRoute(): ActivatedRouteSnapshot | null {
|
||||||
|
return this.outletRouteSubject.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get outletRouteObservable(): Observable<ActivatedRouteSnapshot | null> {
|
||||||
|
return this.outletRouteSubject.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
this.isNested = !!this.element.nativeElement.parentElement?.closest('core-split-view');
|
this.isNested = !!this.element.nativeElement.parentElement?.closest('core-split-view');
|
||||||
this.subscriptions = [
|
this.subscriptions = [
|
||||||
this.outlet.activateEvents.subscribe(() => this.updateClasses()),
|
this.outlet.activateEvents.subscribe(() => {
|
||||||
this.outlet.deactivateEvents.subscribe(() => this.updateClasses()),
|
this.updateClasses();
|
||||||
|
this.outletRouteSubject.next(this.outlet.activatedRoute.snapshot);
|
||||||
|
}),
|
||||||
|
this.outlet.deactivateEvents.subscribe(() => {
|
||||||
|
this.updateClasses();
|
||||||
|
this.outletRouteSubject.next(null);
|
||||||
|
}),
|
||||||
CoreScreen.instance.layoutObservable.subscribe(() => this.updateClasses()),
|
CoreScreen.instance.layoutObservable.subscribe(() => this.updateClasses()),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -11,11 +11,11 @@
|
||||||
<core-split-view>
|
<core-split-view>
|
||||||
<ion-list>
|
<ion-list>
|
||||||
<ion-item
|
<ion-item
|
||||||
*ngFor="let section of sections"
|
*ngFor="let section of sections.items"
|
||||||
[class.core-selected-item]="section.name === activeSection"
|
[class.core-selected-item]="sections.isSelected(section)"
|
||||||
button
|
button
|
||||||
detail
|
detail
|
||||||
(click)="openSection(section)"
|
(click)="sections.select(section)"
|
||||||
>
|
>
|
||||||
<ion-icon [name]="section.icon" slot="start"></ion-icon>
|
<ion-icon [name]="section.icon" slot="start"></ion-icon>
|
||||||
<ion-label>{{ 'core.settings.' + section.name | translate }}</ion-label>
|
<ion-label>{{ 'core.settings.' + section.name | translate }}</ion-label>
|
||||||
|
|
|
@ -12,83 +12,57 @@
|
||||||
// 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 { Component, OnDestroy, OnInit } from '@angular/core';
|
import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core';
|
||||||
import { Subscription } from 'rxjs';
|
|
||||||
|
|
||||||
import { CoreNavigator } from '@services/navigator';
|
|
||||||
import { CoreScreen } from '@services/screen';
|
|
||||||
import { CoreSettingsConstants, CoreSettingsSection } from '@features/settings/constants';
|
import { CoreSettingsConstants, CoreSettingsSection } from '@features/settings/constants';
|
||||||
|
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
|
||||||
|
import { ActivatedRouteSnapshot } from '@angular/router';
|
||||||
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'page-core-settings-index',
|
selector: 'page-core-settings-index',
|
||||||
templateUrl: 'index.html',
|
templateUrl: 'index.html',
|
||||||
})
|
})
|
||||||
export class CoreSettingsIndexPage implements OnInit, OnDestroy {
|
export class CoreSettingsIndexPage implements AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
sections = CoreSettingsConstants.SECTIONS;
|
sections: CoreSettingsSectionsManager = new CoreSettingsSectionsManager(CoreSettingsIndexPage);
|
||||||
activeSection?: string;
|
|
||||||
layoutSubscription?: Subscription;
|
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngAfterViewInit(): void {
|
||||||
this.layoutSubscription = CoreScreen.instance.layoutObservable.subscribe(() => this.updateActiveSection());
|
this.sections.setItems(CoreSettingsConstants.SECTIONS);
|
||||||
}
|
this.sections.watchSplitViewOutlet(this.splitView);
|
||||||
|
this.sections.start();
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
ionViewWillEnter(): void {
|
|
||||||
this.updateActiveSection();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.layoutSubscription?.unsubscribe();
|
this.sections.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open a section page.
|
* Helper class to manage sections.
|
||||||
*
|
|
||||||
* @param section Section to open.
|
|
||||||
*/
|
*/
|
||||||
openSection(section: CoreSettingsSection): void {
|
class CoreSettingsSectionsManager extends CorePageItemsListManager<CoreSettingsSection> {
|
||||||
const path = this.activeSection ? `../${section.path}` : section.path;
|
|
||||||
|
|
||||||
CoreNavigator.instance.navigate(path);
|
/**
|
||||||
|
* @inheritdoc
|
||||||
this.updateActiveSection(section.name);
|
*/
|
||||||
|
protected getItemPath(section: CoreSettingsSection): string {
|
||||||
|
return section.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update active section.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @param activeSection Active section.
|
|
||||||
*/
|
*/
|
||||||
private updateActiveSection(activeSection?: string): void {
|
protected getSelectedItemPath(route: ActivatedRouteSnapshot): string | null {
|
||||||
if (CoreScreen.instance.isMobile) {
|
return route.parent?.routeConfig?.path ?? null;
|
||||||
delete this.activeSection;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.activeSection = activeSection ?? this.guessActiveSection();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Guess active section looking at the current route.
|
|
||||||
*
|
|
||||||
* @return Active section.
|
|
||||||
*/
|
|
||||||
private guessActiveSection(): string | undefined {
|
|
||||||
const activeSection = this.sections.find(
|
|
||||||
section => CoreNavigator.instance.isCurrent(`**/settings/${section.path}`),
|
|
||||||
);
|
|
||||||
|
|
||||||
return activeSection?.name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,14 @@ export type CoreNavigationOptions = {
|
||||||
reset?: boolean;
|
reset?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for CoreNavigatorService#getCurrentRoute method.
|
||||||
|
*/
|
||||||
|
type GetCurrentRouteOptions = Partial<{
|
||||||
|
parentRoute: ActivatedRoute;
|
||||||
|
pageComponent: unknown;
|
||||||
|
}>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to provide some helper functions regarding navigation.
|
* Service to provide some helper functions regarding navigation.
|
||||||
*/
|
*/
|
||||||
|
@ -310,13 +318,26 @@ export class CoreNavigatorService {
|
||||||
/**
|
/**
|
||||||
* Get current activated route.
|
* Get current activated route.
|
||||||
*
|
*
|
||||||
* @param route Parent route.
|
* @param options
|
||||||
|
* - parent: Parent route, if this isn't provided the current active route will be used.
|
||||||
|
* - pageComponent: Page component of the route to find, if this isn't provided the deepest route in the hierarchy
|
||||||
|
* will be returned.
|
||||||
* @return Current activated route.
|
* @return Current activated route.
|
||||||
*/
|
*/
|
||||||
protected getCurrentRoute(route?: ActivatedRoute): ActivatedRoute {
|
getCurrentRoute(): ActivatedRoute;
|
||||||
route = route ?? Router.instance.routerState.root;
|
getCurrentRoute(options: GetCurrentRouteOptions): ActivatedRoute | null;
|
||||||
|
getCurrentRoute({ parentRoute, pageComponent }: GetCurrentRouteOptions = {}): ActivatedRoute | null {
|
||||||
|
parentRoute = parentRoute ?? Router.instance.routerState.root;
|
||||||
|
|
||||||
return route.firstChild ? this.getCurrentRoute(route.firstChild) : route;
|
if (pageComponent && parentRoute.component === pageComponent) {
|
||||||
|
return parentRoute;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentRoute.firstChild) {
|
||||||
|
return this.getCurrentRoute({ parentRoute: parentRoute.firstChild, pageComponent });
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageComponent ? null : parentRoute;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue