@lightningjs/blits
Advanced tools
| import { default as fadeInFadeOutTransition } from './transitions/fadeInOut.js' | ||
| /** | ||
| * Get the current hash | ||
| * @returns {Hash} | ||
| */ | ||
| export const getHash = (hash) => { | ||
| if (!hash) hash = '/' | ||
| const hashParts = hash.replace(/^#/, '').split('?') | ||
| return { | ||
| path: hashParts[0], | ||
| queryParams: new URLSearchParams(hashParts[1]), | ||
| hash: hash, | ||
| } | ||
| } | ||
| export const normalizePath = (path) => { | ||
| return ( | ||
| path | ||
| // remove leading and trailing slashes | ||
| .replace(/^\/+|\/+$/g, '') | ||
| .toLowerCase() | ||
| ) | ||
| } | ||
| /** | ||
| * Check if a value is an object | ||
| * @param {any} v | ||
| * @returns {boolean} True if v is an object | ||
| */ | ||
| export const isObject = (v) => typeof v === 'object' && v !== null | ||
| /** | ||
| * Check if a value is a string | ||
| * @param {any} v | ||
| * @returns {boolean} True if v is a string | ||
| */ | ||
| export const isString = (v) => typeof v === 'string' | ||
| export const queryParamsToObject = (queryParams) => { | ||
| if (!queryParams) return {} | ||
| const object = {} | ||
| const queryParamsEntries = [...queryParams.entries()] | ||
| for (let i = 0; i < queryParamsEntries.length; i++) { | ||
| object[queryParamsEntries[i][0]] = queryParamsEntries[i][1] | ||
| } | ||
| return object | ||
| } | ||
| /** | ||
| * Default Route options | ||
| * | ||
| */ | ||
| const defaultOptions = { | ||
| inHistory: true, | ||
| keepAlive: false, | ||
| passFocus: true, | ||
| reuseComponent: false, | ||
| } | ||
| export const makeRouteObject = (route, overrides, overrideOptions, navigationData) => { | ||
| // FIX: exclude keepAlive from the destination route options. Unlike other | ||
| // overrides, keepAlive applies to the route being LEFT, not the destination. | ||
| // It is consumed by removeView() instead. | ||
| const { keepAlive: _keepAlive, ...destOverrides } = overrideOptions // eslint-disable-line no-unused-vars | ||
| const cleanRoute = { | ||
| hash: overrides.hash, | ||
| path: route.path, | ||
| component: route.component, | ||
| transition: 'transition' in route ? route.transition : fadeInFadeOutTransition, | ||
| options: { ...defaultOptions, ...route.options, ...destOverrides }, | ||
| announce: route.announce || false, | ||
| hooks: route.hooks || {}, | ||
| data: { ...route.data, ...navigationData, ...overrides.queryParams }, | ||
| params: overrides.params || {}, | ||
| meta: route.meta || {}, | ||
| } | ||
| return cleanRoute | ||
| } | ||
| /** | ||
| * Match a path to a route | ||
| * | ||
| * @param {object} hashObject | ||
| * @param {Route[]} routes | ||
| * @returns {Route} | ||
| */ | ||
| export const matchHash = ( | ||
| { hash, path, queryParams }, | ||
| routes = [], | ||
| overrideOptions = {}, | ||
| navigationData = {} | ||
| ) => { | ||
| // remove trailing slashes | ||
| const originalPath = path.replace(/^\/+|\/+$/g, '') | ||
| const originalNormalizedPath = normalizePath(path) | ||
| const override = { | ||
| hash: hash, | ||
| queryParams: queryParamsToObject(queryParams), | ||
| path: path, | ||
| } | ||
| /** @type {boolean|Route} */ | ||
| let matchingRoute = false | ||
| let i = 0 | ||
| while (!matchingRoute && i < routes.length) { | ||
| const route = routes[i] | ||
| const normalizedPath = normalizePath(route.path) | ||
| if (normalizePath(normalizedPath) === originalNormalizedPath) { | ||
| matchingRoute = makeRouteObject(route, override, overrideOptions, navigationData) | ||
| } else if (normalizedPath.indexOf(':') > -1) { | ||
| // match dynamic route parts | ||
| const dynamicRouteParts = [...normalizedPath.matchAll(/:([^\s/]+)/gi)] | ||
| // construct a regex for the route with dynamic parts | ||
| let dynamicRoutePartsRegex = normalizedPath | ||
| dynamicRouteParts.reverse().forEach((part) => { | ||
| dynamicRoutePartsRegex = | ||
| dynamicRoutePartsRegex.substring(0, part.index) + | ||
| '([^\\s/]+)' + | ||
| dynamicRoutePartsRegex.substring(part.index + part[0].length) | ||
| }) | ||
| dynamicRoutePartsRegex = '^' + dynamicRoutePartsRegex | ||
| // test if the constructed regex matches the path | ||
| const match = originalPath.match(new RegExp(`${dynamicRoutePartsRegex}`, 'i')) | ||
| if (match) { | ||
| // map the route params to a params object | ||
| override.params = dynamicRouteParts.reverse().reduce((acc, part, index) => { | ||
| acc[part[1]] = match[index + 1] | ||
| return acc | ||
| }, {}) | ||
| matchingRoute = makeRouteObject(route, override, overrideOptions, navigationData) | ||
| } | ||
| } else if (normalizedPath.endsWith('*')) { | ||
| const regex = new RegExp(normalizedPath.replace(/\/?\*/, '/?([^\\s]*)'), 'i') | ||
| const match = originalNormalizedPath.match(regex) | ||
| if (match) { | ||
| override.params = {} | ||
| if (match[1]) override.params.path = match[1] | ||
| matchingRoute = makeRouteObject(route, override, overrideOptions, navigationData) | ||
| } | ||
| } | ||
| i++ | ||
| } | ||
| // @ts-ignore - Remove me when we have a better way to handle this | ||
| return matchingRoute | ||
| } |
+7
-0
| # Changelog | ||
| ## v2.3.0 | ||
| _20 may 2026_ | ||
| - Refactored router functionality | ||
| - Replaced `window` reference with `self` reference in announcer | ||
| ## v2.2.0 | ||
@@ -4,0 +11,0 @@ |
+1
-1
| { | ||
| "name": "@lightningjs/blits", | ||
| "version": "2.2.0", | ||
| "version": "2.3.0", | ||
| "description": "Blits: The Lightning 3 App Development Framework", | ||
@@ -5,0 +5,0 @@ "bin": "bin/index.js", |
@@ -30,3 +30,3 @@ /* | ||
| let globalDefaultOptions = { | ||
| enableUtteranceKeepAlive: !/android/i.test((window.navigator || {}).userAgent || ''), | ||
| enableUtteranceKeepAlive: !/android/i.test((self.navigator || {}).userAgent || ''), | ||
| } | ||
@@ -33,0 +33,0 @@ |
@@ -20,3 +20,3 @@ /* | ||
| const syn = window.speechSynthesis | ||
| const syn = self.speechSynthesis | ||
@@ -23,0 +23,0 @@ const utterances = new Map() // id -> { utterance, timer, ignoreResume } |
+314
-487
@@ -18,3 +18,3 @@ /* | ||
| import { default as fadeInFadeOutTransition } from './transitions/fadeInOut.js' | ||
| import { getHash, isObject, isString, matchHash } from './utils.js' | ||
| import { reactive } from '../lib/reactivity/reactive.js' | ||
@@ -89,155 +89,4 @@ | ||
| let preventHashChangeNavigation = false | ||
| /** | ||
| * Get the current hash | ||
| * @returns {Hash} | ||
| */ | ||
| export const getHash = (hash) => { | ||
| if (!hash) hash = '/' | ||
| const hashParts = hash.replace(/^#/, '').split('?') | ||
| return { | ||
| path: hashParts[0], | ||
| queryParams: new URLSearchParams(hashParts[1]), | ||
| hash: hash, | ||
| } | ||
| } | ||
| const normalizePath = (path) => { | ||
| return ( | ||
| path | ||
| // remove leading and trailing slashes | ||
| .replace(/^\/+|\/+$/g, '') | ||
| .toLowerCase() | ||
| ) | ||
| } | ||
| /** | ||
| * Check if a value is an object | ||
| * @param {any} v | ||
| * @returns {boolean} True if v is an object | ||
| */ | ||
| const isObject = (v) => typeof v === 'object' && v !== null | ||
| /** | ||
| * Check if a value is a function | ||
| * @param {any} v | ||
| * @returns {boolean} True if v is a string | ||
| */ | ||
| const isString = (v) => typeof v === 'string' | ||
| const queryParamsToObject = (queryParams) => { | ||
| if (!queryParams) return {} | ||
| const object = {} | ||
| const queryParamsEntries = [...queryParams.entries()] | ||
| for (let i = 0; i < queryParamsEntries.length; i++) { | ||
| object[queryParamsEntries[i][0]] = queryParamsEntries[i][1] | ||
| } | ||
| return object | ||
| } | ||
| /** | ||
| * Match a path to a route | ||
| * | ||
| * @param {object} hashObject | ||
| * @param {Route[]} routes | ||
| * @returns {Route} | ||
| */ | ||
| export const matchHash = ({ hash, path, queryParams }, routes = []) => { | ||
| // remove trailing slashes | ||
| const originalPath = path.replace(/^\/+|\/+$/g, '') | ||
| const originalNormalizedPath = normalizePath(path) | ||
| const override = { | ||
| hash: hash, | ||
| queryParams: queryParamsToObject(queryParams), | ||
| path: path, | ||
| } | ||
| /** @type {boolean|Route} */ | ||
| let matchingRoute = false | ||
| let i = 0 | ||
| while (!matchingRoute && i < routes.length) { | ||
| const route = routes[i] | ||
| const normalizedPath = normalizePath(route.path) | ||
| if (normalizePath(normalizedPath) === originalNormalizedPath) { | ||
| matchingRoute = makeRouteObject(route, override) | ||
| } else if (normalizedPath.indexOf(':') > -1) { | ||
| // match dynamic route parts | ||
| const dynamicRouteParts = [...normalizedPath.matchAll(/:([^\s/]+)/gi)] | ||
| // construct a regex for the route with dynamic parts | ||
| let dynamicRoutePartsRegex = normalizedPath | ||
| dynamicRouteParts.reverse().forEach((part) => { | ||
| dynamicRoutePartsRegex = | ||
| dynamicRoutePartsRegex.substring(0, part.index) + | ||
| '([^\\s/]+)' + | ||
| dynamicRoutePartsRegex.substring(part.index + part[0].length) | ||
| }) | ||
| dynamicRoutePartsRegex = '^' + dynamicRoutePartsRegex | ||
| // test if the constructed regex matches the path | ||
| const match = originalPath.match(new RegExp(`${dynamicRoutePartsRegex}`, 'i')) | ||
| if (match) { | ||
| // map the route params to a params object | ||
| override.params = dynamicRouteParts.reverse().reduce((acc, part, index) => { | ||
| acc[part[1]] = match[index + 1] | ||
| return acc | ||
| }, {}) | ||
| matchingRoute = makeRouteObject(route, override) | ||
| } | ||
| } else if (normalizedPath.endsWith('*')) { | ||
| const regex = new RegExp(normalizedPath.replace(/\/?\*/, '/?([^\\s]*)'), 'i') | ||
| const match = originalNormalizedPath.match(regex) | ||
| if (match) { | ||
| override.params = {} | ||
| if (match[1]) override.params.path = match[1] | ||
| matchingRoute = makeRouteObject(route, override) | ||
| } | ||
| } | ||
| i++ | ||
| } | ||
| // @ts-ignore - Remove me when we have a better way to handle this | ||
| return matchingRoute | ||
| } | ||
| /** | ||
| * Default Route options | ||
| * | ||
| */ | ||
| const defaultOptions = { | ||
| inHistory: true, | ||
| keepAlive: false, | ||
| passFocus: true, | ||
| reuseComponent: false, | ||
| } | ||
| const makeRouteObject = (route, overrides) => { | ||
| // FIX: exclude keepAlive from the destination route options. Unlike other | ||
| // overrides, keepAlive applies to the route being LEFT, not the destination. | ||
| // It is consumed by removeView() instead. | ||
| const { keepAlive: _keepAlive, ...destOverrides } = overrideOptions // eslint-disable-line no-unused-vars | ||
| const cleanRoute = { | ||
| hash: overrides.hash, | ||
| path: route.path, | ||
| component: route.component, | ||
| transition: 'transition' in route ? route.transition : fadeInFadeOutTransition, | ||
| options: { ...defaultOptions, ...route.options, ...destOverrides }, | ||
| announce: route.announce || false, | ||
| hooks: route.hooks || {}, | ||
| data: { ...route.data, ...navigationData, ...overrides.queryParams }, | ||
| params: overrides.params || {}, | ||
| meta: route.meta || {}, | ||
| } | ||
| return cleanRoute | ||
| } | ||
| /** | ||
| * Navigate to a route | ||
@@ -255,312 +104,248 @@ * | ||
| export const navigate = async function () { | ||
| // early return when in preventHashChange mode | ||
| if (preventHashChangeNavigation !== false) return | ||
| // early return when no routes | ||
| if (!this[symbols.parent][symbols.routes] || this[symbols.parent][symbols.routes].length === 0) | ||
| return | ||
| state.navigating = true | ||
| Announcer.stop() | ||
| Announcer.clear() | ||
| state.navigating = true | ||
| let reuse = false | ||
| if (preventHashChangeNavigation === false && this[symbols.parent][symbols.routes]) { | ||
| let previousRoute = currentRoute //? Object.assign({}, currentRoute) : undefined | ||
| let route = matchHash(getHash(location.hash), this[symbols.parent][symbols.routes]) | ||
| currentRoute = route | ||
| const hash = getHash(location.hash) | ||
| // try to find the route | ||
| let route = matchHash(hash, this[symbols.parent][symbols.routes], overrideOptions, navigationData) | ||
| if (route) { | ||
| const currentPath = currentRoute.path | ||
| let beforeEachResult | ||
| if (this[symbols.parent][symbols.routerHooks]) { | ||
| const hooks = this[symbols.parent][symbols.routerHooks] | ||
| if (hooks.beforeEach) { | ||
| try { | ||
| beforeEachResult = await hooks.beforeEach.call( | ||
| this[symbols.parent], | ||
| route, | ||
| previousRoute | ||
| ) | ||
| if (isString(beforeEachResult)) { | ||
| currentRoute = previousRoute | ||
| to(beforeEachResult) | ||
| return | ||
| } | ||
| } catch (error) { | ||
| Log.error('Error or Rejected Promise in "BeforeEach" Hook', error) | ||
| // early return when route not found | ||
| if (route === false) { | ||
| state.navigating = false | ||
| if (history.length > 0) { | ||
| preventHashChangeNavigation = true | ||
| currentRoute = previousRoute | ||
| window.history.back() | ||
| Log.error(`Route ${hash.hash} not found`) | ||
| const routerHooks = this[symbols.parent][symbols.routerHooks] | ||
| if (routerHooks && typeof routerHooks.error === 'function') { | ||
| routerHooks.error.call(this[symbols.parent], `Route ${hash.hash} not found`) | ||
| } | ||
| return | ||
| } | ||
| navigatingBack = false | ||
| state.navigating = false | ||
| return | ||
| } | ||
| } | ||
| // If the resolved result is an object, redirect if the path in the object was changed | ||
| if (isObject(beforeEachResult) === true && beforeEachResult.path !== currentPath) { | ||
| currentRoute = previousRoute | ||
| to(beforeEachResult.path, beforeEachResult.data, beforeEachResult.options) | ||
| return | ||
| } | ||
| // If the resolved result is false, cancel navigation | ||
| if (beforeEachResult === false && history.length > 0) { | ||
| preventHashChangeNavigation = true | ||
| currentRoute = previousRoute | ||
| window.history.back() | ||
| let reuse = false | ||
| let previousRoute = currentRoute | ||
| currentRoute = route | ||
| const currentPath = currentRoute.path | ||
| navigatingBack = false | ||
| state.navigating = false | ||
| return | ||
| } | ||
| } | ||
| } | ||
| // execute before each hook | ||
| const beforeEachResult = await executeBeforeHook( | ||
| this[symbols.parent][symbols.routerHooks], | ||
| 'beforeEach', | ||
| this[symbols.parent], | ||
| route, | ||
| previousRoute, | ||
| currentPath | ||
| ) | ||
| if (beforeEachResult === false) { | ||
| preventHashChangeNavigation = false | ||
| return | ||
| } | ||
| let beforeHookOutput | ||
| if (route.hooks.before) { | ||
| try { | ||
| beforeHookOutput = await route.hooks.before.call( | ||
| this[symbols.parent], | ||
| route, | ||
| previousRoute | ||
| ) | ||
| if (isString(beforeHookOutput)) { | ||
| currentRoute = previousRoute | ||
| to(beforeHookOutput) | ||
| return | ||
| } | ||
| } catch (error) { | ||
| Log.error('Error or Rejected Promise in "Before" Hook', error) | ||
| // execute before route hook | ||
| const beforeResult = await executeBeforeHook( | ||
| route.hooks, | ||
| 'before', | ||
| this[symbols.parent], | ||
| route, | ||
| previousRoute, | ||
| currentPath | ||
| ) | ||
| if (beforeResult === false) { | ||
| preventHashChangeNavigation = false | ||
| return | ||
| } | ||
| if (history.length > 0) { | ||
| preventHashChangeNavigation = true | ||
| currentRoute = previousRoute | ||
| window.history.back() | ||
| // add the previous route (technically still the current route at this point) | ||
| // into the history stack when inHistory is true and we're not navigating back | ||
| // | ||
| // FIX: use truthy check instead of `!== undefined` because matchHash() | ||
| // can return `false`, which survives `!== undefined` but has no `.options`. | ||
| if ( | ||
| previousRoute && | ||
| previousRoute.options && | ||
| previousRoute.options.inHistory === true && | ||
| navigatingBack === false | ||
| ) { | ||
| history.push(previousRoute) | ||
| } | ||
| navigatingBack = false | ||
| state.navigating = false | ||
| return | ||
| } | ||
| } | ||
| // If the resolved result is an object, redirect if the path in the object was changed | ||
| if (isObject(beforeHookOutput) === true && beforeHookOutput.path !== currentPath) { | ||
| currentRoute = previousRoute | ||
| to(beforeHookOutput.path, beforeHookOutput.data, beforeHookOutput.options) | ||
| return | ||
| } | ||
| // If the resolved result is false, cancel navigation | ||
| if (beforeHookOutput === false && history.length > 0) { | ||
| preventHashChangeNavigation = true | ||
| currentRoute = previousRoute | ||
| window.history.back() | ||
| // a transition can be a function returning a dynamic transition object | ||
| // based on current and previous route | ||
| if (typeof route.transition === 'function') { | ||
| route.transition = route.transition(previousRoute, route) | ||
| } | ||
| navigatingBack = false | ||
| state.navigating = false | ||
| return | ||
| } | ||
| } | ||
| /** @type {import('../engines/L3/element.js').BlitsElement} */ | ||
| let holder | ||
| // add the previous route (technically still the current route at this point) | ||
| // into the history stack when inHistory is true and we're not navigating back | ||
| // | ||
| // FIX: use truthy check instead of `!== undefined` because matchHash() | ||
| // can return `false`, which survives `!== undefined` but has no `.options`. | ||
| if ( | ||
| previousRoute && | ||
| previousRoute.options && | ||
| previousRoute.options.inHistory === true && | ||
| navigatingBack === false | ||
| ) { | ||
| history.push(previousRoute) | ||
| } | ||
| /** @type {RouteViewWithOptionalDefault|undefined|null} */ | ||
| let view | ||
| let focus | ||
| // when navigating back let's see if we're navigating back to a route that was kept alive | ||
| if (navigatingBack === true && navigatingBackTo !== undefined) { | ||
| view = navigatingBackTo.view | ||
| focus = navigatingBackTo.focus | ||
| navigatingBackTo = null | ||
| } | ||
| // merge props with potential route params, navigation data and route data to be injected into the component instance | ||
| const props = { | ||
| ...this[symbols.props], | ||
| ...route.params, | ||
| ...route.data, | ||
| } | ||
| // a transition can be a function returning a dynamic transition object | ||
| // based on current and previous route | ||
| if (typeof route.transition === 'function') { | ||
| route.transition = route.transition(previousRoute, route) | ||
| } | ||
| // see if the component of the previous route can be reused for the | ||
| // current route | ||
| if ( | ||
| previousRoute && | ||
| route.options.reuseComponent === true && | ||
| route.options.keepAlive !== true && | ||
| route.component === previousRoute.component | ||
| ) { | ||
| reuse = true | ||
| view = this[symbols.children][this[symbols.children].length - 1] | ||
| for (const prop in props) { | ||
| view[symbols.props][prop] = props[prop] | ||
| } | ||
| } | ||
| /** @type {import('../engines/L3/element.js').BlitsElement} */ | ||
| let holder | ||
| // Announce route change if a message has been specified for this route | ||
| if (route.announce) { | ||
| if (typeof route.announce === 'string') { | ||
| Announcer.speak(route.announce) | ||
| } else { | ||
| Announcer.speak(route.announce.message, route.announce.politeness) | ||
| } | ||
| } | ||
| /** @type {RouteViewWithOptionalDefault|undefined|null} */ | ||
| let view | ||
| let focus | ||
| // when navigating back let's see if we're navigating back to a route that was kept alive | ||
| if (navigatingBack === true && navigatingBackTo !== undefined) { | ||
| view = navigatingBackTo.view | ||
| focus = navigatingBackTo.focus | ||
| navigatingBackTo = null | ||
| } | ||
| // merge props with potential route params, navigation data and route data to be injected into the component instance | ||
| const props = { | ||
| ...this[symbols.props], | ||
| ...route.params, | ||
| ...route.data, | ||
| } | ||
| // Update router state after announcements and final route resolution, | ||
| // right before initializing or restoring the view | ||
| state.path = route.path | ||
| state.params = Object.keys(route.params).length === 0 ? null : route.params | ||
| state.hash = route.hash | ||
| state.data = null | ||
| state.data = route.data || {} | ||
| // see if the component of the previous route can be reused for the | ||
| // current route | ||
| if ( | ||
| previousRoute && | ||
| route.options.reuseComponent === true && | ||
| route.options.keepAlive !== true && | ||
| route.component === previousRoute.component | ||
| ) { | ||
| reuse = true | ||
| view = this[symbols.children][this[symbols.children].length - 1] | ||
| for (const prop in props) { | ||
| view[symbols.props][prop] = props[prop] | ||
| } | ||
| } | ||
| // routing to a new page (instead of routing back to a keepAlive page) | ||
| if (view === undefined) { | ||
| // create a holder element for the new view | ||
| holder = stage.element({ parent: this[symbols.children][0] }) | ||
| holder.populate({}) | ||
| holder.set('w', '100%') | ||
| holder.set('h', '100%') | ||
| // Announce route change if a message has been specified for this route | ||
| if (route.announce) { | ||
| if (typeof route.announce === 'string') { | ||
| route.announce = { | ||
| message: route.announce, | ||
| } | ||
| } | ||
| Announcer.speak(route.announce.message, route.announce.politeness) | ||
| } | ||
| view = await loadPage.call(this, route, holder, props) | ||
| } else { | ||
| holder = view[symbols.holder] | ||
| // Update router state after announcements and final route resolution, | ||
| // right before initializing or restoring the view | ||
| state.path = route.path | ||
| state.params = Object.keys(route.params).length === 0 ? null : route.params | ||
| state.hash = route.hash | ||
| state.data = null | ||
| state.data = route.data || {} | ||
| if (!view) { | ||
| // create a holder element for the new view | ||
| holder = stage.element({ parent: this[symbols.children][0] }) | ||
| holder.populate({}) | ||
| holder.set('w', '100%') | ||
| holder.set('h', '100%') | ||
| view = await route.component({ props }, holder, this) | ||
| // is the component a dynamic module? | ||
| if (view[Symbol.toStringTag] === 'Module') { | ||
| if (view.default && typeof view.default === 'function') { | ||
| view = view.default({ props }, holder, this) | ||
| } else { | ||
| Log.error("Dynamic import doesn't have a default export or default is not a function") | ||
| } | ||
| } | ||
| if (typeof view === 'function') { | ||
| // had to inline this because the tscompiler does not like LHS reassignments | ||
| // that also change the type of the variable in a variable union | ||
| view = /** @type {BlitsComponentFactory} */ (view)({ props }, holder, this) | ||
| } | ||
| } else { | ||
| holder = view[symbols.holder] | ||
| // Check, whether cached view holder's alpha prop is exists in transition or not | ||
| let hasAlphaProp = false | ||
| if (route.transition.before) { | ||
| if (Array.isArray(route.transition.before)) { | ||
| for (let i = 0; i < route.transition.before.length; i++) { | ||
| if (route.transition.before[i].prop === 'alpha') { | ||
| hasAlphaProp = true | ||
| break | ||
| } | ||
| } | ||
| } else if (route.transition.before.prop === 'alpha') { | ||
| // Check, whether cached view holder's alpha prop is exists in transition or not | ||
| let hasAlphaProp = false | ||
| if (route.transition.before) { | ||
| if (Array.isArray(route.transition.before)) { | ||
| for (let i = 0; i < route.transition.before.length; i++) { | ||
| if (route.transition.before[i].prop === 'alpha') { | ||
| hasAlphaProp = true | ||
| break | ||
| } | ||
| } | ||
| // set holder alpha when alpha prop is not exists in route transition | ||
| if (hasAlphaProp === false) { | ||
| holder.set('alpha', 1) | ||
| } | ||
| } else if (route.transition.before.prop === 'alpha') { | ||
| hasAlphaProp = true | ||
| } | ||
| } | ||
| // set holder alpha when alpha prop is not exists in route transition | ||
| if (hasAlphaProp === false) { | ||
| holder.set('alpha', 1) | ||
| } | ||
| } | ||
| // store the new view as new child, only if we're not reusing the previous page component | ||
| if (reuse === false) { | ||
| this[symbols.children].push(view) | ||
| } | ||
| // store the new view as new child, only if we're not reusing the previous page component | ||
| if (reuse === false) { | ||
| this[symbols.children].push(view) | ||
| } | ||
| // keep reference to the previous focus for storing in cache | ||
| previousFocus = Focus.get() | ||
| // keep reference to the previous focus for storing in cache | ||
| previousFocus = Focus.get() | ||
| const children = this[symbols.children] | ||
| this.activeView = children[children.length - 1] | ||
| const children = this[symbols.children] | ||
| this.activeView = children[children.length - 1] | ||
| // set focus to the view that we're routing to (unless explicitly disabling passing focus) | ||
| if (route.options.passFocus !== false) { | ||
| focus ? focus.$focus() : /** @type {BlitsComponent} */ (view).$focus() | ||
| } | ||
| // set focus to the view that we're routing to (unless explicitly disabling passing focus) | ||
| if (route.options.passFocus !== false) { | ||
| focus ? focus.$focus() : /** @type {BlitsComponent} */ (view).$focus() | ||
| } | ||
| // apply before settings to holder element | ||
| if (route.transition.before) { | ||
| if (Array.isArray(route.transition.before)) { | ||
| for (let i = 0; i < route.transition.before.length; i++) { | ||
| holder.set(route.transition.before[i].prop, route.transition.before[i].value) | ||
| } | ||
| } else { | ||
| holder.set(route.transition.before.prop, route.transition.before.value) | ||
| } | ||
| } | ||
| // apply starting state of transition | ||
| if (route.transition.before) { | ||
| await executeTransition(route.transition.before, holder, false) | ||
| } | ||
| let shouldAnimate = false | ||
| let shouldAnimate = false | ||
| // apply out out transition on previous view if available, unless | ||
| // we're reusing the prvious page component | ||
| // FIX: truthy guard — previousRoute can be `false` (see history-push comment above). | ||
| if (previousRoute && reuse === false) { | ||
| // only animate when there is a previous route | ||
| shouldAnimate = true | ||
| const oldView = this[symbols.children].splice(1, 1).pop() | ||
| if (oldView) { | ||
| await removeView(previousRoute, oldView, route.transition.out, navigatingBack) | ||
| } | ||
| } | ||
| // apply out out transition on previous view if available, unless | ||
| // we're reusing the prvious page component | ||
| // FIX: truthy guard — previousRoute can be `false` (see history-push comment above). | ||
| if (previousRoute && reuse === false) { | ||
| // only animate when there is a previous route | ||
| shouldAnimate = true | ||
| let oldView = this[symbols.children].splice(1, 1).pop() | ||
| if (oldView) { | ||
| executeTransition(previousRoute.transition.out, oldView[symbols.holder], true) | ||
| // apply in transition | ||
| if (route.transition.in) { | ||
| if (Array.isArray(route.transition.in)) { | ||
| for (let i = 0; i < route.transition.in.length; i++) { | ||
| i === route.transition.in.length - 1 | ||
| ? await setOrAnimate(holder, route.transition.in[i], shouldAnimate) | ||
| : setOrAnimate(holder, route.transition.in[i], shouldAnimate) | ||
| } | ||
| } else { | ||
| await setOrAnimate(holder, route.transition.in, shouldAnimate) | ||
| // Resolve effective keepAlive: runtime override from $router.to() takes precedence | ||
| // over the static route config option | ||
| const keepAlive = | ||
| overrideOptions.keepAlive !== undefined | ||
| ? overrideOptions.keepAlive | ||
| : previousRoute.options && previousRoute.options.keepAlive | ||
| // cache the page when it's as 'keepAlive' instead of destroying | ||
| if ( | ||
| navigatingBack === false && | ||
| keepAlive === true && | ||
| previousRoute.options && | ||
| previousRoute.options.inHistory === true | ||
| ) { | ||
| const historyItem = history[history.length - 1] | ||
| if (historyItem !== undefined) { | ||
| historyItem.view = oldView | ||
| historyItem.focus = previousFocus | ||
| } | ||
| } | ||
| if (this[symbols.parent][symbols.routerHooks]) { | ||
| const hooks = this[symbols.parent][symbols.routerHooks] | ||
| if (hooks.afterEach) { | ||
| try { | ||
| await hooks.afterEach.call( | ||
| this[symbols.parent], | ||
| route, // to | ||
| previousRoute // from | ||
| ) | ||
| } catch (error) { | ||
| Log.error('Error in "AfterEach" Hook', error) | ||
| } | ||
| } | ||
| /* Destroy the view in the following cases: | ||
| * 1. Navigating forward, and the previous route is not configured with "keep alive" set to true. | ||
| * 2. Navigating back, and the previous route is configured with "keep alive" set to true. | ||
| * 3. Navigating back, and the previous route is not configured with "keep alive" set to true. | ||
| */ | ||
| if (previousRoute.options && (keepAlive !== true || navigatingBack === true)) { | ||
| oldView.destroy() | ||
| oldView = null | ||
| } | ||
| if (route.hooks.after) { | ||
| try { | ||
| await route.hooks.after.call( | ||
| this[symbols.parent], | ||
| route, // to | ||
| previousRoute // from | ||
| ) | ||
| } catch (error) { | ||
| Log.error('Error or Rejected Promise in "After" Hook', error) | ||
| } | ||
| } | ||
| } else { | ||
| Log.error(`Route ${route.hash} not found`) | ||
| const routerHooks = this[symbols.parent][symbols.routerHooks] | ||
| if (routerHooks && typeof routerHooks.error === 'function') { | ||
| routerHooks.error.call(this[symbols.parent], `Route ${route.hash} not found`) | ||
| } | ||
| previousFocus = null | ||
| } | ||
| } | ||
| // apply in transition | ||
| if (route.transition.in) await executeTransition(route.transition.in, holder, shouldAnimate) | ||
| // execute after each Hook | ||
| await executeAfterHook( | ||
| this[symbols.parent][symbols.routerHooks], | ||
| 'afterEach', | ||
| this[symbols.parent], | ||
| route, | ||
| previousRoute | ||
| ) | ||
| // execute after route Hook | ||
| await executeAfterHook(route.hooks, 'after', this[symbols.parent], route, previousRoute) | ||
| // Clear module-level variables after removeView has consumed them. | ||
@@ -578,79 +363,116 @@ // Placed here so it executes for all navigation flows, not only when | ||
| /** | ||
| * Remove the currently active view | ||
| * | ||
| * @param {Route} route | ||
| * @param {BlitsComponent} view | ||
| * @param {Object} transition | ||
| */ | ||
| const removeView = async (route, view, transition, navigatingBack) => { | ||
| // apply out transition | ||
| if (transition) { | ||
| if (Array.isArray(transition)) { | ||
| for (let i = 0; i < transition.length; i++) { | ||
| i === transition.length - 1 | ||
| ? await setOrAnimate(view[symbols.holder], transition[i]) | ||
| : setOrAnimate(view[symbols.holder], transition[i]) | ||
| const setOrAnimate = (element, transition, shouldAnimate = true) => { | ||
| if (shouldAnimate === true) { | ||
| return new Promise((resolve) => { | ||
| // resolve the promise in the transition end-callback | ||
| // ("extending" end callback when one is already specified) | ||
| let existingEndCallback = transition.end | ||
| transition.end = (...args) => { | ||
| existingEndCallback && existingEndCallback(args) | ||
| // null the callback to enable memory cleanup | ||
| existingEndCallback = null | ||
| resolve() | ||
| } | ||
| } else { | ||
| await setOrAnimate(view[symbols.holder], transition) | ||
| if (element !== undefined) element.set(transition.prop, { transition }) | ||
| else resolve() | ||
| }) | ||
| } else { | ||
| element !== undefined && element.set(transition.prop, transition.value) | ||
| return true | ||
| } | ||
| } | ||
| const executeBeforeHook = async function ( | ||
| hooks, | ||
| hookName, | ||
| parent, | ||
| route, | ||
| previousRoute, | ||
| currentPath | ||
| ) { | ||
| let result | ||
| if (hooks && hooks[hookName]) { | ||
| try { | ||
| result = await hooks[hookName].call(parent, route, previousRoute) | ||
| if (isString(result)) { | ||
| currentRoute = previousRoute | ||
| to(result) | ||
| return false | ||
| } | ||
| } catch (error) { | ||
| Log.error(`Error or Rejected Promise in "${hookName}" Hook`, error) | ||
| if (history.length > 0) { | ||
| preventHashChangeNavigation = true | ||
| currentRoute = previousRoute | ||
| window.history.back() | ||
| navigatingBack = false | ||
| state.navigating = false | ||
| return false | ||
| } | ||
| } | ||
| // If the resolved result is an object, redirect if the path in the object was changed | ||
| if (isObject(result) === true && result.path !== currentPath) { | ||
| currentRoute = previousRoute | ||
| to(result.path, result.data, result.options) | ||
| return false | ||
| } | ||
| // If the resolved result is false, cancel navigation | ||
| if (result === false && history.length > 0) { | ||
| preventHashChangeNavigation = true | ||
| currentRoute = previousRoute | ||
| window.history.back() | ||
| navigatingBack = false | ||
| state.navigating = false | ||
| return false | ||
| } | ||
| } | ||
| } | ||
| // Resolve effective keepAlive: runtime override from $router.to() takes precedence | ||
| // over the static route config option | ||
| const keepAlive = | ||
| overrideOptions.keepAlive !== undefined | ||
| ? overrideOptions.keepAlive | ||
| : route.options && route.options.keepAlive | ||
| const loadPage = async function (route, holder, props) { | ||
| let view = await route.component({ props }, holder, this) | ||
| // cache the page when it's as 'keepAlive' instead of destroying | ||
| if ( | ||
| navigatingBack === false && | ||
| route.options && | ||
| keepAlive === true && | ||
| route.options.inHistory === true | ||
| ) { | ||
| const historyItem = history[history.length - 1] | ||
| if (historyItem !== undefined) { | ||
| historyItem.view = view | ||
| historyItem.focus = previousFocus | ||
| // is the component a dynamic module? | ||
| if (view[Symbol.toStringTag] === 'Module') { | ||
| if (view.default && typeof view.default === 'function') { | ||
| view = view.default({ props }, holder, this) | ||
| } else { | ||
| Log.error("Dynamic import doesn't have a default export or default is not a function") | ||
| } | ||
| } | ||
| /* Destroy the view in the following cases: | ||
| * 1. Navigating forward, and the previous route is not configured with "keep alive" set to true. | ||
| * 2. Navigating back, and the previous route is configured with "keep alive" set to true. | ||
| * 3. Navigating back, and the previous route is not configured with "keep alive" set to true. | ||
| */ | ||
| if (route.options && (keepAlive !== true || navigatingBack === true)) { | ||
| view.destroy() | ||
| view = null | ||
| if (typeof view === 'function') { | ||
| // had to inline this because the tscompiler does not like LHS reassignments | ||
| // that also change the type of the variable in a variable union | ||
| view = /** @type {BlitsComponentFactory} */ (view)({ props }, holder, this) | ||
| } | ||
| previousFocus = null | ||
| route = null | ||
| return view | ||
| } | ||
| const setOrAnimate = (node, transition, shouldAnimate = true) => { | ||
| return new Promise((resolve) => { | ||
| if (shouldAnimate === true) { | ||
| // resolve the promise in the transition end-callback | ||
| // ("extending" end callback when one is already specified) | ||
| let existingEndCallback = transition.end | ||
| transition.end = (...args) => { | ||
| existingEndCallback && existingEndCallback(args) | ||
| // null the callback to enable memory cleanup | ||
| existingEndCallback = null | ||
| resolve() | ||
| } | ||
| if (node !== undefined) node.set(transition.prop, { transition }) | ||
| else resolve() | ||
| } else { | ||
| node !== undefined && node.set(transition.prop, transition.value) | ||
| resolve() | ||
| const executeAfterHook = async function (hooks, hookName, parent, route, previousRoute) { | ||
| if (hooks && hooks[hookName]) { | ||
| try { | ||
| await hooks[hookName].call( | ||
| parent, | ||
| route, // to | ||
| previousRoute // from | ||
| ) | ||
| } catch (error) { | ||
| Log.error(`Error or Rejected Promise in "${hookName}" Hook`, error) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| const executeTransition = async (transition, element, animate) => { | ||
| if (Array.isArray(transition)) { | ||
| for (let i = 0; i < transition.length; i++) { | ||
| i === transition.length - 1 | ||
| ? await setOrAnimate(element, transition[i], animate) | ||
| : setOrAnimate(element, transition[i], animate) | ||
| } | ||
| } else { | ||
| await setOrAnimate(element, transition, animate) | ||
| } | ||
| } | ||
| export const to = (path, data = {}, options = {}) => { | ||
@@ -697,3 +519,8 @@ navigationData = data | ||
| path = path.replace(hashEnd, '') | ||
| const route = matchHash(getHash(path), this[symbols.parent][symbols.routes]) | ||
| const route = matchHash( | ||
| getHash(path), | ||
| this[symbols.parent][symbols.routes], | ||
| overrideOptions, | ||
| navigationData | ||
| ) | ||
@@ -700,0 +527,0 @@ if (route && backtrack) { |
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Unidentified License
LicenseSomething that seems like a license was found, but its contents could not be matched with a known license.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Unidentified License
LicenseSomething that seems like a license was found, but its contents could not be matched with a known license.
Found 1 instance in 1 package
104
0.97%35
-5.41%638263
-0.26%13395
-0.14%