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.
|
||||
|
||||
import { AfterViewInit, Component, ElementRef, HostBinding, Input, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot } from '@angular/router';
|
||||
import { IonRouterOutlet } from '@ionic/angular';
|
||||
import { CoreScreen } from '@services/screen';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||
|
||||
enum CoreSplitViewMode {
|
||||
MenuOnly = 'menu-only', // Hides content.
|
||||
|
@ -35,18 +36,33 @@ export class CoreSplitViewComponent implements AfterViewInit, OnDestroy {
|
|||
@Input() placeholderText = 'core.emptysplit';
|
||||
isNested = false;
|
||||
|
||||
private outletRouteSubject: BehaviorSubject<ActivatedRouteSnapshot | null> = new BehaviorSubject(null);
|
||||
private subscriptions?: Subscription[];
|
||||
|
||||
constructor(private element: ElementRef<HTMLElement>) {}
|
||||
|
||||
get outletRoute(): ActivatedRouteSnapshot | null {
|
||||
return this.outletRouteSubject.value;
|
||||
}
|
||||
|
||||
get outletRouteObservable(): Observable<ActivatedRouteSnapshot | null> {
|
||||
return this.outletRouteSubject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngAfterViewInit(): void {
|
||||
this.isNested = !!this.element.nativeElement.parentElement?.closest('core-split-view');
|
||||
this.subscriptions = [
|
||||
this.outlet.activateEvents.subscribe(() => this.updateClasses()),
|
||||
this.outlet.deactivateEvents.subscribe(() => this.updateClasses()),
|
||||
this.outlet.activateEvents.subscribe(() => {
|
||||
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()),
|
||||
];
|
||||
|
||||
|
|
|
@ -11,11 +11,11 @@
|
|||
<core-split-view>
|
||||
<ion-list>
|
||||
<ion-item
|
||||
*ngFor="let section of sections"
|
||||
[class.core-selected-item]="section.name === activeSection"
|
||||
*ngFor="let section of sections.items"
|
||||
[class.core-selected-item]="sections.isSelected(section)"
|
||||
button
|
||||
detail
|
||||
(click)="openSection(section)"
|
||||
(click)="sections.select(section)"
|
||||
>
|
||||
<ion-icon [name]="section.icon" slot="start"></ion-icon>
|
||||
<ion-label>{{ 'core.settings.' + section.name | translate }}</ion-label>
|
||||
|
|
|
@ -12,83 +12,57 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreScreen } from '@services/screen';
|
||||
import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core';
|
||||
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({
|
||||
selector: 'page-core-settings-index',
|
||||
templateUrl: 'index.html',
|
||||
})
|
||||
export class CoreSettingsIndexPage implements OnInit, OnDestroy {
|
||||
export class CoreSettingsIndexPage implements AfterViewInit, OnDestroy {
|
||||
|
||||
sections = CoreSettingsConstants.SECTIONS;
|
||||
activeSection?: string;
|
||||
layoutSubscription?: Subscription;
|
||||
sections: CoreSettingsSectionsManager = new CoreSettingsSectionsManager(CoreSettingsIndexPage);
|
||||
|
||||
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.layoutSubscription = CoreScreen.instance.layoutObservable.subscribe(() => this.updateActiveSection());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ionViewWillEnter(): void {
|
||||
this.updateActiveSection();
|
||||
ngAfterViewInit(): void {
|
||||
this.sections.setItems(CoreSettingsConstants.SECTIONS);
|
||||
this.sections.watchSplitViewOutlet(this.splitView);
|
||||
this.sections.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.layoutSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a section page.
|
||||
*
|
||||
* @param section Section to open.
|
||||
*/
|
||||
openSection(section: CoreSettingsSection): void {
|
||||
const path = this.activeSection ? `../${section.path}` : section.path;
|
||||
|
||||
CoreNavigator.instance.navigate(path);
|
||||
|
||||
this.updateActiveSection(section.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update active section.
|
||||
*
|
||||
* @param activeSection Active section.
|
||||
*/
|
||||
private updateActiveSection(activeSection?: string): void {
|
||||
if (CoreScreen.instance.isMobile) {
|
||||
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;
|
||||
this.sections.destroy();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to manage sections.
|
||||
*/
|
||||
class CoreSettingsSectionsManager extends CorePageItemsListManager<CoreSettingsSection> {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected getItemPath(section: CoreSettingsSection): string {
|
||||
return section.path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected getSelectedItemPath(route: ActivatedRouteSnapshot): string | null {
|
||||
return route.parent?.routeConfig?.path ?? null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -50,6 +50,14 @@ export type CoreNavigationOptions = {
|
|||
reset?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for CoreNavigatorService#getCurrentRoute method.
|
||||
*/
|
||||
type GetCurrentRouteOptions = Partial<{
|
||||
parentRoute: ActivatedRoute;
|
||||
pageComponent: unknown;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Service to provide some helper functions regarding navigation.
|
||||
*/
|
||||
|
@ -310,13 +318,26 @@ export class CoreNavigatorService {
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
protected getCurrentRoute(route?: ActivatedRoute): ActivatedRoute {
|
||||
route = route ?? Router.instance.routerState.root;
|
||||
getCurrentRoute(): ActivatedRoute;
|
||||
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