@lightningjs/blits 2.1.3 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/index.d.ts +15 -2
- package/package.json +2 -2
- package/src/announcer/announcer.js +1 -1
- package/src/announcer/speechSynthesis.js +1 -1
- package/src/component/base/index.js +15 -1
- package/src/component.js +6 -2
- package/src/engines/L3/element.js +8 -0
- package/src/lib/codegenerator/generator.js +0 -1
- package/src/router/router.js +322 -495
- package/src/router/utils.js +158 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.3.0
|
|
4
|
+
|
|
5
|
+
_20 may 2026_
|
|
6
|
+
|
|
7
|
+
- Refactored router functionality
|
|
8
|
+
- Replaced `window` reference with `self` reference in announcer
|
|
9
|
+
|
|
10
|
+
## v2.2.0
|
|
11
|
+
|
|
12
|
+
_11 may 2026_
|
|
13
|
+
|
|
14
|
+
- Exposed built-in reactivity to be used in custom Blits plugins
|
|
15
|
+
- Cleaned up unused key names from default key mapping
|
|
16
|
+
- Improved documentation
|
|
17
|
+
- Fixed reactive color not working for components
|
|
18
|
+
- Added EOL guard for Blits elements to prevent potential race conditions
|
|
19
|
+
- Updated MSDF generator version in boilerplate
|
|
20
|
+
- Fixed / improved GH workflow for automated tests
|
|
21
|
+
- Upgraded renderer to 3.0.3
|
|
22
|
+
|
|
23
|
+
|
|
3
24
|
## v2.1.3
|
|
4
25
|
|
|
5
26
|
_16 apr 2026_
|
package/index.d.ts
CHANGED
|
@@ -405,6 +405,11 @@ declare module '@lightningjs/blits' {
|
|
|
405
405
|
*/
|
|
406
406
|
readonly $parent: ComponentBase | undefined,
|
|
407
407
|
|
|
408
|
+
/**
|
|
409
|
+
* Human-readable identifier for this component instance (e.g. "MyComponent_0")
|
|
410
|
+
*/
|
|
411
|
+
readonly $componentId: string,
|
|
412
|
+
|
|
408
413
|
/**
|
|
409
414
|
* Indicates whether the component currently is hovered
|
|
410
415
|
*
|
|
@@ -536,6 +541,14 @@ declare module '@lightningjs/blits' {
|
|
|
536
541
|
* Router instance
|
|
537
542
|
*/
|
|
538
543
|
$router: Router
|
|
544
|
+
/**
|
|
545
|
+
* Creates a reactive object. Changes to properties on the returned object
|
|
546
|
+
* will automatically trigger re-renders in any component that accesses them.
|
|
547
|
+
*
|
|
548
|
+
* Pre-configured with the app's reactivity mode and global scope.
|
|
549
|
+
* Primarily intended for use inside custom plugins.
|
|
550
|
+
*/
|
|
551
|
+
$reactive: <T extends Record<string, any>>(target: T) => T
|
|
539
552
|
/**
|
|
540
553
|
* Dynamically set the size of a component holder node
|
|
541
554
|
*/
|
|
@@ -1331,7 +1344,7 @@ declare module '@lightningjs/blits' {
|
|
|
1331
1344
|
* Should do all necessary setup and ideally return an object with
|
|
1332
1345
|
* properties or methods that can be used in the App
|
|
1333
1346
|
*/
|
|
1334
|
-
plugin: () => any
|
|
1347
|
+
plugin: (options?: any) => any
|
|
1335
1348
|
}
|
|
1336
1349
|
|
|
1337
1350
|
|
|
@@ -1389,7 +1402,7 @@ declare module '@lightningjs/blits' {
|
|
|
1389
1402
|
* Object with options to be passed into the plugin instantiation method.
|
|
1390
1403
|
* Can be used to set default values
|
|
1391
1404
|
*/
|
|
1392
|
-
Plugin(plugin: Plugin, nameOrOptions?: string | object , options?: object) : void
|
|
1405
|
+
Plugin(plugin: Plugin | ((options?: any) => any), nameOrOptions?: string | object , options?: object) : void
|
|
1393
1406
|
}
|
|
1394
1407
|
|
|
1395
1408
|
const Blits: Blits;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightningjs/blits",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Blits: The Lightning 3 App Development Framework",
|
|
5
5
|
"bin": "bin/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"tape": "^5.5.0"
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@lightningjs/renderer": "^3.0.
|
|
78
|
+
"@lightningjs/renderer": "^3.0.3",
|
|
79
79
|
"magic-string": "^0.30.21"
|
|
80
80
|
},
|
|
81
81
|
"repository": {
|
|
@@ -27,7 +27,7 @@ let debounce = null
|
|
|
27
27
|
|
|
28
28
|
// Global default utterance options
|
|
29
29
|
let globalDefaultOptions = {
|
|
30
|
-
enableUtteranceKeepAlive: !/android/i.test((
|
|
30
|
+
enableUtteranceKeepAlive: !/android/i.test((self.navigator || {}).userAgent || ''),
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const noopAnnouncement = {
|
|
@@ -22,8 +22,22 @@ import router from './router.js'
|
|
|
22
22
|
import announcer from './announcer.js'
|
|
23
23
|
import utils from './utils.js'
|
|
24
24
|
import symbols from '../../lib/symbols.js'
|
|
25
|
+
import { reactive } from '../../lib/reactivity/reactive.js'
|
|
26
|
+
import Settings from '../../settings.js'
|
|
25
27
|
|
|
26
|
-
export const shared = {
|
|
28
|
+
export const shared = {
|
|
29
|
+
...events,
|
|
30
|
+
...router,
|
|
31
|
+
...announcer,
|
|
32
|
+
$reactive: {
|
|
33
|
+
value: function (target) {
|
|
34
|
+
return reactive(target, Settings.get('reactivityMode'), true)
|
|
35
|
+
},
|
|
36
|
+
writable: false,
|
|
37
|
+
enumerable: true,
|
|
38
|
+
configurable: false,
|
|
39
|
+
},
|
|
40
|
+
}
|
|
27
41
|
|
|
28
42
|
export default Object.defineProperties(
|
|
29
43
|
{
|
package/src/component.js
CHANGED
|
@@ -432,6 +432,10 @@ const Component = (name = required('name'), config = required('config')) => {
|
|
|
432
432
|
*/
|
|
433
433
|
const factory = (options = {}, parentEl, parentComponent, rootComponent) => {
|
|
434
434
|
if (Base[symbols['launched']] === false) {
|
|
435
|
+
// create a context with shared properties so this.$reactive (etc.)
|
|
436
|
+
// is available during plugin initialization
|
|
437
|
+
const pluginContext = Object.defineProperties({}, shared)
|
|
438
|
+
|
|
435
439
|
// Register user defined plugins once on the Base object (after launch)
|
|
436
440
|
const pluginKeys = Object.keys(plugins)
|
|
437
441
|
const pluginKeysLength = pluginKeys.length
|
|
@@ -449,8 +453,8 @@ const Component = (name = required('name'), config = required('config')) => {
|
|
|
449
453
|
const plugin = plugins[pluginName]
|
|
450
454
|
|
|
451
455
|
pluginInstances[prefixedPluginName] = {
|
|
452
|
-
// instantiate the plugin,
|
|
453
|
-
value: Object.defineProperties(plugin.plugin(plugin.options), shared),
|
|
456
|
+
// instantiate the plugin with shared context, then apply shared to the result
|
|
457
|
+
value: Object.defineProperties(plugin.plugin.call(pluginContext, plugin.options), shared),
|
|
454
458
|
writable: false,
|
|
455
459
|
enumerable: true,
|
|
456
460
|
configurable: true,
|
|
@@ -629,6 +629,7 @@ const Element = {
|
|
|
629
629
|
* @param {Object} data - Framework inspector metadata to merge
|
|
630
630
|
*/
|
|
631
631
|
setInspectorMetadata(data) {
|
|
632
|
+
if (this.eol === true) return
|
|
632
633
|
// Early return if inspector not enabled (performance optimization)
|
|
633
634
|
if (inspectorEnabled !== true) {
|
|
634
635
|
return
|
|
@@ -666,6 +667,7 @@ const Element = {
|
|
|
666
667
|
* @returns {void}
|
|
667
668
|
*/
|
|
668
669
|
set(prop, value) {
|
|
670
|
+
if (this.eol === true) return
|
|
669
671
|
if (value === undefined) return
|
|
670
672
|
// @ts-ignore
|
|
671
673
|
if (this.props.raw[prop] === value) return
|
|
@@ -701,6 +703,8 @@ const Element = {
|
|
|
701
703
|
}
|
|
702
704
|
},
|
|
703
705
|
animate(prop, value, transition) {
|
|
706
|
+
if (this.eol === true) return
|
|
707
|
+
|
|
704
708
|
// Clear any existing debounce timeout for this property
|
|
705
709
|
if (this.debounceTimeouts[prop] !== undefined) {
|
|
706
710
|
clearTimeout(this.debounceTimeouts[prop])
|
|
@@ -812,6 +816,9 @@ const Element = {
|
|
|
812
816
|
f.start()
|
|
813
817
|
},
|
|
814
818
|
destroy() {
|
|
819
|
+
if (this.eol === true) return
|
|
820
|
+
this.eol = true
|
|
821
|
+
|
|
815
822
|
if (this.node === null) return
|
|
816
823
|
|
|
817
824
|
Log.debug('Deleting Node', this.nodeId)
|
|
@@ -903,6 +910,7 @@ export default (config, component) => {
|
|
|
903
910
|
inspectorEnabled = Settings.get('inspector', false)
|
|
904
911
|
}
|
|
905
912
|
return Object.assign(Object.create(Element), {
|
|
913
|
+
eol: false,
|
|
906
914
|
props: Object.assign(Object.create(propsTransformer), { props: {} }),
|
|
907
915
|
scheduledTransitions: {},
|
|
908
916
|
debounceTimeouts: {},
|
|
@@ -225,7 +225,6 @@ const generateElementCode = function (
|
|
|
225
225
|
const value = templateObject[key]
|
|
226
226
|
|
|
227
227
|
if (isReactiveKey(key)) {
|
|
228
|
-
if (options.holder && key === ':color') return
|
|
229
228
|
if (options.holder) {
|
|
230
229
|
this.effectsCode.push(`
|
|
231
230
|
if(typeof skips === 'undefined' || (typeof skips[${counter}] === 'undefined' ||
|
package/src/router/router.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* SPDX-License-Identifier: Apache-2.0
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import { getHash, isObject, isString, matchHash } from './utils.js'
|
|
19
19
|
import { reactive } from '../lib/reactivity/reactive.js'
|
|
20
20
|
|
|
21
21
|
import symbols from '../lib/symbols.js'
|
|
@@ -85,157 +85,6 @@ let previousFocus
|
|
|
85
85
|
// Skips internal router navigation when set to true only for the next "navigate"
|
|
86
86
|
// execution, needed for window.history management
|
|
87
87
|
let preventHashChangeNavigation = false
|
|
88
|
-
/**
|
|
89
|
-
* Get the current hash
|
|
90
|
-
* @returns {Hash}
|
|
91
|
-
*/
|
|
92
|
-
export const getHash = (hash) => {
|
|
93
|
-
if (!hash) hash = '/'
|
|
94
|
-
const hashParts = hash.replace(/^#/, '').split('?')
|
|
95
|
-
return {
|
|
96
|
-
path: hashParts[0],
|
|
97
|
-
queryParams: new URLSearchParams(hashParts[1]),
|
|
98
|
-
hash: hash,
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const normalizePath = (path) => {
|
|
103
|
-
return (
|
|
104
|
-
path
|
|
105
|
-
// remove leading and trailing slashes
|
|
106
|
-
.replace(/^\/+|\/+$/g, '')
|
|
107
|
-
.toLowerCase()
|
|
108
|
-
)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Check if a value is an object
|
|
113
|
-
* @param {any} v
|
|
114
|
-
* @returns {boolean} True if v is an object
|
|
115
|
-
*/
|
|
116
|
-
const isObject = (v) => typeof v === 'object' && v !== null
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Check if a value is a function
|
|
120
|
-
* @param {any} v
|
|
121
|
-
* @returns {boolean} True if v is a string
|
|
122
|
-
*/
|
|
123
|
-
const isString = (v) => typeof v === 'string'
|
|
124
|
-
|
|
125
|
-
const queryParamsToObject = (queryParams) => {
|
|
126
|
-
if (!queryParams) return {}
|
|
127
|
-
const object = {}
|
|
128
|
-
const queryParamsEntries = [...queryParams.entries()]
|
|
129
|
-
for (let i = 0; i < queryParamsEntries.length; i++) {
|
|
130
|
-
object[queryParamsEntries[i][0]] = queryParamsEntries[i][1]
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return object
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Match a path to a route
|
|
138
|
-
*
|
|
139
|
-
* @param {object} hashObject
|
|
140
|
-
* @param {Route[]} routes
|
|
141
|
-
* @returns {Route}
|
|
142
|
-
*/
|
|
143
|
-
export const matchHash = ({ hash, path, queryParams }, routes = []) => {
|
|
144
|
-
// remove trailing slashes
|
|
145
|
-
const originalPath = path.replace(/^\/+|\/+$/g, '')
|
|
146
|
-
const originalNormalizedPath = normalizePath(path)
|
|
147
|
-
|
|
148
|
-
const override = {
|
|
149
|
-
hash: hash,
|
|
150
|
-
queryParams: queryParamsToObject(queryParams),
|
|
151
|
-
path: path,
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** @type {boolean|Route} */
|
|
155
|
-
let matchingRoute = false
|
|
156
|
-
let i = 0
|
|
157
|
-
while (!matchingRoute && i < routes.length) {
|
|
158
|
-
const route = routes[i]
|
|
159
|
-
|
|
160
|
-
const normalizedPath = normalizePath(route.path)
|
|
161
|
-
if (normalizePath(normalizedPath) === originalNormalizedPath) {
|
|
162
|
-
matchingRoute = makeRouteObject(route, override)
|
|
163
|
-
} else if (normalizedPath.indexOf(':') > -1) {
|
|
164
|
-
// match dynamic route parts
|
|
165
|
-
const dynamicRouteParts = [...normalizedPath.matchAll(/:([^\s/]+)/gi)]
|
|
166
|
-
|
|
167
|
-
// construct a regex for the route with dynamic parts
|
|
168
|
-
let dynamicRoutePartsRegex = normalizedPath
|
|
169
|
-
dynamicRouteParts.reverse().forEach((part) => {
|
|
170
|
-
dynamicRoutePartsRegex =
|
|
171
|
-
dynamicRoutePartsRegex.substring(0, part.index) +
|
|
172
|
-
'([^\\s/]+)' +
|
|
173
|
-
dynamicRoutePartsRegex.substring(part.index + part[0].length)
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
dynamicRoutePartsRegex = '^' + dynamicRoutePartsRegex
|
|
177
|
-
|
|
178
|
-
// test if the constructed regex matches the path
|
|
179
|
-
const match = originalPath.match(new RegExp(`${dynamicRoutePartsRegex}`, 'i'))
|
|
180
|
-
|
|
181
|
-
if (match) {
|
|
182
|
-
// map the route params to a params object
|
|
183
|
-
override.params = dynamicRouteParts.reverse().reduce((acc, part, index) => {
|
|
184
|
-
acc[part[1]] = match[index + 1]
|
|
185
|
-
return acc
|
|
186
|
-
}, {})
|
|
187
|
-
|
|
188
|
-
matchingRoute = makeRouteObject(route, override)
|
|
189
|
-
}
|
|
190
|
-
} else if (normalizedPath.endsWith('*')) {
|
|
191
|
-
const regex = new RegExp(normalizedPath.replace(/\/?\*/, '/?([^\\s]*)'), 'i')
|
|
192
|
-
const match = originalNormalizedPath.match(regex)
|
|
193
|
-
|
|
194
|
-
if (match) {
|
|
195
|
-
override.params = {}
|
|
196
|
-
if (match[1]) override.params.path = match[1]
|
|
197
|
-
matchingRoute = makeRouteObject(route, override)
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
i++
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// @ts-ignore - Remove me when we have a better way to handle this
|
|
204
|
-
return matchingRoute
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Default Route options
|
|
209
|
-
*
|
|
210
|
-
*/
|
|
211
|
-
const defaultOptions = {
|
|
212
|
-
inHistory: true,
|
|
213
|
-
keepAlive: false,
|
|
214
|
-
passFocus: true,
|
|
215
|
-
reuseComponent: false,
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const makeRouteObject = (route, overrides) => {
|
|
219
|
-
// FIX: exclude keepAlive from the destination route options. Unlike other
|
|
220
|
-
// overrides, keepAlive applies to the route being LEFT, not the destination.
|
|
221
|
-
// It is consumed by removeView() instead.
|
|
222
|
-
const { keepAlive: _keepAlive, ...destOverrides } = overrideOptions // eslint-disable-line no-unused-vars
|
|
223
|
-
|
|
224
|
-
const cleanRoute = {
|
|
225
|
-
hash: overrides.hash,
|
|
226
|
-
path: route.path,
|
|
227
|
-
component: route.component,
|
|
228
|
-
transition: 'transition' in route ? route.transition : fadeInFadeOutTransition,
|
|
229
|
-
options: { ...defaultOptions, ...route.options, ...destOverrides },
|
|
230
|
-
announce: route.announce || false,
|
|
231
|
-
hooks: route.hooks || {},
|
|
232
|
-
data: { ...route.data, ...navigationData, ...overrides.queryParams },
|
|
233
|
-
params: overrides.params || {},
|
|
234
|
-
meta: route.meta || {},
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return cleanRoute
|
|
238
|
-
}
|
|
239
88
|
|
|
240
89
|
/**
|
|
241
90
|
* Navigate to a route
|
|
@@ -250,316 +99,252 @@ const makeRouteObject = (route, overrides) => {
|
|
|
250
99
|
* @returns {Promise<void>}
|
|
251
100
|
*/
|
|
252
101
|
export const navigate = async function () {
|
|
102
|
+
// early return when in preventHashChange mode
|
|
103
|
+
if (preventHashChangeNavigation !== false) return
|
|
104
|
+
// early return when no routes
|
|
105
|
+
if (!this[symbols.parent][symbols.routes] || this[symbols.parent][symbols.routes].length === 0)
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
state.navigating = true
|
|
109
|
+
|
|
253
110
|
Announcer.stop()
|
|
254
111
|
Announcer.clear()
|
|
255
|
-
state.navigating = true
|
|
256
|
-
let reuse = false
|
|
257
|
-
if (preventHashChangeNavigation === false && this[symbols.parent][symbols.routes]) {
|
|
258
|
-
let previousRoute = currentRoute //? Object.assign({}, currentRoute) : undefined
|
|
259
|
-
let route = matchHash(getHash(location.hash), this[symbols.parent][symbols.routes])
|
|
260
|
-
|
|
261
|
-
currentRoute = route
|
|
262
|
-
|
|
263
|
-
if (route) {
|
|
264
|
-
const currentPath = currentRoute.path
|
|
265
|
-
let beforeEachResult
|
|
266
|
-
if (this[symbols.parent][symbols.routerHooks]) {
|
|
267
|
-
const hooks = this[symbols.parent][symbols.routerHooks]
|
|
268
|
-
if (hooks.beforeEach) {
|
|
269
|
-
try {
|
|
270
|
-
beforeEachResult = await hooks.beforeEach.call(
|
|
271
|
-
this[symbols.parent],
|
|
272
|
-
route,
|
|
273
|
-
previousRoute
|
|
274
|
-
)
|
|
275
|
-
if (isString(beforeEachResult)) {
|
|
276
|
-
currentRoute = previousRoute
|
|
277
|
-
to(beforeEachResult)
|
|
278
|
-
return
|
|
279
|
-
}
|
|
280
|
-
} catch (error) {
|
|
281
|
-
Log.error('Error or Rejected Promise in "BeforeEach" Hook', error)
|
|
282
|
-
|
|
283
|
-
if (history.length > 0) {
|
|
284
|
-
preventHashChangeNavigation = true
|
|
285
|
-
currentRoute = previousRoute
|
|
286
|
-
window.history.back()
|
|
287
|
-
|
|
288
|
-
navigatingBack = false
|
|
289
|
-
state.navigating = false
|
|
290
|
-
return
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
// If the resolved result is an object, redirect if the path in the object was changed
|
|
294
|
-
if (isObject(beforeEachResult) === true && beforeEachResult.path !== currentPath) {
|
|
295
|
-
currentRoute = previousRoute
|
|
296
|
-
to(beforeEachResult.path, beforeEachResult.data, beforeEachResult.options)
|
|
297
|
-
return
|
|
298
|
-
}
|
|
299
|
-
// If the resolved result is false, cancel navigation
|
|
300
|
-
if (beforeEachResult === false && history.length > 0) {
|
|
301
|
-
preventHashChangeNavigation = true
|
|
302
|
-
currentRoute = previousRoute
|
|
303
|
-
window.history.back()
|
|
304
|
-
|
|
305
|
-
navigatingBack = false
|
|
306
|
-
state.navigating = false
|
|
307
|
-
return
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
112
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
beforeHookOutput = await route.hooks.before.call(
|
|
316
|
-
this[symbols.parent],
|
|
317
|
-
route,
|
|
318
|
-
previousRoute
|
|
319
|
-
)
|
|
320
|
-
if (isString(beforeHookOutput)) {
|
|
321
|
-
currentRoute = previousRoute
|
|
322
|
-
to(beforeHookOutput)
|
|
323
|
-
return
|
|
324
|
-
}
|
|
325
|
-
} catch (error) {
|
|
326
|
-
Log.error('Error or Rejected Promise in "Before" Hook', error)
|
|
113
|
+
const hash = getHash(location.hash)
|
|
114
|
+
// try to find the route
|
|
115
|
+
let route = matchHash(hash, this[symbols.parent][symbols.routes], overrideOptions, navigationData)
|
|
327
116
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
window.history.back()
|
|
117
|
+
// early return when route not found
|
|
118
|
+
if (route === false) {
|
|
119
|
+
state.navigating = false
|
|
332
120
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
currentRoute = previousRoute
|
|
341
|
-
to(beforeHookOutput.path, beforeHookOutput.data, beforeHookOutput.options)
|
|
342
|
-
return
|
|
343
|
-
}
|
|
344
|
-
// If the resolved result is false, cancel navigation
|
|
345
|
-
if (beforeHookOutput === false && history.length > 0) {
|
|
346
|
-
preventHashChangeNavigation = true
|
|
347
|
-
currentRoute = previousRoute
|
|
348
|
-
window.history.back()
|
|
349
|
-
|
|
350
|
-
navigatingBack = false
|
|
351
|
-
state.navigating = false
|
|
352
|
-
return
|
|
353
|
-
}
|
|
354
|
-
}
|
|
121
|
+
Log.error(`Route ${hash.hash} not found`)
|
|
122
|
+
const routerHooks = this[symbols.parent][symbols.routerHooks]
|
|
123
|
+
if (routerHooks && typeof routerHooks.error === 'function') {
|
|
124
|
+
routerHooks.error.call(this[symbols.parent], `Route ${hash.hash} not found`)
|
|
125
|
+
}
|
|
126
|
+
return
|
|
127
|
+
}
|
|
355
128
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
129
|
+
let reuse = false
|
|
130
|
+
let previousRoute = currentRoute
|
|
131
|
+
currentRoute = route
|
|
132
|
+
const currentPath = currentRoute.path
|
|
133
|
+
|
|
134
|
+
// execute before each hook
|
|
135
|
+
const beforeEachResult = await executeBeforeHook(
|
|
136
|
+
this[symbols.parent][symbols.routerHooks],
|
|
137
|
+
'beforeEach',
|
|
138
|
+
this[symbols.parent],
|
|
139
|
+
route,
|
|
140
|
+
previousRoute,
|
|
141
|
+
currentPath
|
|
142
|
+
)
|
|
143
|
+
if (beforeEachResult === false) {
|
|
144
|
+
preventHashChangeNavigation = false
|
|
145
|
+
return
|
|
146
|
+
}
|
|
369
147
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
148
|
+
// execute before route hook
|
|
149
|
+
const beforeResult = await executeBeforeHook(
|
|
150
|
+
route.hooks,
|
|
151
|
+
'before',
|
|
152
|
+
this[symbols.parent],
|
|
153
|
+
route,
|
|
154
|
+
previousRoute,
|
|
155
|
+
currentPath
|
|
156
|
+
)
|
|
157
|
+
if (beforeResult === false) {
|
|
158
|
+
preventHashChangeNavigation = false
|
|
159
|
+
return
|
|
160
|
+
}
|
|
375
161
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const props = {
|
|
390
|
-
...this[symbols.props],
|
|
391
|
-
...route.params,
|
|
392
|
-
...route.data,
|
|
393
|
-
}
|
|
162
|
+
// add the previous route (technically still the current route at this point)
|
|
163
|
+
// into the history stack when inHistory is true and we're not navigating back
|
|
164
|
+
//
|
|
165
|
+
// FIX: use truthy check instead of `!== undefined` because matchHash()
|
|
166
|
+
// can return `false`, which survives `!== undefined` but has no `.options`.
|
|
167
|
+
if (
|
|
168
|
+
previousRoute &&
|
|
169
|
+
previousRoute.options &&
|
|
170
|
+
previousRoute.options.inHistory === true &&
|
|
171
|
+
navigatingBack === false
|
|
172
|
+
) {
|
|
173
|
+
history.push(previousRoute)
|
|
174
|
+
}
|
|
394
175
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
route.options.keepAlive !== true &&
|
|
401
|
-
route.component === previousRoute.component
|
|
402
|
-
) {
|
|
403
|
-
reuse = true
|
|
404
|
-
view = this[symbols.children][this[symbols.children].length - 1]
|
|
405
|
-
for (const prop in props) {
|
|
406
|
-
view[symbols.props][prop] = props[prop]
|
|
407
|
-
}
|
|
408
|
-
}
|
|
176
|
+
// a transition can be a function returning a dynamic transition object
|
|
177
|
+
// based on current and previous route
|
|
178
|
+
if (typeof route.transition === 'function') {
|
|
179
|
+
route.transition = route.transition(previousRoute, route)
|
|
180
|
+
}
|
|
409
181
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
182
|
+
/** @type {import('../engines/L3/element.js').BlitsElement} */
|
|
183
|
+
let holder
|
|
184
|
+
|
|
185
|
+
/** @type {RouteViewWithOptionalDefault|undefined|null} */
|
|
186
|
+
let view
|
|
187
|
+
let focus
|
|
188
|
+
// when navigating back let's see if we're navigating back to a route that was kept alive
|
|
189
|
+
if (navigatingBack === true && navigatingBackTo !== undefined) {
|
|
190
|
+
view = navigatingBackTo.view
|
|
191
|
+
focus = navigatingBackTo.focus
|
|
192
|
+
navigatingBackTo = null
|
|
193
|
+
}
|
|
194
|
+
// merge props with potential route params, navigation data and route data to be injected into the component instance
|
|
195
|
+
const props = {
|
|
196
|
+
...this[symbols.props],
|
|
197
|
+
...route.params,
|
|
198
|
+
...route.data,
|
|
199
|
+
}
|
|
419
200
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
view = await route.component({ props }, holder, this)
|
|
436
|
-
|
|
437
|
-
// is the component a dynamic module?
|
|
438
|
-
if (view[Symbol.toStringTag] === 'Module') {
|
|
439
|
-
if (view.default && typeof view.default === 'function') {
|
|
440
|
-
view = view.default({ props }, holder, this)
|
|
441
|
-
} else {
|
|
442
|
-
Log.error("Dynamic import doesn't have a default export or default is not a function")
|
|
443
|
-
}
|
|
444
|
-
}
|
|
201
|
+
// see if the component of the previous route can be reused for the
|
|
202
|
+
// current route
|
|
203
|
+
if (
|
|
204
|
+
previousRoute &&
|
|
205
|
+
route.options.reuseComponent === true &&
|
|
206
|
+
route.options.keepAlive !== true &&
|
|
207
|
+
route.component === previousRoute.component
|
|
208
|
+
) {
|
|
209
|
+
reuse = true
|
|
210
|
+
view = this[symbols.children][this[symbols.children].length - 1]
|
|
211
|
+
for (const prop in props) {
|
|
212
|
+
view[symbols.props][prop] = props[prop]
|
|
213
|
+
}
|
|
214
|
+
}
|
|
445
215
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
216
|
+
// Announce route change if a message has been specified for this route
|
|
217
|
+
if (route.announce) {
|
|
218
|
+
if (typeof route.announce === 'string') {
|
|
219
|
+
Announcer.speak(route.announce)
|
|
220
|
+
} else {
|
|
221
|
+
Announcer.speak(route.announce.message, route.announce.politeness)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Update router state after announcements and final route resolution,
|
|
226
|
+
// right before initializing or restoring the view
|
|
227
|
+
state.path = route.path
|
|
228
|
+
state.params = Object.keys(route.params).length === 0 ? null : route.params
|
|
229
|
+
state.hash = route.hash
|
|
230
|
+
state.data = null
|
|
231
|
+
state.data = route.data || {}
|
|
232
|
+
|
|
233
|
+
// routing to a new page (instead of routing back to a keepAlive page)
|
|
234
|
+
if (view === undefined) {
|
|
235
|
+
// create a holder element for the new view
|
|
236
|
+
holder = stage.element({ parent: this[symbols.children][0] })
|
|
237
|
+
holder.populate({})
|
|
238
|
+
holder.set('w', '100%')
|
|
239
|
+
holder.set('h', '100%')
|
|
240
|
+
|
|
241
|
+
view = await loadPage.call(this, route, holder, props)
|
|
242
|
+
} else {
|
|
243
|
+
holder = view[symbols.holder]
|
|
244
|
+
|
|
245
|
+
// Check, whether cached view holder's alpha prop is exists in transition or not
|
|
246
|
+
let hasAlphaProp = false
|
|
247
|
+
if (route.transition.before) {
|
|
248
|
+
if (Array.isArray(route.transition.before)) {
|
|
249
|
+
for (let i = 0; i < route.transition.before.length; i++) {
|
|
250
|
+
if (route.transition.before[i].prop === 'alpha') {
|
|
465
251
|
hasAlphaProp = true
|
|
252
|
+
break
|
|
466
253
|
}
|
|
467
254
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
holder.set('alpha', 1)
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// store the new view as new child, only if we're not reusing the previous page component
|
|
475
|
-
if (reuse === false) {
|
|
476
|
-
this[symbols.children].push(view)
|
|
255
|
+
} else if (route.transition.before.prop === 'alpha') {
|
|
256
|
+
hasAlphaProp = true
|
|
477
257
|
}
|
|
258
|
+
}
|
|
259
|
+
// set holder alpha when alpha prop is not exists in route transition
|
|
260
|
+
if (hasAlphaProp === false) {
|
|
261
|
+
holder.set('alpha', 1)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
478
264
|
|
|
479
|
-
|
|
480
|
-
|
|
265
|
+
// store the new view as new child, only if we're not reusing the previous page component
|
|
266
|
+
if (reuse === false) {
|
|
267
|
+
this[symbols.children].push(view)
|
|
268
|
+
}
|
|
481
269
|
|
|
482
|
-
|
|
483
|
-
|
|
270
|
+
// keep reference to the previous focus for storing in cache
|
|
271
|
+
previousFocus = Focus.get()
|
|
484
272
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
focus ? focus.$focus() : /** @type {BlitsComponent} */ (view).$focus()
|
|
488
|
-
}
|
|
273
|
+
const children = this[symbols.children]
|
|
274
|
+
this.activeView = children[children.length - 1]
|
|
489
275
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
holder.set(route.transition.before[i].prop, route.transition.before[i].value)
|
|
495
|
-
}
|
|
496
|
-
} else {
|
|
497
|
-
holder.set(route.transition.before.prop, route.transition.before.value)
|
|
498
|
-
}
|
|
499
|
-
}
|
|
276
|
+
// set focus to the view that we're routing to (unless explicitly disabling passing focus)
|
|
277
|
+
if (route.options.passFocus !== false) {
|
|
278
|
+
focus ? focus.$focus() : /** @type {BlitsComponent} */ (view).$focus()
|
|
279
|
+
}
|
|
500
280
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
// FIX: truthy guard — previousRoute can be `false` (see history-push comment above).
|
|
506
|
-
if (previousRoute && reuse === false) {
|
|
507
|
-
// only animate when there is a previous route
|
|
508
|
-
shouldAnimate = true
|
|
509
|
-
const oldView = this[symbols.children].splice(1, 1).pop()
|
|
510
|
-
if (oldView) {
|
|
511
|
-
await removeView(previousRoute, oldView, route.transition.out, navigatingBack)
|
|
512
|
-
}
|
|
513
|
-
}
|
|
281
|
+
// apply starting state of transition
|
|
282
|
+
if (route.transition.before) {
|
|
283
|
+
await executeTransition(route.transition.before, holder, false)
|
|
284
|
+
}
|
|
514
285
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
286
|
+
let shouldAnimate = false
|
|
287
|
+
|
|
288
|
+
// apply out out transition on previous view if available, unless
|
|
289
|
+
// we're reusing the prvious page component
|
|
290
|
+
// FIX: truthy guard — previousRoute can be `false` (see history-push comment above).
|
|
291
|
+
if (previousRoute && reuse === false) {
|
|
292
|
+
// only animate when there is a previous route
|
|
293
|
+
shouldAnimate = true
|
|
294
|
+
let oldView = this[symbols.children].splice(1, 1).pop()
|
|
295
|
+
if (oldView) {
|
|
296
|
+
executeTransition(previousRoute.transition.out, oldView[symbols.holder], true)
|
|
297
|
+
|
|
298
|
+
// Resolve effective keepAlive: runtime override from $router.to() takes precedence
|
|
299
|
+
// over the static route config option
|
|
300
|
+
const keepAlive =
|
|
301
|
+
overrideOptions.keepAlive !== undefined
|
|
302
|
+
? overrideOptions.keepAlive
|
|
303
|
+
: previousRoute.options && previousRoute.options.keepAlive
|
|
304
|
+
|
|
305
|
+
// cache the page when it's as 'keepAlive' instead of destroying
|
|
306
|
+
if (
|
|
307
|
+
navigatingBack === false &&
|
|
308
|
+
keepAlive === true &&
|
|
309
|
+
previousRoute.options &&
|
|
310
|
+
previousRoute.options.inHistory === true
|
|
311
|
+
) {
|
|
312
|
+
const historyItem = history[history.length - 1]
|
|
313
|
+
if (historyItem !== undefined) {
|
|
314
|
+
historyItem.view = oldView
|
|
315
|
+
historyItem.focus = previousFocus
|
|
525
316
|
}
|
|
526
317
|
}
|
|
527
318
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
)
|
|
537
|
-
} catch (error) {
|
|
538
|
-
Log.error('Error in "AfterEach" Hook', error)
|
|
539
|
-
}
|
|
540
|
-
}
|
|
319
|
+
/* Destroy the view in the following cases:
|
|
320
|
+
* 1. Navigating forward, and the previous route is not configured with "keep alive" set to true.
|
|
321
|
+
* 2. Navigating back, and the previous route is configured with "keep alive" set to true.
|
|
322
|
+
* 3. Navigating back, and the previous route is not configured with "keep alive" set to true.
|
|
323
|
+
*/
|
|
324
|
+
if (previousRoute.options && (keepAlive !== true || navigatingBack === true)) {
|
|
325
|
+
oldView.destroy()
|
|
326
|
+
oldView = null
|
|
541
327
|
}
|
|
542
328
|
|
|
543
|
-
|
|
544
|
-
try {
|
|
545
|
-
await route.hooks.after.call(
|
|
546
|
-
this[symbols.parent],
|
|
547
|
-
route, // to
|
|
548
|
-
previousRoute // from
|
|
549
|
-
)
|
|
550
|
-
} catch (error) {
|
|
551
|
-
Log.error('Error or Rejected Promise in "After" Hook', error)
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
} else {
|
|
555
|
-
Log.error(`Route ${route.hash} not found`)
|
|
556
|
-
const routerHooks = this[symbols.parent][symbols.routerHooks]
|
|
557
|
-
if (routerHooks && typeof routerHooks.error === 'function') {
|
|
558
|
-
routerHooks.error.call(this[symbols.parent], `Route ${route.hash} not found`)
|
|
559
|
-
}
|
|
329
|
+
previousFocus = null
|
|
560
330
|
}
|
|
561
331
|
}
|
|
562
332
|
|
|
333
|
+
// apply in transition
|
|
334
|
+
if (route.transition.in) await executeTransition(route.transition.in, holder, shouldAnimate)
|
|
335
|
+
|
|
336
|
+
// execute after each Hook
|
|
337
|
+
await executeAfterHook(
|
|
338
|
+
this[symbols.parent][symbols.routerHooks],
|
|
339
|
+
'afterEach',
|
|
340
|
+
this[symbols.parent],
|
|
341
|
+
route,
|
|
342
|
+
previousRoute
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
// execute after route Hook
|
|
346
|
+
await executeAfterHook(route.hooks, 'after', this[symbols.parent], route, previousRoute)
|
|
347
|
+
|
|
563
348
|
// Clear module-level variables after removeView has consumed them.
|
|
564
349
|
// Placed here so it executes for all navigation flows, not only when
|
|
565
350
|
// previousRoute exists and reuse is false.
|
|
@@ -572,81 +357,118 @@ export const navigate = async function () {
|
|
|
572
357
|
preventHashChangeNavigation = false
|
|
573
358
|
}
|
|
574
359
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
for (let i = 0; i < transition.length; i++) {
|
|
587
|
-
i === transition.length - 1
|
|
588
|
-
? await setOrAnimate(view[symbols.holder], transition[i])
|
|
589
|
-
: setOrAnimate(view[symbols.holder], transition[i])
|
|
360
|
+
const setOrAnimate = (element, transition, shouldAnimate = true) => {
|
|
361
|
+
if (shouldAnimate === true) {
|
|
362
|
+
return new Promise((resolve) => {
|
|
363
|
+
// resolve the promise in the transition end-callback
|
|
364
|
+
// ("extending" end callback when one is already specified)
|
|
365
|
+
let existingEndCallback = transition.end
|
|
366
|
+
transition.end = (...args) => {
|
|
367
|
+
existingEndCallback && existingEndCallback(args)
|
|
368
|
+
// null the callback to enable memory cleanup
|
|
369
|
+
existingEndCallback = null
|
|
370
|
+
resolve()
|
|
590
371
|
}
|
|
591
|
-
|
|
592
|
-
|
|
372
|
+
if (element !== undefined) element.set(transition.prop, { transition })
|
|
373
|
+
else resolve()
|
|
374
|
+
})
|
|
375
|
+
} else {
|
|
376
|
+
element !== undefined && element.set(transition.prop, transition.value)
|
|
377
|
+
return true
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const executeBeforeHook = async function (
|
|
382
|
+
hooks,
|
|
383
|
+
hookName,
|
|
384
|
+
parent,
|
|
385
|
+
route,
|
|
386
|
+
previousRoute,
|
|
387
|
+
currentPath
|
|
388
|
+
) {
|
|
389
|
+
let result
|
|
390
|
+
if (hooks && hooks[hookName]) {
|
|
391
|
+
try {
|
|
392
|
+
result = await hooks[hookName].call(parent, route, previousRoute)
|
|
393
|
+
if (isString(result)) {
|
|
394
|
+
currentRoute = previousRoute
|
|
395
|
+
to(result)
|
|
396
|
+
return false
|
|
397
|
+
}
|
|
398
|
+
} catch (error) {
|
|
399
|
+
Log.error(`Error or Rejected Promise in "${hookName}" Hook`, error)
|
|
400
|
+
if (history.length > 0) {
|
|
401
|
+
preventHashChangeNavigation = true
|
|
402
|
+
currentRoute = previousRoute
|
|
403
|
+
window.history.back()
|
|
404
|
+
navigatingBack = false
|
|
405
|
+
state.navigating = false
|
|
406
|
+
return false
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// If the resolved result is an object, redirect if the path in the object was changed
|
|
410
|
+
if (isObject(result) === true && result.path !== currentPath) {
|
|
411
|
+
currentRoute = previousRoute
|
|
412
|
+
to(result.path, result.data, result.options)
|
|
413
|
+
return false
|
|
414
|
+
}
|
|
415
|
+
// If the resolved result is false, cancel navigation
|
|
416
|
+
if (result === false && history.length > 0) {
|
|
417
|
+
preventHashChangeNavigation = true
|
|
418
|
+
currentRoute = previousRoute
|
|
419
|
+
window.history.back()
|
|
420
|
+
navigatingBack = false
|
|
421
|
+
state.navigating = false
|
|
422
|
+
return false
|
|
593
423
|
}
|
|
594
424
|
}
|
|
425
|
+
}
|
|
595
426
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
const keepAlive =
|
|
599
|
-
overrideOptions.keepAlive !== undefined
|
|
600
|
-
? overrideOptions.keepAlive
|
|
601
|
-
: route.options && route.options.keepAlive
|
|
427
|
+
const loadPage = async function (route, holder, props) {
|
|
428
|
+
let view = await route.component({ props }, holder, this)
|
|
602
429
|
|
|
603
|
-
//
|
|
604
|
-
if (
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
) {
|
|
610
|
-
const historyItem = history[history.length - 1]
|
|
611
|
-
if (historyItem !== undefined) {
|
|
612
|
-
historyItem.view = view
|
|
613
|
-
historyItem.focus = previousFocus
|
|
430
|
+
// is the component a dynamic module?
|
|
431
|
+
if (view[Symbol.toStringTag] === 'Module') {
|
|
432
|
+
if (view.default && typeof view.default === 'function') {
|
|
433
|
+
view = view.default({ props }, holder, this)
|
|
434
|
+
} else {
|
|
435
|
+
Log.error("Dynamic import doesn't have a default export or default is not a function")
|
|
614
436
|
}
|
|
615
437
|
}
|
|
616
438
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
*/
|
|
622
|
-
if (route.options && (keepAlive !== true || navigatingBack === true)) {
|
|
623
|
-
view.destroy()
|
|
624
|
-
view = null
|
|
439
|
+
if (typeof view === 'function') {
|
|
440
|
+
// had to inline this because the tscompiler does not like LHS reassignments
|
|
441
|
+
// that also change the type of the variable in a variable union
|
|
442
|
+
view = /** @type {BlitsComponentFactory} */ (view)({ props }, holder, this)
|
|
625
443
|
}
|
|
626
444
|
|
|
627
|
-
|
|
628
|
-
route = null
|
|
445
|
+
return view
|
|
629
446
|
}
|
|
630
447
|
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
resolve()
|
|
642
|
-
}
|
|
643
|
-
if (node !== undefined) node.set(transition.prop, { transition })
|
|
644
|
-
else resolve()
|
|
645
|
-
} else {
|
|
646
|
-
node !== undefined && node.set(transition.prop, transition.value)
|
|
647
|
-
resolve()
|
|
448
|
+
const executeAfterHook = async function (hooks, hookName, parent, route, previousRoute) {
|
|
449
|
+
if (hooks && hooks[hookName]) {
|
|
450
|
+
try {
|
|
451
|
+
await hooks[hookName].call(
|
|
452
|
+
parent,
|
|
453
|
+
route, // to
|
|
454
|
+
previousRoute // from
|
|
455
|
+
)
|
|
456
|
+
} catch (error) {
|
|
457
|
+
Log.error(`Error or Rejected Promise in "${hookName}" Hook`, error)
|
|
648
458
|
}
|
|
649
|
-
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const executeTransition = async (transition, element, animate) => {
|
|
463
|
+
if (Array.isArray(transition)) {
|
|
464
|
+
for (let i = 0; i < transition.length; i++) {
|
|
465
|
+
i === transition.length - 1
|
|
466
|
+
? await setOrAnimate(element, transition[i], animate)
|
|
467
|
+
: setOrAnimate(element, transition[i], animate)
|
|
468
|
+
}
|
|
469
|
+
} else {
|
|
470
|
+
await setOrAnimate(element, transition, animate)
|
|
471
|
+
}
|
|
650
472
|
}
|
|
651
473
|
|
|
652
474
|
export const to = (path, data = {}, options = {}) => {
|
|
@@ -690,7 +512,12 @@ export const back = function () {
|
|
|
690
512
|
}
|
|
691
513
|
// Construct new path to backtrack to
|
|
692
514
|
path = path.replace(hashEnd, '')
|
|
693
|
-
const route = matchHash(
|
|
515
|
+
const route = matchHash(
|
|
516
|
+
getHash(path),
|
|
517
|
+
this[symbols.parent][symbols.routes],
|
|
518
|
+
overrideOptions,
|
|
519
|
+
navigationData
|
|
520
|
+
)
|
|
694
521
|
|
|
695
522
|
if (route && backtrack) {
|
|
696
523
|
to(route.path, route.data, route.options)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { default as fadeInFadeOutTransition } from './transitions/fadeInOut.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Get the current hash
|
|
5
|
+
* @returns {Hash}
|
|
6
|
+
*/
|
|
7
|
+
export const getHash = (hash) => {
|
|
8
|
+
if (!hash) hash = '/'
|
|
9
|
+
const hashParts = hash.replace(/^#/, '').split('?')
|
|
10
|
+
return {
|
|
11
|
+
path: hashParts[0],
|
|
12
|
+
queryParams: new URLSearchParams(hashParts[1]),
|
|
13
|
+
hash: hash,
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const normalizePath = (path) => {
|
|
18
|
+
return (
|
|
19
|
+
path
|
|
20
|
+
// remove leading and trailing slashes
|
|
21
|
+
.replace(/^\/+|\/+$/g, '')
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if a value is an object
|
|
28
|
+
* @param {any} v
|
|
29
|
+
* @returns {boolean} True if v is an object
|
|
30
|
+
*/
|
|
31
|
+
export const isObject = (v) => typeof v === 'object' && v !== null
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if a value is a string
|
|
35
|
+
* @param {any} v
|
|
36
|
+
* @returns {boolean} True if v is a string
|
|
37
|
+
*/
|
|
38
|
+
export const isString = (v) => typeof v === 'string'
|
|
39
|
+
|
|
40
|
+
export const queryParamsToObject = (queryParams) => {
|
|
41
|
+
if (!queryParams) return {}
|
|
42
|
+
const object = {}
|
|
43
|
+
const queryParamsEntries = [...queryParams.entries()]
|
|
44
|
+
for (let i = 0; i < queryParamsEntries.length; i++) {
|
|
45
|
+
object[queryParamsEntries[i][0]] = queryParamsEntries[i][1]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return object
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Default Route options
|
|
53
|
+
*
|
|
54
|
+
*/
|
|
55
|
+
const defaultOptions = {
|
|
56
|
+
inHistory: true,
|
|
57
|
+
keepAlive: false,
|
|
58
|
+
passFocus: true,
|
|
59
|
+
reuseComponent: false,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const makeRouteObject = (route, overrides, overrideOptions, navigationData) => {
|
|
63
|
+
// FIX: exclude keepAlive from the destination route options. Unlike other
|
|
64
|
+
// overrides, keepAlive applies to the route being LEFT, not the destination.
|
|
65
|
+
// It is consumed by removeView() instead.
|
|
66
|
+
const { keepAlive: _keepAlive, ...destOverrides } = overrideOptions // eslint-disable-line no-unused-vars
|
|
67
|
+
|
|
68
|
+
const cleanRoute = {
|
|
69
|
+
hash: overrides.hash,
|
|
70
|
+
path: route.path,
|
|
71
|
+
component: route.component,
|
|
72
|
+
transition: 'transition' in route ? route.transition : fadeInFadeOutTransition,
|
|
73
|
+
options: { ...defaultOptions, ...route.options, ...destOverrides },
|
|
74
|
+
announce: route.announce || false,
|
|
75
|
+
hooks: route.hooks || {},
|
|
76
|
+
data: { ...route.data, ...navigationData, ...overrides.queryParams },
|
|
77
|
+
params: overrides.params || {},
|
|
78
|
+
meta: route.meta || {},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return cleanRoute
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Match a path to a route
|
|
86
|
+
*
|
|
87
|
+
* @param {object} hashObject
|
|
88
|
+
* @param {Route[]} routes
|
|
89
|
+
* @returns {Route}
|
|
90
|
+
*/
|
|
91
|
+
export const matchHash = (
|
|
92
|
+
{ hash, path, queryParams },
|
|
93
|
+
routes = [],
|
|
94
|
+
overrideOptions = {},
|
|
95
|
+
navigationData = {}
|
|
96
|
+
) => {
|
|
97
|
+
// remove trailing slashes
|
|
98
|
+
const originalPath = path.replace(/^\/+|\/+$/g, '')
|
|
99
|
+
const originalNormalizedPath = normalizePath(path)
|
|
100
|
+
|
|
101
|
+
const override = {
|
|
102
|
+
hash: hash,
|
|
103
|
+
queryParams: queryParamsToObject(queryParams),
|
|
104
|
+
path: path,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @type {boolean|Route} */
|
|
108
|
+
let matchingRoute = false
|
|
109
|
+
let i = 0
|
|
110
|
+
while (!matchingRoute && i < routes.length) {
|
|
111
|
+
const route = routes[i]
|
|
112
|
+
|
|
113
|
+
const normalizedPath = normalizePath(route.path)
|
|
114
|
+
if (normalizePath(normalizedPath) === originalNormalizedPath) {
|
|
115
|
+
matchingRoute = makeRouteObject(route, override, overrideOptions, navigationData)
|
|
116
|
+
} else if (normalizedPath.indexOf(':') > -1) {
|
|
117
|
+
// match dynamic route parts
|
|
118
|
+
const dynamicRouteParts = [...normalizedPath.matchAll(/:([^\s/]+)/gi)]
|
|
119
|
+
|
|
120
|
+
// construct a regex for the route with dynamic parts
|
|
121
|
+
let dynamicRoutePartsRegex = normalizedPath
|
|
122
|
+
dynamicRouteParts.reverse().forEach((part) => {
|
|
123
|
+
dynamicRoutePartsRegex =
|
|
124
|
+
dynamicRoutePartsRegex.substring(0, part.index) +
|
|
125
|
+
'([^\\s/]+)' +
|
|
126
|
+
dynamicRoutePartsRegex.substring(part.index + part[0].length)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
dynamicRoutePartsRegex = '^' + dynamicRoutePartsRegex
|
|
130
|
+
|
|
131
|
+
// test if the constructed regex matches the path
|
|
132
|
+
const match = originalPath.match(new RegExp(`${dynamicRoutePartsRegex}`, 'i'))
|
|
133
|
+
|
|
134
|
+
if (match) {
|
|
135
|
+
// map the route params to a params object
|
|
136
|
+
override.params = dynamicRouteParts.reverse().reduce((acc, part, index) => {
|
|
137
|
+
acc[part[1]] = match[index + 1]
|
|
138
|
+
return acc
|
|
139
|
+
}, {})
|
|
140
|
+
|
|
141
|
+
matchingRoute = makeRouteObject(route, override, overrideOptions, navigationData)
|
|
142
|
+
}
|
|
143
|
+
} else if (normalizedPath.endsWith('*')) {
|
|
144
|
+
const regex = new RegExp(normalizedPath.replace(/\/?\*/, '/?([^\\s]*)'), 'i')
|
|
145
|
+
const match = originalNormalizedPath.match(regex)
|
|
146
|
+
|
|
147
|
+
if (match) {
|
|
148
|
+
override.params = {}
|
|
149
|
+
if (match[1]) override.params.path = match[1]
|
|
150
|
+
matchingRoute = makeRouteObject(route, override, overrideOptions, navigationData)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
i++
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// @ts-ignore - Remove me when we have a better way to handle this
|
|
157
|
+
return matchingRoute
|
|
158
|
+
}
|