@nent/core
Version:
1,507 lines (1,496 loc) • 60.9 kB
JavaScript
/*!
* NENT 2022
*/
import { r as registerInstance, w as writeTask, h, H as Host, a as getElement } from './index-916ca544.js';
import { E as EventEmitter, e as eventBus, a as actionBus } from './index-f7016b94.js';
import { c as logIf, f as debugIf, b as warnIf, w as warn } from './logging-5a93c8af.js';
import { a as state, o as onChange } from './state-27a8a5bc.js';
import { g as getDataProvider, a as addDataProvider } from './factory-acbf0d3d.js';
import { D as DATA_EVENTS } from './interfaces-8c5cd1b8.js';
import { a as activateActionActivators } from './elements-4818d39b.js';
import { A as ActionActivationStrategy } from './interfaces-837cdb60.js';
import { r as resolveChildElementXAttributes } from './elements-1b845a48.js';
import { hasToken, resolveTokens } from './tokens-78f8cdbe.js';
import { g as getChildInputValidity } from './elements-397b851b.js';
import { R as ROUTE_EVENTS, N as NAVIGATION_TOPIC, a as NAVIGATION_COMMANDS } from './interfaces-3b78db83.js';
import { i as isValue } from './values-ddfac998.js';
import { g as getSessionVisits, a as getStoredVisits, b as getVisits } from './visits-b52975ad.js';
import { s as state$1 } from './state-adf07580.js';
import './index-4bfabbbd.js';
import './promises-584c4ece.js';
import './expressions-2c27c47c.js';
import './strings-47d55561.js';
import './mutex-e5645c85.js';
import './memory-0d63dacd.js';
/* istanbul ignore file */
/**
* Ensures basename
* @param path
* @param prefix
* @returns
*/
function ensureBasename(path, prefix) {
let result = hasBasename(path, prefix) ? path : `${prefix}/${path}`;
result = result.replace(/\/{2,}/g, '/'); // stripTrailingSlash()
return addLeadingSlash(result);
}
/**
* Paths has basename
* @param path
* @param prefix
*/
const hasBasename = (path, prefix = '/') => path.startsWith(prefix) ||
new RegExp(`^${prefix}(\\/|\\?|#|$)`, 'i').test(path);
/**
* Paths strip basename
* @param path
* @param prefix
* @returns
*/
const stripBasename = (path, prefix) => {
let stripped = hasBasename(path, prefix)
? path.slice(prefix.length)
: path;
return addLeadingSlash(stripped);
};
/**
* Paths is filename
* @param path
*/
const isFilename = (path) => path.includes('.');
/**
* Paths add leading slash
* @param path
*/
const addLeadingSlash = (path) => (path === null || path === void 0 ? void 0 : path.startsWith('/')) ? path : `/${path}`;
/**
* Parses path
* @param [path]
* @returns path
*/
function parsePath(path = '/') {
let pathname = path;
let search = '';
let hash = '';
const hashIndex = pathname.indexOf('#');
if (hashIndex !== -1) {
hash = pathname.slice(hashIndex);
pathname = pathname.slice(0, Math.max(0, hashIndex));
}
const searchIndex = pathname.indexOf('?');
if (searchIndex !== -1) {
search = pathname.slice(searchIndex);
pathname = pathname.slice(0, Math.max(0, searchIndex));
}
return {
pathname,
search: search === '?' ? '' : search,
hash: hash === '#' ? '' : hash,
query: {},
key: '',
params: {},
};
}
/**
* Creates path
* @param location
* @returns
*/
function createPath(location) {
const { pathname, search, hash } = location;
let path = pathname || '/';
if (search && search !== '?') {
path += (search === null || search === void 0 ? void 0 : search.startsWith('?')) ? search : `?${search}`;
}
if (hash && hash !== '#') {
path += (hash === null || hash === void 0 ? void 0 : hash.startsWith('#')) ? hash : `#${hash}`;
}
return path;
}
/**
* Parses query string
* @param query
* @returns
*/
function parseQueryString(query) {
if (!query) {
return {};
}
return (/^[?#]/.test(query) ? query.slice(1) : query)
.split('&')
.reduce((parameters, parameter) => {
const [key, value] = parameter.split('=');
parameters[key] = value
? decodeURIComponent(value.replace(/\+/g, ' '))
: '';
return parameters;
}, {});
}
/**
* Turn a URL path to an array of possible parent-routes
*
* '/home/profile' -> ['/','/home', '/home/profile']
*/
function getPossibleParentPaths(path) {
if (!isValue(path))
return [];
let workingPath = path.endsWith('/')
? path.slice(0, path.length - 1)
: path.slice();
const results = [path.slice()];
let index = workingPath.lastIndexOf('/');
while (index > 0) {
workingPath = workingPath.substr(0, index);
results.push(workingPath.slice());
index = workingPath.lastIndexOf('/');
}
if (path != '/')
results.push('/');
return results.reverse();
}
/* istanbul ignore file */
const isAbsolute = (pathname) => (pathname === null || pathname === void 0 ? void 0 : pathname.startsWith('/')) || false;
const createKey = (keyLength) => Math.random().toString(36).slice(2, keyLength);
// About 1.5x faster than the two-arg version of Array#splice()
const spliceOne = (list, index) => {
for (let i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) {
list[i] = list[k];
}
list.pop();
};
// This implementation is based heavily on node's url.parse
function resolvePathname(to, from = '') {
let fromParts = (from === null || from === void 0 ? void 0 : from.split('/')) || [];
let hasTrailingSlash;
let up = 0;
const toParts = (to === null || to === void 0 ? void 0 : to.split('/')) || [];
const isToAbs = to && isAbsolute(to);
const isFromAbs = from && isAbsolute(from);
const mustEndAbs = isToAbs || isFromAbs;
if (to && isAbsolute(to)) {
// To is absolute
fromParts = toParts;
}
else if (toParts.length > 0) {
// To is relative, drop the filename
fromParts.pop();
fromParts = fromParts.concat(toParts);
}
if (fromParts.length === 0) {
return '/';
}
if (fromParts.length > 0) {
const last = fromParts[fromParts.length - 1];
hasTrailingSlash = last === '.' || last === '..' || last === '';
}
else {
hasTrailingSlash = false;
}
for (let i = fromParts.length; i >= 0; i--) {
const part = fromParts[i];
if (part === '.') {
spliceOne(fromParts, i);
}
else if (part === '..') {
spliceOne(fromParts, i);
up++;
}
else if (up) {
spliceOne(fromParts, i);
up--;
}
}
if (!mustEndAbs) {
for (; up--; up) {
fromParts.unshift('..');
}
}
if (mustEndAbs &&
fromParts[0] !== '' &&
(!fromParts[0] || !isAbsolute(fromParts[0]))) {
fromParts.unshift('');
}
let result = fromParts.join('/');
if (hasTrailingSlash && !result.endsWith('/')) {
result += '/';
}
return result;
}
const valueEqual = (a, b) => {
if (a === b) {
return true;
}
if (a === null || b === null) {
return false;
}
if (Array.isArray(a)) {
return (Array.isArray(b) &&
a.length === b.length &&
a.every((item, index) => valueEqual(item, b[index])));
}
const aType = typeof a;
const bType = typeof b;
if (aType !== bType) {
return false;
}
if (aType === 'object') {
const aValue = a.valueOf();
const bValue = b.valueOf();
if (aValue !== a || bValue !== b) {
return valueEqual(aValue, bValue);
}
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length) {
return false;
}
return aKeys.every(key => valueEqual(a[key], b[key]));
}
return false;
};
const locationsAreEqual = (a, b) => a.pathname === b.pathname &&
a.search === b.search &&
a.hash === b.hash &&
a.key === b.key &&
valueEqual(a.state, b.state);
const createLocation = (path, state, key, currentLocation) => {
var _a, _b, _c;
let location;
if (typeof path === 'string') {
// Two-arg form: push(path, state)
location = parsePath(path);
if (state !== undefined) {
location.state = state;
}
}
else {
// One-arg form: push(location)
location = Object.assign({}, path);
if (location.search && !location.search.startsWith('?')) {
location.search = `?${location.search}`;
}
if (location.hash && !location.hash.startsWith('#')) {
location.hash = `#${location.hash}`;
}
if (state !== undefined && location.state === undefined) {
location.state = state;
}
}
try {
location.pathname = decodeURI(location.pathname);
}
catch (error_) {
const error = error_ instanceof URIError
? new URIError(`Pathname "${location.pathname}" could not be decoded. This is likely caused by an invalid percent-encoding.`)
: error_;
throw error;
}
location.key = key;
location.params = {};
if (currentLocation) {
// Resolve incomplete/relative pathname relative to current location.
if (!location.pathname) {
location.pathname = currentLocation.pathname;
}
else if (!((_a = location.pathname) === null || _a === void 0 ? void 0 : _a.startsWith('/'))) {
location.pathname = resolvePathname(location.pathname, currentLocation.pathname);
}
}
else if (!location.pathname) {
location.pathname = '/';
}
location.query = parseQueryString(location.search || '') || {};
location.pathParts = ((_b = location.pathname) === null || _b === void 0 ? void 0 : _b.split('/')) || [];
location.hashParts = ((_c = location.hash) === null || _c === void 0 ? void 0 : _c.split('/')) || [];
return location;
};
/* istanbul ignore file */
/**
* TS adaption of https://github.com/pillarjs/path-to-regexp/blob/master/index.js
*/
/**
* Default configs.
*/
const DEFAULT_DELIMITER = '/';
const DEFAULT_DELIMITERS = './';
/**
* The main path matching regexp utility.
*/
const PATH_REGEXP = new RegExp([
// Match escaped characters that would otherwise appear in future matches.
// This allows the user to escape special characters that won't transform.
'(\\\\.)',
// Match Express-style parameters and un-named parameters with a prefix
// and optional suffixes. Matches appear as:
//
// "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?"]
// "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined]
'(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?',
].join('|'), 'g');
/**
* Parse a string for the raw tokens.
*/
const parse = (string, options) => {
const tokens = [];
let key = 0;
let index = 0;
let path = '';
const defaultDelimiter = (options === null || options === void 0 ? void 0 : options.delimiter) || DEFAULT_DELIMITER;
const delimiters = (options === null || options === void 0 ? void 0 : options.delimiters) || DEFAULT_DELIMITERS;
let pathEscaped = false;
let res;
while ((res = PATH_REGEXP.exec(string)) !== null) {
const m = res[0];
const escaped = res[1];
const offset = res.index;
path += string.slice(index, offset);
index = offset + m.length;
// Ignore already escaped sequences.
if (escaped) {
path += escaped[1];
pathEscaped = true;
continue;
}
let previous = '';
const next = string[index];
const name = res[2];
const capture = res[3];
const group = res[4];
const modifier = res[5];
if (!pathEscaped && path.length > 0) {
const k = path.length - 1;
if (delimiters.includes(path[k])) {
previous = path[k];
path = path.slice(0, k);
}
}
// Push the current path onto the tokens.
if (path) {
tokens.push(path);
path = '';
pathEscaped = false;
}
const partial = previous !== '' && next !== undefined && next !== previous;
const repeat = modifier === '+' || modifier === '*';
const optional = modifier === '?' || modifier === '*';
const delimiter = previous || defaultDelimiter;
const pattern = capture || group;
tokens.push({
name: name || key++,
prefix: previous,
delimiter,
optional,
repeat,
partial,
pattern: pattern
? escapeGroup(pattern)
: `[^${escapeString(delimiter)}]+?`,
});
}
// Push any remaining characters.
if (path || index < string.length) {
tokens.push(path + string.slice(index));
}
return tokens;
};
/**
* Escape a regular expression string.
*/
const escapeString = (string) => string.replace(/([.+*?=^!:${}()\[\]|/\\])/g, '\\$1');
/**
* Escape the capturing group by escaping special characters and meaning.
*/
const escapeGroup = (group) => group.replace(/([=!:$/()])/g, '\\$1');
/**
* Get the flags for a regexp from the options.
*/
const flags = (options) => (options === null || options === void 0 ? void 0 : options.sensitive) ? '' : 'i';
/**
* Pull out keys from a regexp.
*/
const regexpToRegexp = (path, keys) => {
if (!keys) {
return path;
}
// Use a negative lookahead to match only capturing groups.
const groups = path.source.match(/\((?!\?)/g);
if (groups) {
for (let i = 0; i < groups.length; i++) {
keys.push({
name: i,
prefix: null,
delimiter: null,
optional: false,
repeat: false,
partial: false,
pattern: null,
});
}
}
return path;
};
/**
* Transform an array into a regexp.
*/
const arrayToRegexp = (path, keys, options) => {
const parts = [];
for (const element of path) {
parts.push(pathToRegexp(element, keys, options).source);
}
return new RegExp(`(?:${parts.join('|')})`, flags(options));
};
/**
* Create a path regexp from string input.
*/
const stringToRegexp = (path, keys, options) => tokensToRegExp(parse(path, options), keys, options);
/**
* Expose a function for taking tokens and returning a RegExp.
*/
const tokensToRegExp = (tokens, keys, options) => {
var _a;
options = options || {};
const { strict } = options;
const end = options.end !== false;
const delimiter = escapeString(options.delimiter || DEFAULT_DELIMITER);
const delimiters = options.delimiters || DEFAULT_DELIMITERS;
const endsWith = (((_a = options.endsWith) === null || _a === void 0 ? void 0 : _a.length)
? [...options.endsWith]
: options.endsWith
? [options.endsWith]
: [])
.map(i => escapeString(i))
.concat('$')
.join('|');
let route = '';
let isEndDelimited = false;
// Iterate over the tokens and create our regexp string.
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (typeof token === 'string') {
route += escapeString(token);
isEndDelimited =
i === tokens.length - 1 &&
delimiters.includes(token[token.length - 1]);
}
else {
const prefix = escapeString(token.prefix || '');
const capture = token.repeat
? `(?:${token.pattern})(?:${prefix}(?:${token.pattern}))*`
: token.pattern;
if (keys) {
keys.push(token);
}
if (token.optional) {
route += token.partial
? `${prefix}(${capture})?`
: `(?:${prefix}(${capture}))?`;
}
else {
route += `${prefix}(${capture})`;
}
}
}
if (end) {
if (!strict) {
route += `(?:${delimiter})?`;
}
route += endsWith === '$' ? '$' : `(?=${endsWith})`;
}
else {
if (!strict) {
route += `(?:${delimiter}(?=${endsWith}))?`;
}
if (!isEndDelimited) {
route += `(?=${delimiter}|${endsWith})`;
}
}
return new RegExp(`^${route}`, flags(options));
};
/**
* Normalize the given path string, returning a regular expression.
*
* An empty array can be passed in for the keys, which will hold the
* placeholder key descriptions. For example, using `/user/:id`, `keys` will
* contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`.
*/
const pathToRegexp = (path, keys, options) => {
if (path instanceof RegExp) {
return regexpToRegexp(path, keys);
}
if (Array.isArray(path)) {
return arrayToRegexp(path, keys, options);
}
return stringToRegexp(path, keys, options);
};
/* istanbul ignore file */
let cacheCount = 0;
const patternCache = {};
const cacheLimit = 10000;
// Memoized function for creating the path match regex
const compilePath = (pattern, options) => {
const cacheKey = `${options.end}${options.strict}`;
const cache = patternCache[cacheKey] || (patternCache[cacheKey] = {});
const cachePattern = JSON.stringify(pattern instanceof RegExp ? pattern.source : pattern);
if (cache[cachePattern]) {
return cache[cachePattern];
}
const keys = [];
const re = pathToRegexp(pattern, keys, options);
const compiledPattern = { re, keys };
if (cacheCount < cacheLimit) {
cache[cachePattern] = compiledPattern;
cacheCount += 1;
}
return compiledPattern;
};
/**
* Public API for matching a URL pathname to a path pattern.
*/
function matchPath(location, options = {}) {
if (typeof options === 'string') {
options = { path: options };
}
const { pathname } = location;
const { path = '/', exact = false, strict = false } = options;
const { re, keys } = compilePath(path, { end: exact, strict });
const match = re.exec(pathname);
if (!match) {
return null;
}
const [url, ...values] = match;
const isExact = pathname === url;
if (exact && !isExact) {
return null;
}
const result = {
path,
url: path === '/' && url === '' ? '/' : url,
isExact,
params: keys.reduce((memo, key, index) => {
memo[key.name] = values[index];
return memo;
}, {}),
};
if (result.isExact) {
Object.assign(location.params, result.params);
}
return result;
}
const matchesAreEqual = (a, b) => {
if (a === null && b === null) {
return true;
}
if (b === null) {
return false;
}
return (a &&
b &&
a.path === b.path &&
a.url === b.url &&
valueEqual(a.params, b.params));
};
/* It's a wrapper around a route element that provides a bunch of methods for interacting with the
route */
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(state.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(state.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 (state.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 (state.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);
}
}
/* It listens to the `NAVIGATION_TOPIC` topic and when it receives an event, it calls the appropriate
method on the `RouterService` class */
class NavigationActionListener {
constructor(router, events, actions) {
this.router = router;
this.events = events;
this.actions = actions;
this.removeSubscription = this.actions.on(NAVIGATION_TOPIC, e => {
this.handleEventAction(e);
});
}
/**
* `notifyRouterInitialized()` is a function that emits an event to the `Router`'s `EventEmitter`
* object
*/
notifyRouterInitialized() {
logIf(state.debug, `route event: initialized`);
this.events.emit(ROUTE_EVENTS.Initialized, {});
}
/**
* It emits a route change event
* @param {string} newPath - The new path that the router is navigating to.
*/
notifyRouteChangeStarted(newPath) {
logIf(state.debug, `route event: started ${newPath}`);
this.events.emit(ROUTE_EVENTS.RouteChangeStart, newPath);
}
/**
* `notifyRouteChanged` is a function that emits a `RouteChanged` event
* @param {LocationSegments} location - LocationSegments
*/
notifyRouteChanged(location) {
logIf(state.debug, `route event: changed`);
this.events.emit(ROUTE_EVENTS.RouteChanged, location);
}
/**
* `notifyRouteFinalized` is a function that takes a `location` parameter and emits a
* `ROUTE_EVENTS.RouteChangeFinish` event
* @param {LocationSegments} location - LocationSegments
*/
notifyRouteFinalized(location) {
logIf(state.debug, `route event: finalized`);
this.events.emit(ROUTE_EVENTS.RouteChangeFinish, location);
}
/**
* It emits a RouteMatched event
* @param {Route} route - The route that was matched
* @param {MatchResults} match - MatchResults
*/
notifyMatch(route, match) {
logIf(state.debug, `route event: matched`);
this.events.emit(ROUTE_EVENTS.RouteMatched, {
route,
match,
});
}
/**
* `notifyMatchExact` is a function that emits a `RouteMatchedExact` event
* @param {Route} route - The route that was matched
* @param {MatchResults} match - MatchResults
*/
notifyMatchExact(route, match) {
logIf(state.debug, `route event: matched-exact`);
this.events.emit(ROUTE_EVENTS.RouteMatchedExact, {
route,
match,
});
}
handleEventAction(eventAction) {
debugIf(state.debug, `route-listener: action received ${JSON.stringify(eventAction)}`);
switch (eventAction.command) {
case NAVIGATION_COMMANDS.goNext: {
this.router.goNext();
break;
}
case NAVIGATION_COMMANDS.goBack: {
this.router.goBack();
break;
}
case NAVIGATION_COMMANDS.goToParent: {
this.router.goToParentRoute();
break;
}
case NAVIGATION_COMMANDS.goTo: {
const { path } = eventAction.data;
this.router.goToRoute(path);
break;
}
case NAVIGATION_COMMANDS.back: {
this.router.history.goBack();
break;
}
case NAVIGATION_COMMANDS.scrollTo: {
const { id } = eventAction.data;
this.router.scrollToId(id);
break;
}
}
}
/**
* It removes the subscription to the observable.
*/
destroy() {
this.removeSubscription();
}
}
/* istanbul ignore file */
/**
* It attaches an event handler to all elements matching a query selector, but only once per element
* @param {HTMLElement} rootElement - The root element to search for the query.
* @param {string} query - The query to find the elements.
* @param {string} event - The event name, such as "click" or "mouseover".
* @param eventHandler - (el: TElement, ev: TEvent) => void
*/
function captureElementsEventOnce(rootElement, query, event, eventHandler) {
const attribute = `n-attached-${event}`;
Array.from(rootElement.querySelectorAll(query) || [])
.map(el => el)
.filter(el => !el.hasAttribute(attribute))
.forEach((el) => {
el.addEventListener(event, ev => {
eventHandler(el, ev);
});
el.setAttribute(attribute, '');
});
}
const RouterScrollKey = 'scrollPositions';
/* It's a class that stores scroll positions in a map and saves them to the session storage */
class ScrollHistory {
/**
* We're getting the scroll position from the session storage and setting it to the scrollPositions
* variable
* @param {Window} win - Window - This is the window object.
*/
constructor(win) {
this.win = win;
this.scrollPositions = new Map();
getDataProvider('session').then(provider => {
this.provider = provider;
return provider === null || provider === void 0 ? void 0 : provider.get(RouterScrollKey).then(scrollData => {
if (scrollData)
this.scrollPositions = new Map(JSON.parse(scrollData));
});
});
if (win && 'scrollRestoration' in win.history) {
win.history.scrollRestoration = 'manual';
}
}
set(key, value) {
this.scrollPositions.set(key, value);
if (this.provider) {
const arrayData = [];
this.scrollPositions.forEach((v, k) => {
arrayData.push([k, v]);
});
this.provider
.set(RouterScrollKey, JSON.stringify(arrayData))
.then(() => { });
}
}
get(key) {
return this.scrollPositions.get(key);
}
has(key) {
return this.scrollPositions.has(key);
}
capture(key) {
this.set(key, [this.win.scrollX, this.win.scrollY]);
}
}
const KeyLength = 6;
/* It's a wrapper around the browser's history API that emits events when the location changes */
class HistoryService {
constructor(win, basename) {
this.win = win;
this.basename = basename;
this.allKeys = [];
this.events = new EventEmitter();
this.location = this.getDOMLocation(this.getHistoryState());
this.previousLocation = this.location;
this.allKeys.push(this.location.key);
this.scrollHistory = new ScrollHistory(win);
this.win.addEventListener('popstate', e => {
this.handlePop(this.getDOMLocation(e.state));
});
}
getHistoryState() {
return this.win.history.state || {};
}
/**
* It returns a location object with the pathname, state, and key properties
* @param {any} historyState - any
* @returns A location object
*/
getDOMLocation(historyState) {
const { key, state = {} } = historyState || {};
const { pathname, search, hash } = this.win.location;
let path = pathname + search + hash;
warnIf(!hasBasename(path, this.basename), `You are attempting to use a basename on a page whose URL path does not begin with the basename. Expected path "${path}" to begin with "${this.basename}".`);
if (this.basename) {
path = stripBasename(path, this.basename);
}
return createLocation(path, state, key || createKey(6));
}
handlePop(location) {
if (locationsAreEqual(this.location, location)) {
return; // A hashchange doesn't always == location change.
}
this.setState('POP', location);
}
/**
* It pushes a new location to the history stack, and updates the state of the history object
* @param {string} path - string
* @param {any} state - any = {}
* @returns the location object.
*/
push(path, state = {}) {
const action = 'PUSH';
const location = createLocation(path, state, createKey(KeyLength), this.location);
const href = this.createHref(location);
const { key } = location;
if (locationsAreEqual(this.location, location))
return;
this.win.history.pushState({ key, state }, '', href);
const previousIndex = this.allKeys.indexOf(this.location.key);
const nextKeys = this.allKeys.slice(0, previousIndex === -1 ? 0 : previousIndex + 1);
nextKeys.push(location.key);
this.allKeys = nextKeys;
this.setState(action, location);
}
/**
* It replaces the current history entry with a new one
* @param {string} path - The path of the URL.
* @param {any} state - any = {}
*/
replace(path, state = {}) {
const action = 'REPLACE';
const location = createLocation(path, state, createKey(KeyLength), this.location);
location.search = this.location.search;
const href = this.createHref(location);
const { key } = location;
this.win.history.replaceState({ key, state }, '', href);
const previousIndex = this.allKeys.indexOf(this.location.key);
if (previousIndex !== -1) {
this.allKeys[previousIndex] = location.key;
}
this.setState(action, location);
}
/**
* It takes a location object and returns a path string
* @param {LocationSegments} location - LocationSegments
* @returns A string that is the pathname of the location object.
*/
createHref(location) {
return ensureBasename(createPath(location), this.basename);
}
/**
* It captures the scroll position of the current view, then updates the location and scroll position
* of the new view
* @param {string} action - string
* @param {LocationSegments} location - LocationSegments
*/
setState(action, location) {
// Capture location for the view before changing history.
this.scrollHistory.capture(this.location.key);
this.previousLocation = this.location;
this.location = location;
// Set scroll position based on its previous storage value
this.location.scrollPosition = this.scrollHistory.get(this.location.key);
this.events.emit(action, this.location);
}
/**
* It goes to a specific page in the history
* @param {number} n - number - The number of steps to go back or forward in the history.
*/
go(n) {
this.win.history.go(n);
this.events.emit('GO', this.location);
}
/**
* It goes back one page in the browser's history, and then emits an event called BACK
*/
goBack() {
this.win.history.back();
this.events.emit('BACK', this.location);
}
/**
* It goes forward in the browser history
*/
goForward() {
this.win.history.forward();
this.events.emit('FORWARD', this.location);
}
/**
* It takes a listener function as an argument, calls that function with the current location, and
* then returns a function that will remove the listener from the event emitter
* @param {Listener} listener - Listener
* @returns A function that removes the listener from the events object.
*/
listen(listener) {
listener(this.location);
return this.events.on('*', (_a, location) => {
listener(location);
});
}
/**
* Destroys history service
*/
destroy() {
this.events.removeAllListeners();
}
}
/* It's a data provider that gets its data from the current route */
class RoutingDataProvider {
constructor(accessor) {
this.accessor = accessor;
this.changed = new EventEmitter();
}
async get(key) {
return this.accessor(key);
}
async set(_key, _value) {
// Do nothing
}
}
/* The RouterService is responsible for managing the browser history and the routes that are registered
with the router */
class RouterService {
/**
* It creates a new instance of the NavigationService class
* @param {Window} win - Window - the window object
* @param writeTask - (t: RafCallback) => void
* @param {IEventEmitter} eventBus - IEventEmitter - this is the event bus that the router uses to
* communicate with the rest of the application.
* @param {IEventEmitter} actions - IEventEmitter - this is the actions object that is passed to the
* app.
* @param {string} [root] - The root of the application.
* @param {string} [appTitle] - The title of the app.
* @param {string} [appDescription] - string = '',
* @param {string} [appKeywords] - string = '',
* @param {string} [transition] - string = '',
* @param [scrollTopOffset=0] - This is the number of pixels from the top of the page that the
* browser should scroll to when a new page is loaded.
*/
constructor(win, writeTask, eventBus, actions, root = '', appTitle = '', appDescription = '', appKeywords = '', transition = '', scrollTopOffset = 0) {
this.win = win;
this.writeTask = writeTask;
this.eventBus = eventBus;
this.actions = actions;
this.root = root;
this.appTitle = appTitle;
this.appDescription = appDescription;
this.appKeywords = appKeywords;
this.transition = transition;
this.scrollTopOffset = scrollTopOffset;
this.routes = [];
this.history = new HistoryService(win, root);
this.listener = new NavigationActionListener(this, this.eventBus, this.actions);
if (state.dataEnabled)
this.enableDataProviders();
else {
const dataSubscription = onChange('dataEnabled', enabled => {
if (enabled) {
this.enableDataProviders();
}
dataSubscription();
});
}
this.removeHandler = this.history.listen((location) => {
var _a;
this.location = location;
this.listener.notifyRouteChanged(location);
(_a = this.routeData) === null || _a === void 0 ? void 0 : _a.changed.emit(DATA_EVENTS.DataChanged, {
changed: ['route'],
});
});
this.listener.notifyRouteChanged(this.history.location);
}
/**
* It adds three data providers to the data provider registry
*/
async enableDataProviders() {
this.routeData = new RoutingDataProvider((key) => {
let route = { data: this.location.params };
if (this.hasExactRoute())
route = Object.assign(route, this.exactRoute);
return route.data[key] || route[key];
});
addDataProvider('route', this.routeData);
this.queryData = new RoutingDataProvider((key) => this.location.query[key]);
addDataProvider('query', this.queryData);
this.queryData = new RoutingDataProvider((key) => this.location.query[key]);
this.visitData = new RoutingDataProvider(async (key) => {
switch (key) {
case 'all':
const all = await getVisits();
return JSON.stringify(all).split(`"`).join(`'`);
case 'stored':
const stored = await getStoredVisits();
return JSON.stringify(stored).split(`"`).join(`'`);
case 'session':
const session = await getSessionVisits();
return JSON.stringify(session).split(`"`).join(`'`);
}
});
addDataProvider('visits', this.visitData);
}
/**
* It takes a path and returns a path with a leading slash
* @param {string} path - The path to be adjusted.
* @returns The path with the root removed.
*/
adjustRootViewUrls(path) {
let stripped = this.root && hasBasename(path, this.root)
? path.slice(this.root.length)
: path;
if (isFilename(this.root)) {
return '#' + addLeadingSlash(stripped);
}
return addLeadingSlash(stripped);
}
/**
* If the current location is the root, or if the current location is the root, return true
* @returns The pathname of the current location.
*/
atRoot() {
var _a;
return (((_a = this.location) === null || _a === void 0 ? void 0 : _a.pathname) == this.root ||
this.location.pathname == '/');
}
/**
* It initializes the router by setting the startUrl, replacing the current route with the startUrl
* if the startUrl is at the root, capturing inner links, notifying the listener that the router has
* been initialized, and calling allRoutesComplete if all routes are complete
* @param {string} [startUrl] - The URL that the router should start at.
*/
initialize(startUrl) {
this.startUrl = startUrl;
if (startUrl && this.atRoot())
this.replaceWithRoute(stripBasename(startUrl, this.root));
this.captureInnerLinks(this.win.document.body);
this.listener.notifyRouterInitialized();
if (this.routes.every(r => r.completed)) {
this.allRoutesComplete();
}
}
/**
* If the route is not found, set the page title to "Not found" and the robots meta tag to "nofollow"
*/
allRoutesComplete() {
//if (!this.hasExactRoute()) {
// this.setPageTags({
// title: 'Not found',
// robots: 'nofollow',
// })
//}
this.listener.notifyRouteFinalized(this.location);
}
/**
* If all routes are completed, then call the allRoutesComplete function
*/
routeCompleted() {
if (this.routes.every(r => r.completed)) {
this.allRoutesComplete();
}
}
/**
* If the current route has a next route, go to that route. Otherwise, go back in the browser history
* @returns the previous location pathname.
*/
async goBack() {
if (this.exactRoute) {
const nextRoute = await this.exactRoute.getNextRoute();
if (nextRoute) {
this.goToRoute(nextRoute === null || nextRoute === void 0 ? void 0 : nextRoute.path);
return;
}
}
this.listener.notifyRouteChangeStarted(this.history.previousLocation.pathname);
this.history.goBack();
}
/**
* If the current route is valid, then go to the next route, otherwise go to the parent route
* @returns The next route
*/
async goNext() {
if (this.exactRoute) {
if (!this.exactRoute.isValidForNext())
return;
const nextRoute = await this.exactRoute.getNextRoute();
// if the route returns null, then we can't move due to validation
if (nextRoute) {
this.goToRoute(nextRoute.path);
return;
}
}
this.goToParentRoute();
}
/**
* If the current route has a parent route, go to that parent route. Otherwise, go to the parent
* route of the current route
* @returns The parent route of the current route.
*/
goToParentRoute() {
var _a;
if (this.exactRoute) {
const parentRoute = this.exactRoute.getParentRoute();
if (parentRoute) {
this.goToRoute(parentRoute.path);
return;
}
}
const parentSegments = (_a = this.history.location.pathParts) === null || _a === void 0 ? void 0 : _a.slice(0, -1);
if (parentSegments) {
this.goToRoute(addLeadingSlash(parentSegments.join('/')));
}
else {
this.goToRoute(this.startUrl || '/');
}
}
/**
* If the history has a stored scroll position, scroll to that position. Otherwise, scroll to the top
* of the page
* @param {number} scrollOffset - number
*/
scrollTo(scrollOffset) {
// Okay, the frame has passed. Go ahead and render now
this.writeTask(() => {
var _a;
// first check if we have a stored scroll location
if (Array.isArray((_a = this.history.location) === null || _a === void 0 ? void 0 : _a.scrollPosition)) {
this.win.scrollTo(this.history.location.scrollPosition[0], this.history.location.scrollPosition[1]);
return;
}
this.win.scrollTo(0, scrollOffset || 0);
});
}
/**
* It takes an id, finds the element with that id, and scrolls it into view
* @param {string} id - The id of the element to scroll to.
*/
scrollToId(id) {
this.writeTask(() => {
const elm = this