MOBILE-3320 course: Support nested navigation
parent
1e82b7796c
commit
c39d6cc8a5
|
@ -97,6 +97,33 @@ function buildConditionalUrlMatcher(pathOrMatcher: string | UrlMatcher, conditio
|
|||
};
|
||||
}
|
||||
|
||||
export function buildRegExpUrlMatcher(regexp: RegExp): UrlMatcher {
|
||||
return (segments: UrlSegment[]): UrlMatchResult | null => {
|
||||
// Ignore empty paths.
|
||||
if (segments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const path = segments.map(segment => segment.path).join('/');
|
||||
const match = regexp.exec(path)?.[0];
|
||||
|
||||
// Ignore paths that don't match the start of the url.
|
||||
if (!match || !path.startsWith(match)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Consume segments that match.
|
||||
const [consumed] = segments.slice(1).reduce(([consumed, path], segment) => path === match
|
||||
? [consumed, path]
|
||||
:[
|
||||
consumed.concat(segment),
|
||||
`${path}/${segment.path}`,
|
||||
], [[segments[0]] as UrlSegment[], segments[0].path]);
|
||||
|
||||
return { consumed };
|
||||
};
|
||||
}
|
||||
|
||||
export type ModuleRoutes = { children: Routes; siblings: Routes };
|
||||
export type ModuleRoutesConfig = Routes | Partial<ModuleRoutes>;
|
||||
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
// (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 { Route } from '@angular/compiler/src/core';
|
||||
import { UrlSegment, UrlSegmentGroup } from '@angular/router';
|
||||
|
||||
import { mock } from '@/testing/utils';
|
||||
|
||||
import { buildRegExpUrlMatcher } from '../app-routing.module';
|
||||
|
||||
describe('Routing utils', () => {
|
||||
|
||||
it('matches paths using a RegExp', () => {
|
||||
const matcher = buildRegExpUrlMatcher(/foo(\/bar)*/);
|
||||
const route = mock<Route>();
|
||||
const segmentGroup = mock<UrlSegmentGroup>();
|
||||
const toUrlSegment = (path: string) => new UrlSegment(path, {});
|
||||
const testMatcher = (path: string, consumedParts: string[] | null) =>
|
||||
expect(matcher(path.split('/').map(toUrlSegment), segmentGroup, route))
|
||||
.toEqual(
|
||||
consumedParts
|
||||
? { consumed: consumedParts.map(toUrlSegment) }
|
||||
: null,
|
||||
);
|
||||
|
||||
testMatcher('baz/foo/bar', null);
|
||||
testMatcher('foo', ['foo']);
|
||||
testMatcher('foo/baz', ['foo']);
|
||||
testMatcher('foo/bar/bar/baz', ['foo', 'bar', 'bar']);
|
||||
});
|
||||
|
||||
});
|
|
@ -40,6 +40,7 @@ import { CoreCourseOptionsDelegateService } from './services/course-options-dele
|
|||
import { CoreCourseOfflineProvider } from './services/course-offline';
|
||||
import { CoreCourseSyncProvider } from './services/sync';
|
||||
import { COURSE_INDEX_PATH } from '@features/course/course-lazy.module';
|
||||
import { buildRegExpUrlMatcher } from '@/app/app-routing.module';
|
||||
|
||||
export const CORE_COURSE_SERVICES: Type<unknown>[] = [
|
||||
CoreCourseProvider,
|
||||
|
@ -59,7 +60,7 @@ export const COURSE_CONTENTS_PATH = `${COURSE_PAGE_NAME}/${COURSE_INDEX_PATH}/${
|
|||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: COURSE_PAGE_NAME,
|
||||
matcher: buildRegExpUrlMatcher(new RegExp(`^${COURSE_PAGE_NAME}(/deep)*`)),
|
||||
loadChildren: () => import('@features/course/course-lazy.module').then(m => m.CoreCourseLazyModule),
|
||||
},
|
||||
];
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Component, ViewChild, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Params } from '@angular/router';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
|
||||
import { CoreTabsOutletTab, CoreTabsOutletComponent } from '@components/tabs-outlet/tabs-outlet';
|
||||
import { CoreCourseFormatDelegate } from '../../services/format-delegate';
|
||||
|
@ -54,7 +54,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
|||
pageParams: {},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
constructor(private route: ActivatedRoute) {
|
||||
this.selectTabObserver = CoreEvents.on(CoreEvents.SELECT_COURSE_TAB, (data) => {
|
||||
if (!data.name) {
|
||||
// If needed, set sectionId and sectionNumber. They'll only be used if the content tabs hasn't been loaded yet.
|
||||
|
@ -81,6 +81,11 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
|||
* Component being initialized.
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
// Increase route depth.
|
||||
const path = CoreNavigator.getRouteFullPath(this.route.snapshot);
|
||||
|
||||
CoreNavigator.increaseRouteDepth(path.replace(/(\/deep)+/, ''));
|
||||
|
||||
// Get params.
|
||||
this.course = CoreNavigator.getRouteParam('course');
|
||||
this.firstTabName = CoreNavigator.getRouteParam('selectedTab');
|
||||
|
@ -180,6 +185,9 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
|||
* Page destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
const path = CoreNavigator.getRouteFullPath(this.route.snapshot);
|
||||
|
||||
CoreNavigator.decreaseRouteDepth(path.replace(/(\/deep)+/, ''));
|
||||
this.selectTabObserver?.off();
|
||||
}
|
||||
|
||||
|
|
|
@ -177,7 +177,11 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
|
|||
Object.assign(params, { course: course });
|
||||
|
||||
// Don't return the .push promise, we don't want to display a loading modal during the page transition.
|
||||
CoreNavigator.navigateToSitePath(`course/${course.id}`, { params });
|
||||
const currentTab = CoreNavigator.getCurrentMainMenuTab();
|
||||
const routeDepth = CoreNavigator.getRouteDepth(`/main/${currentTab}/course/${course.id}`);
|
||||
const deepPath = '/deep'.repeat(routeDepth);
|
||||
|
||||
CoreNavigator.navigateToSitePath(`course${deepPath}/${course.id}`, { params });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
import { ActivatedRoute, ActivatedRouteSnapshot, Params } from '@angular/router';
|
||||
|
||||
import { NavigationOptions } from '@ionic/angular/providers/nav-controller';
|
||||
|
||||
|
@ -71,6 +71,7 @@ export type CoreNavigatorCurrentRouteOptions = Partial<{
|
|||
@Injectable({ providedIn: 'root' })
|
||||
export class CoreNavigatorService {
|
||||
|
||||
protected routesDepth: Record<string, number> = {};
|
||||
protected storedParams: Record<number, unknown> = {};
|
||||
protected lastParamId = 0;
|
||||
|
||||
|
@ -397,6 +398,38 @@ export class CoreNavigatorService {
|
|||
return pageComponent || routeData ? null : route;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increase the number of times a route is repeated on the navigation stack.
|
||||
*
|
||||
* @param path Absolute route path.
|
||||
*/
|
||||
increaseRouteDepth(path: string): void {
|
||||
this.routesDepth[path] = this.getRouteDepth(path) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrease the number of times a route is repeated on the navigation stack.
|
||||
*
|
||||
* @param path Absolute route path.
|
||||
*/
|
||||
decreaseRouteDepth(path: string): void {
|
||||
if (this.getRouteDepth(path) <= 1) {
|
||||
delete this.routesDepth[path];
|
||||
} else {
|
||||
this.routesDepth[path]--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of times a route is repeated on the navigation stack.
|
||||
*
|
||||
* @param path Absolute route path.
|
||||
* @return Route depth.
|
||||
*/
|
||||
getRouteDepth(path: string): number {
|
||||
return this.routesDepth[path] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a path within the main menu.
|
||||
* If the path belongs to a visible tab, that tab will be selected.
|
||||
|
@ -493,16 +526,16 @@ export class CoreNavigatorService {
|
|||
/**
|
||||
* Get the full path of a certain route, including parent routes paths.
|
||||
*
|
||||
* @param route Route.
|
||||
* @param route Route snapshot.
|
||||
* @return Path.
|
||||
*/
|
||||
getRouteFullPath(route: ActivatedRoute | null): string {
|
||||
getRouteFullPath(route: ActivatedRouteSnapshot | null): string {
|
||||
if (!route) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const parentPath = this.getRouteFullPath(route.parent);
|
||||
const routePath = route.snapshot.url.join('/');
|
||||
const routePath = route.url.join('/');
|
||||
|
||||
if (!parentPath && !routePath) {
|
||||
return '';
|
||||
|
|
Loading…
Reference in New Issue