@nent/core
Version:
374 lines (373 loc) • 14.3 kB
JavaScript
/*!
* NENT 2022
*/
import { activateActionActivators } from '../../../services/actions/elements';
import { ActionActivationStrategy } from '../../../services/actions/interfaces';
import { logIf } from '../../../services/common/logging';
import { commonState } from '../../../services/common/state';
import { resolveChildElementXAttributes } from '../../../services/data/elements';
import { hasToken, resolveTokens, } from '../../../services/data/tokens';
import { getChildInputValidity } from '../../n-view-prompt/services/elements';
import { ROUTE_EVENTS, } from '../../n-views/services/interfaces';
import { getPossibleParentPaths, isAbsolute, locationsAreEqual, matchesAreEqual, } from '../../n-views/services/utils';
/* It's a wrapper around a route element that provides a bunch of methods for interacting with the
route */
export class Route {
/**
* It creates a new route, adds it to the router, and then sets up the event listeners for the route
* @param {RouterService} router - RouterService
* @param {HTMLElement} routeElement - HTMLElement - the element that will be used to determine if
* the route is active.
* @param {string} path - The path to match against.
* @param {Route | null} [parentRoute=null] - The parent route of this route.
* @param {boolean} [exact=true] - boolean = true,
* @param {PageData} pageData - PageData = {}
* @param {string | null} [transition=null] - string | null = null,
* @param {number} [scrollTopOffset=0] - number = 0,
* @param matchSetter - (m: MatchResults | null) => void = () => {},
* @param [routeDestroy] - (self: Route) => void
*/
constructor(router, routeElement, path, parentRoute = null, exact = true, pageData = {}, transition = null, scrollTopOffset = 0, matchSetter = () => { }, routeDestroy) {
var _b;
this.router = router;
this.routeElement = routeElement;
this.path = path;
this.parentRoute = parentRoute;
this.exact = exact;
this.pageData = pageData;
this.transition = transition;
this.scrollTopOffset = scrollTopOffset;
this.routeDestroy = routeDestroy;
this.completed = false;
this.match = null;
this.scrollOnNextRender = false;
this.previousMatch = null;
this.childRoutes = [];
this.router.routes.push(this);
(_b = this.parentRoute) === null || _b === void 0 ? void 0 : _b.addChildRoute(this);
this.onStartedSubscription = router.eventBus.on(ROUTE_EVENTS.RouteChangeStart, async (location) => {
logIf(commonState.debug, `route: ${this.path} started -> ${location.pathname} `);
this.previousMatch = this.match;
if (!locationsAreEqual(this.router.location, location)) {
await this.activateActions(ActionActivationStrategy.OnExit);
this.completed = false;
}
});
const evaluateRoute = (sendEvent = true) => {
logIf(commonState.debug, `route: ${this.path} changed -> ${location.pathname}`);
this.match = router.matchPath({
path: this.path,
exact: this.exact,
strict: true,
}, this, sendEvent);
matchSetter(this.match);
this.adjustClasses();
};
this.onChangedSubscription = router.eventBus.on(ROUTE_EVENTS.RouteChanged, () => {
evaluateRoute();
});
evaluateRoute();
}
/**
* It takes a route, adds it to the childRoutes array, and then sorts the array so that the routes
* are in the same order as they appear in the DOM
* @param {Route} route - Route - The route to add to the child routes
*/
addChildRoute(route) {
this.childRoutes = [...this.childRoutes, route].sort((a, b) => a.routeElement.compareDocumentPosition(b.routeElement) &
Node.DOCUMENT_POSITION_FOLLOWING
? -1
: 1);
}
/**
* If the childUrl is absolute, return it. Otherwise, return the childUrl normalized by the router
* @param {string} childUrl - The URL to normalize.
* @returns The normalized child url.
*/
normalizeChildUrl(childUrl) {
if (isAbsolute(childUrl))
return childUrl;
return this.router.normalizeChildUrl(childUrl, this.path);
}
/**
* If the current match is not exact and the current match is not equal to the previous match, then
* return true
* @returns A boolean value.
*/
didExit() {
var _b;
return (!((_b = this.match) === null || _b === void 0 ? void 0 : _b.isExact) &&
!matchesAreEqual(this.match, this.previousMatch));
}
/**
* It returns an array of all the `n-action-activator` elements that are children of the `n-route`
* element
* @returns An array of HTMLNActionActivatorElement objects.
*/
get actionActivators() {
return Array.from(this.routeElement.querySelectorAll('n-action-activator')).filter(e => this.isChild(e));
}
/**
* It returns true if the element is a child of the route element
* @param {HTMLElement} element - HTMLElement - The element that is being clicked
* @returns - If the element is a child of the routeElement, it will return true.
* - If the element is not a child of the routeElement, it will return false.
*/
isChild(element) {
var _b;
const tag = this.routeElement.tagName.toLocaleLowerCase();
return (element.closest(tag) == this.routeElement ||
element.parentElement == this.routeElement ||
((_b = element.parentElement) === null || _b === void 0 ? void 0 : _b.closest(tag)) === this.routeElement);
}
/**
* If the route matches, then capture the inner links and resolve the HTML
*/
async loadCompleted() {
var _b;
if (this.match) {
this.captureInnerLinksAndResolveHtml();
// If this is an independent route and it matches then routes have updated.
if ((_b = this.match) === null || _b === void 0 ? void 0 : _b.isExact) {
this.routeElement
.querySelectorAll('[defer-load]')
.forEach((el) => {
el.removeAttribute('defer-load');
});
await this.activateActions(ActionActivationStrategy.OnEnter);
await this.adjustPageTags();
}
}
this.completed = true;
this.router.routeCompleted();
}
/**
* If the class exists and force is false, remove the class. If the class doesn't exist and force is
* true, add the class
* @param {string} className - The class name to toggle
* @param {boolean} force - boolean - if true, the class will be added, if false, the class will be
* removed
*/
toggleClass(className, force) {
const exists = this.routeElement.classList.contains(className);
if (exists && force == false)
this.routeElement.classList.remove(className);
if (!exists && force)
this.routeElement.classList.add(className);
}
/**
* If the route matches, add the `active` class, and if the route matches exactly, add the `exact`
* class
*/
adjustClasses() {
var _b;
const match = this.match != null;
const exact = ((_b = this.match) === null || _b === void 0 ? void 0 : _b.isExact) || false;
this.toggleClass('active', match);
this.toggleClass('exact', exact);
if (this.transition)
this.toggleClass(this.transition, exact);
}
/**
* It captures all the inner links of the current route and resolves the HTML of the current route
* @param {HTMLElement} [root] - The root element to search for links.
*/
captureInnerLinksAndResolveHtml(root) {
this.router.captureInnerLinks(root || this.routeElement, this.path);
resolveChildElementXAttributes(this.routeElement);
}
/**
* It resolves the page title by replacing any tokens in the title with the corresponding data from
* the data store
* @returns The title of the page.
*/
async resolvePageTitle() {
let title = this.pageData.title;
if (commonState.dataEnabled &&
this.pageData.title &&
hasToken(this.pageData.title)) {
title = await resolveTokens(this.pageData.title);
}
return title || this.pageData.title;
}
/**
* It sets the page title, description, and keywords based on the page data
*/
async adjustPageTags() {
const data = this.pageData;
data.title = await this.resolvePageTitle();
if (commonState.dataEnabled) {
if (!this.pageData.description &&
hasToken(this.pageData.description)) {
data.description = await resolveTokens(this.pageData.description);
}
if (!this.pageData.keywords &&
hasToken(this.pageData.keywords)) {
data.keywords = await resolveTokens(this.pageData.keywords);
}
}
this.router.setPageTags(data);
}
/**
* "Get the previous route in the route tree."
*
* The function is async because it calls `getSiblingRoutes()` which is async
* @returns The previous route in the route tree.
*/
async getPreviousRoute() {
var _b;
const siblings = await this.getSiblingRoutes();
const index = this.getSiblingIndex(siblings.map(r => r.route));
let back = index > 0 ? siblings.slice(index - 1) : [];
return ((_b = back[0]) === null || _b === void 0 ? void 0 : _b.route) || this.parentRoute;
}
/**
* > If the current route element is a child of a form, return the validity of the child input
* @returns The validity of the child input elements of the routeElement.
*/
isValidForNext() {
return getChildInputValidity(this.routeElement);
}
/**
* It returns the next route in the route tree
* @returns The next route in the route stack.
*/
async getNextRoute() {
if (this.routeElement.tagName == 'N-VIEW-PROMPT') {
return this.parentRoute;
}
const siblings = await this.getSiblingRoutes();
const index = this.getSiblingIndex(siblings.map(r => r.route));
let next = siblings.slice(index + 1);
return next.length && next[0] ? next[0].route : this.parentRoute;
}
/**
* It takes the current route's path, finds all the possible parent paths, finds the routes that
* match those paths, and then resolves the title for each of those routes
* @returns An array of objects with the following properties:
* - route: The route object
* - path: The path of the route
* - title: The title of the route
*/
async getParentRoutes() {
const parents = await Promise.all(getPossibleParentPaths(this.path)
.map(path => this.router.routes.find(p => p.path == path))
.filter(r => r)
.map(async (route) => {
var _b;
const path = ((_b = route === null || route === void 0 ? void 0 : route.match) === null || _b === void 0 ? void 0 : _b.path.toString()) || (route === null || route === void 0 ? void 0 : route.path);
const title = await route.resolvePageTitle();
return {
route,
path,
title,
};
}));
return parents;
}
/**
* It returns the parent route of the current route
* @returns The parent route
*/
getParentRoute() {
return this.parentRoute;
}
/**
* It sorts an array of DOM elements by their order in the DOM
* @param {Element[]} elements - Element[] - An array of elements to sort.
* @returns The elements are being sorted by their position in the DOM.
*/
sortRoutes(elements) {
return elements.sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING
? -1
: 1);
}
/**
* It returns a list of all the sibling routes of the current route, sorted by their order in the DOM
* @returns An array of objects with the following properties:
* - route: The route object
* - path: The path of the route
* - title: The title of the route
*/
async getSiblingRoutes() {
var _b;
return await Promise.all((((_b = this.parentRoute) === null || _b === void 0 ? void 0 : _b.childRoutes) ||
this.router.routes.filter(r => r.parentRoute == null))
.sort((a, b) => a.routeElement.compareDocumentPosition(b.routeElement) &
Node.DOCUMENT_POSITION_FOLLOWING
? -1
: 1)
.map(async (route) => {
var _b;
const path = ((_b = route.match) === null || _b === void 0 ? void 0 : _b.path.toString()) || route.path;
const title = await route.resolvePageTitle();
return {
route,
path,
title,
};
}));
}
/**
* It returns a promise that resolves to an array of objects, each of which contains a route, a path,
* and a title
* @returns An array of objects with the following properties:
* - route: the route object
* - path: the path of the route
* - title: the title of the route
*/
async getChildRoutes() {
return await Promise.all(this.childRoutes
.sort((a, b) => a.routeElement.compareDocumentPosition(b.routeElement) &
Node.DOCUMENT_POSITION_FOLLOWING
? -1
: 1)
.map(async (route) => {
var _b;
const path = ((_b = route.match) === null || _b === void 0 ? void 0 : _b.path.toString()) || route.path;
const title = await route.resolvePageTitle();
return {
route,
path,
title,
};
}));
}
/**
* It returns the index of the current route in the array of routes passed to it
* @param {Route[]} siblings - The array of routes that are siblings to the current route.
* @returns The index of the current route in the array of siblings.
*/
getSiblingIndex(siblings) {
return (siblings === null || siblings === void 0 ? void 0 : siblings.findIndex(p => p.path == this.path)) || 0;
}
/**
* It takes a path and returns a route
* @param {string} path - string
*/
replaceWithRoute(path) {
const route = isAbsolute(path)
? path
: this.router.resolvePathname(path, this.path);
this.router.replaceWithRoute(route);
}
/**
* "Activate all action activators that match the given filter."
*
* The function is asynchronous because it may need to wait for the DOM to be ready
* @param {ActionActivationStrategy} forEvent - ActionActivationStrategy
* @param filter - (
*/
async activateActions(forEvent, filter = _a => true) {
await activateActionActivators(this.actionActivators, forEvent, filter);
}
/**
* It unsubscribes from the event listeners.
*/
destroy() {
var _b;
this.onStartedSubscription();
this.onChangedSubscription();
(_b = this.routeDestroy) === null || _b === void 0 ? void 0 : _b.call(this, this);
}
}