UNPKG

@nent/core

Version:

Functional elements to add routing, data-binding, dynamic HTML, declarative actions, audio, video, and so much more. Supercharge static HTML files into web apps without script or builds.

1,507 lines (1,496 loc) 60.9 kB
/*! * 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