@lightningjs/blits 2.2.0 → 2.4.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 +14 -0
- package/index.d.ts +13 -2
- package/package.json +1 -1
- package/src/announcer/announcer.js +1 -1
- package/src/announcer/speechSynthesis.js +1 -1
- package/src/component/base/router.js +136 -22
- package/src/components/RouterView.js +27 -7
- package/src/focus/focus.js +1 -0
- package/src/lib/reactivityguard/computedprops.js +235 -153
- package/src/router/router.js +381 -503
- package/src/router/utils.js +245 -0
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, sameRouteObject, setHash } from './utils.js'
|
|
19
19
|
import { reactive } from '../lib/reactivity/reactive.js'
|
|
20
20
|
|
|
21
21
|
import symbols from '../lib/symbols.js'
|
|
@@ -52,7 +52,8 @@ import Settings from '../settings.js'
|
|
|
52
52
|
*/
|
|
53
53
|
|
|
54
54
|
/** @type {Route} */
|
|
55
|
-
export let currentRoute
|
|
55
|
+
export let currentRoute = {}
|
|
56
|
+
|
|
56
57
|
export const state = reactive(
|
|
57
58
|
{
|
|
58
59
|
path: '',
|
|
@@ -76,6 +77,8 @@ export const state = reactive(
|
|
|
76
77
|
*/
|
|
77
78
|
|
|
78
79
|
const history = []
|
|
80
|
+
const routerViews = new Set()
|
|
81
|
+
let singleRouterView = null
|
|
79
82
|
|
|
80
83
|
let overrideOptions = {}
|
|
81
84
|
let navigationData = {}
|
|
@@ -85,157 +88,6 @@ let previousFocus
|
|
|
85
88
|
// Skips internal router navigation when set to true only for the next "navigate"
|
|
86
89
|
// execution, needed for window.history management
|
|
87
90
|
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
91
|
|
|
240
92
|
/**
|
|
241
93
|
* Navigate to a route
|
|
@@ -250,316 +102,266 @@ const makeRouteObject = (route, overrides) => {
|
|
|
250
102
|
* @returns {Promise<void>}
|
|
251
103
|
*/
|
|
252
104
|
export const navigate = async function () {
|
|
105
|
+
// early return when in preventHashChange mode
|
|
106
|
+
if (preventHashChangeNavigation !== false) return
|
|
107
|
+
// early return when no routes
|
|
108
|
+
if (!this[symbols.parent][symbols.routes] || this[symbols.parent][symbols.routes].length === 0)
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
if (this.history === undefined) this.history = []
|
|
112
|
+
|
|
113
|
+
state.navigating = true
|
|
114
|
+
|
|
253
115
|
Announcer.stop()
|
|
254
116
|
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
117
|
|
|
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)
|
|
118
|
+
const hash = getHash(location.hash, this.name)
|
|
119
|
+
// try to find the route
|
|
120
|
+
let route = matchHash(hash, this[symbols.parent][symbols.routes], overrideOptions, navigationData)
|
|
327
121
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
window.history.back()
|
|
122
|
+
// early return when route not found
|
|
123
|
+
if (route === false) {
|
|
124
|
+
state.navigating = false
|
|
332
125
|
|
|
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
|
-
}
|
|
126
|
+
Log.error(`Route ${hash.hash} not found`)
|
|
127
|
+
const routerHooks = this[symbols.parent][symbols.routerHooks]
|
|
128
|
+
if (routerHooks && typeof routerHooks.error === 'function') {
|
|
129
|
+
routerHooks.error.call(this[symbols.parent], `Route ${hash.hash} not found`)
|
|
130
|
+
}
|
|
131
|
+
return
|
|
132
|
+
}
|
|
355
133
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (
|
|
362
|
-
previousRoute &&
|
|
363
|
-
previousRoute.options &&
|
|
364
|
-
previousRoute.options.inHistory === true &&
|
|
365
|
-
navigatingBack === false
|
|
366
|
-
) {
|
|
367
|
-
history.push(previousRoute)
|
|
368
|
-
}
|
|
134
|
+
// sameRouteObject is too simple check for now!
|
|
135
|
+
if (this.currentRoute !== undefined && sameRouteObject(route, this.currentRoute)) {
|
|
136
|
+
state.navigating = false
|
|
137
|
+
return
|
|
138
|
+
}
|
|
369
139
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
140
|
+
let reuse = false
|
|
141
|
+
this.previousRoute = this.currentRoute
|
|
142
|
+
this.currentRoute = route
|
|
143
|
+
const currentPath = this.currentRoute.path
|
|
144
|
+
|
|
145
|
+
// execute before each hook
|
|
146
|
+
const beforeEachResult = await executeBeforeHook.call(
|
|
147
|
+
this,
|
|
148
|
+
this[symbols.parent][symbols.routerHooks],
|
|
149
|
+
'beforeEach',
|
|
150
|
+
this[symbols.parent],
|
|
151
|
+
route,
|
|
152
|
+
this.previousRoute,
|
|
153
|
+
currentPath
|
|
154
|
+
)
|
|
155
|
+
if (beforeEachResult === false) {
|
|
156
|
+
preventHashChangeNavigation = false
|
|
157
|
+
state.navigating = false
|
|
158
|
+
return
|
|
159
|
+
}
|
|
375
160
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
...route.params,
|
|
392
|
-
...route.data,
|
|
393
|
-
}
|
|
161
|
+
// execute before route hook
|
|
162
|
+
const beforeResult = await executeBeforeHook.call(
|
|
163
|
+
this,
|
|
164
|
+
route.hooks,
|
|
165
|
+
'before',
|
|
166
|
+
this[symbols.parent],
|
|
167
|
+
route,
|
|
168
|
+
this.previousRoute,
|
|
169
|
+
currentPath
|
|
170
|
+
)
|
|
171
|
+
if (beforeResult === false) {
|
|
172
|
+
preventHashChangeNavigation = false
|
|
173
|
+
state.navigating = false
|
|
174
|
+
return
|
|
175
|
+
}
|
|
394
176
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
177
|
+
// add the previous route (technically still the current route at this point)
|
|
178
|
+
// into the history stack when inHistory is true and we're not navigating back
|
|
179
|
+
//
|
|
180
|
+
// FIX: use truthy check instead of `!== undefined` because matchHash()
|
|
181
|
+
// can return `false`, which survives `!== undefined` but has no `.options`.
|
|
182
|
+
if (
|
|
183
|
+
this.previousRoute &&
|
|
184
|
+
this.previousRoute.options &&
|
|
185
|
+
this.previousRoute.options.inHistory === true &&
|
|
186
|
+
navigatingBack === false
|
|
187
|
+
) {
|
|
188
|
+
this.history.push(this.previousRoute)
|
|
189
|
+
}
|
|
409
190
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
Announcer.speak(route.announce.message, route.announce.politeness)
|
|
418
|
-
}
|
|
191
|
+
// a transition can be a function returning a dynamic transition object
|
|
192
|
+
// based on current and previous route
|
|
193
|
+
if (typeof route.transition === 'function') {
|
|
194
|
+
route.transition = route.transition(this.previousRoute, route)
|
|
195
|
+
}
|
|
419
196
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
}
|
|
197
|
+
/** @type {import('../engines/L3/element.js').BlitsElement} */
|
|
198
|
+
let holder
|
|
199
|
+
|
|
200
|
+
/** @type {RouteViewWithOptionalDefault|undefined|null} */
|
|
201
|
+
let view
|
|
202
|
+
let focus
|
|
203
|
+
// when navigating back let's see if we're navigating back to a route that was kept alive
|
|
204
|
+
if (navigatingBack === true && navigatingBackTo !== undefined) {
|
|
205
|
+
view = navigatingBackTo.view
|
|
206
|
+
focus = navigatingBackTo.focus
|
|
207
|
+
navigatingBackTo = null
|
|
208
|
+
}
|
|
209
|
+
// merge props with potential route params, navigation data and route data to be injected into the component instance
|
|
210
|
+
const props = {
|
|
211
|
+
...this[symbols.props],
|
|
212
|
+
...route.params,
|
|
213
|
+
...route.data,
|
|
214
|
+
}
|
|
445
215
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
216
|
+
// see if the component of the previous route can be reused for the
|
|
217
|
+
// current route
|
|
218
|
+
if (
|
|
219
|
+
this.previousRoute &&
|
|
220
|
+
route.options.reuseComponent === true &&
|
|
221
|
+
route.options.keepAlive !== true &&
|
|
222
|
+
route.component === this.previousRoute.component
|
|
223
|
+
) {
|
|
224
|
+
reuse = true
|
|
225
|
+
view = this[symbols.children][this[symbols.children].length - 1]
|
|
226
|
+
for (const prop in props) {
|
|
227
|
+
view[symbols.props][prop] = props[prop]
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Announce route change if a message has been specified for this route
|
|
232
|
+
if (route.announce) {
|
|
233
|
+
if (typeof route.announce === 'string') {
|
|
234
|
+
Announcer.speak(route.announce)
|
|
235
|
+
} else {
|
|
236
|
+
Announcer.speak(route.announce.message, route.announce.politeness)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Update router state after announcements and final route resolution,
|
|
241
|
+
// right before initializing or restoring the view
|
|
242
|
+
state.path = route.path
|
|
243
|
+
state.params = Object.keys(route.params).length === 0 ? null : route.params
|
|
244
|
+
state.hash = route.hash
|
|
245
|
+
state.data = null
|
|
246
|
+
state.data = route.data || {}
|
|
247
|
+
|
|
248
|
+
// routing to a new page (instead of routing back to a keepAlive page)
|
|
249
|
+
if (view === undefined) {
|
|
250
|
+
// create a holder element for the new view
|
|
251
|
+
holder = stage.element({ parent: this[symbols.children][0] })
|
|
252
|
+
holder.populate({})
|
|
253
|
+
holder.set('w', '100%')
|
|
254
|
+
holder.set('h', '100%')
|
|
255
|
+
|
|
256
|
+
view = await loadPage.call(this, route, holder, props)
|
|
257
|
+
} else {
|
|
258
|
+
holder = view[symbols.holder]
|
|
259
|
+
|
|
260
|
+
// Check, whether cached view holder's alpha prop is exists in transition or not
|
|
261
|
+
let hasAlphaProp = false
|
|
262
|
+
if (route.transition.before) {
|
|
263
|
+
if (Array.isArray(route.transition.before)) {
|
|
264
|
+
for (let i = 0; i < route.transition.before.length; i++) {
|
|
265
|
+
if (route.transition.before[i].prop === 'alpha') {
|
|
465
266
|
hasAlphaProp = true
|
|
267
|
+
break
|
|
466
268
|
}
|
|
467
269
|
}
|
|
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)
|
|
270
|
+
} else if (route.transition.before.prop === 'alpha') {
|
|
271
|
+
hasAlphaProp = true
|
|
477
272
|
}
|
|
273
|
+
}
|
|
274
|
+
// set holder alpha when alpha prop is not exists in route transition
|
|
275
|
+
if (hasAlphaProp === false) {
|
|
276
|
+
holder.set('alpha', 1)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
478
279
|
|
|
479
|
-
|
|
480
|
-
|
|
280
|
+
// store the new view as new child, only if we're not reusing the previous page component
|
|
281
|
+
if (reuse === false) {
|
|
282
|
+
this[symbols.children].push(view)
|
|
283
|
+
}
|
|
481
284
|
|
|
482
|
-
|
|
483
|
-
|
|
285
|
+
// keep reference to the previous focus for storing in cache
|
|
286
|
+
previousFocus = Focus.get()
|
|
484
287
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
focus ? focus.$focus() : /** @type {BlitsComponent} */ (view).$focus()
|
|
488
|
-
}
|
|
288
|
+
const children = this[symbols.children]
|
|
289
|
+
this.activeView = children[children.length - 1]
|
|
489
290
|
|
|
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
|
-
}
|
|
291
|
+
// set focus to the view that we're routing to (unless explicitly disabling passing focus)
|
|
292
|
+
if (route.options.passFocus !== false) {
|
|
293
|
+
focus ? focus.$focus() : /** @type {BlitsComponent} */ (view).$focus()
|
|
294
|
+
}
|
|
500
295
|
|
|
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
|
-
}
|
|
296
|
+
// apply starting state of transition
|
|
297
|
+
if (route.transition.before) {
|
|
298
|
+
await executeTransition(route.transition.before, holder, false)
|
|
299
|
+
}
|
|
514
300
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
301
|
+
let shouldAnimate = false
|
|
302
|
+
|
|
303
|
+
// apply out out transition on previous view if available, unless
|
|
304
|
+
// we're reusing the prvious page component
|
|
305
|
+
// FIX: truthy guard — previousRoute can be `false` (see history-push comment above).
|
|
306
|
+
if (this.previousRoute && reuse === false) {
|
|
307
|
+
// only animate when there is a previous route
|
|
308
|
+
shouldAnimate = true
|
|
309
|
+
let oldView = this[symbols.children].splice(1, 1).pop()
|
|
310
|
+
if (oldView) {
|
|
311
|
+
executeTransition(this.previousRoute.transition.out, oldView[symbols.holder], true)
|
|
312
|
+
|
|
313
|
+
// Resolve effective keepAlive: runtime override from $router.to() takes precedence
|
|
314
|
+
// over the static route config option
|
|
315
|
+
const hasOverrideOptions = overrideOptions && typeof overrideOptions === 'object'
|
|
316
|
+
const keepAlive =
|
|
317
|
+
hasOverrideOptions && overrideOptions.keepAlive !== undefined
|
|
318
|
+
? overrideOptions.keepAlive
|
|
319
|
+
: this.previousRoute.options && this.previousRoute.options.keepAlive
|
|
320
|
+
|
|
321
|
+
// cache the page when it's as 'keepAlive' instead of destroying
|
|
322
|
+
if (
|
|
323
|
+
navigatingBack === false &&
|
|
324
|
+
keepAlive === true &&
|
|
325
|
+
this.previousRoute &&
|
|
326
|
+
this.previousRoute.options &&
|
|
327
|
+
this.previousRoute.options.inHistory === true
|
|
328
|
+
) {
|
|
329
|
+
const historyItem = this.history[this.history.length - 1]
|
|
330
|
+
if (historyItem !== undefined) {
|
|
331
|
+
historyItem.view = oldView
|
|
332
|
+
historyItem.focus = previousFocus
|
|
525
333
|
}
|
|
526
334
|
}
|
|
527
335
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
)
|
|
537
|
-
} catch (error) {
|
|
538
|
-
Log.error('Error in "AfterEach" Hook', error)
|
|
539
|
-
}
|
|
540
|
-
}
|
|
336
|
+
/* Destroy the view in the following cases:
|
|
337
|
+
* 1. Navigating forward, and the previous route is not configured with "keep alive" set to true.
|
|
338
|
+
* 2. Navigating back, and the previous route is configured with "keep alive" set to true.
|
|
339
|
+
* 3. Navigating back, and the previous route is not configured with "keep alive" set to true.
|
|
340
|
+
*/
|
|
341
|
+
if (this.previousRoute.options && (keepAlive !== true || navigatingBack === true)) {
|
|
342
|
+
oldView.destroy()
|
|
343
|
+
oldView = null
|
|
541
344
|
}
|
|
542
345
|
|
|
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
|
-
}
|
|
346
|
+
previousFocus = null
|
|
560
347
|
}
|
|
561
348
|
}
|
|
562
349
|
|
|
350
|
+
// apply in transition
|
|
351
|
+
if (route.transition.in) await executeTransition(route.transition.in, holder, shouldAnimate)
|
|
352
|
+
|
|
353
|
+
// execute after each Hook
|
|
354
|
+
await executeAfterHook(
|
|
355
|
+
this[symbols.parent][symbols.routerHooks],
|
|
356
|
+
'afterEach',
|
|
357
|
+
this[symbols.parent],
|
|
358
|
+
route,
|
|
359
|
+
this.previousRoute
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
// execute after route Hook
|
|
363
|
+
await executeAfterHook(route.hooks, 'after', this[symbols.parent], route, this.previousRoute)
|
|
364
|
+
|
|
563
365
|
// Clear module-level variables after removeView has consumed them.
|
|
564
366
|
// Placed here so it executes for all navigation flows, not only when
|
|
565
367
|
// previousRoute exists and reuse is false.
|
|
@@ -572,101 +374,172 @@ export const navigate = async function () {
|
|
|
572
374
|
preventHashChangeNavigation = false
|
|
573
375
|
}
|
|
574
376
|
|
|
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])
|
|
377
|
+
const setOrAnimate = (element, transition, shouldAnimate = true) => {
|
|
378
|
+
if (shouldAnimate === true) {
|
|
379
|
+
return new Promise((resolve) => {
|
|
380
|
+
// resolve the promise in the transition end-callback
|
|
381
|
+
// ("extending" end callback when one is already specified)
|
|
382
|
+
let existingEndCallback = transition.end
|
|
383
|
+
transition.end = (...args) => {
|
|
384
|
+
existingEndCallback && existingEndCallback(args)
|
|
385
|
+
// null the callback to enable memory cleanup
|
|
386
|
+
existingEndCallback = null
|
|
387
|
+
resolve()
|
|
590
388
|
}
|
|
591
|
-
|
|
592
|
-
|
|
389
|
+
if (element !== undefined) element.set(transition.prop, { transition })
|
|
390
|
+
else resolve()
|
|
391
|
+
})
|
|
392
|
+
} else {
|
|
393
|
+
element !== undefined && element.set(transition.prop, transition.value)
|
|
394
|
+
return true
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const executeBeforeHook = async function (
|
|
399
|
+
hooks,
|
|
400
|
+
hookName,
|
|
401
|
+
parent,
|
|
402
|
+
route,
|
|
403
|
+
previousRoute,
|
|
404
|
+
currentPath
|
|
405
|
+
) {
|
|
406
|
+
let result
|
|
407
|
+
if (hooks && hooks[hookName]) {
|
|
408
|
+
try {
|
|
409
|
+
result = await hooks[hookName].call(parent, route, previousRoute)
|
|
410
|
+
if (isString(result)) {
|
|
411
|
+
currentRoute = previousRoute
|
|
412
|
+
to(result)
|
|
413
|
+
return false
|
|
414
|
+
}
|
|
415
|
+
} catch (error) {
|
|
416
|
+
Log.error(`Error or Rejected Promise in "${hookName}" Hook`, error)
|
|
417
|
+
if (this.history.length > 0) {
|
|
418
|
+
preventHashChangeNavigation = true
|
|
419
|
+
currentRoute = previousRoute
|
|
420
|
+
/// hmmmm
|
|
421
|
+
window.history.back()
|
|
422
|
+
navigatingBack = false
|
|
423
|
+
state.navigating = false
|
|
424
|
+
return false
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// If the resolved result is an object, redirect if the path in the object was changed
|
|
428
|
+
if (isObject(result) === true && result.path !== currentPath) {
|
|
429
|
+
currentRoute = previousRoute
|
|
430
|
+
to(result.path, result.data, result.options)
|
|
431
|
+
return false
|
|
432
|
+
}
|
|
433
|
+
// If the resolved result is false, cancel navigation
|
|
434
|
+
if (result === false) {
|
|
435
|
+
if (this.history.length > 0) {
|
|
436
|
+
preventHashChangeNavigation = true
|
|
437
|
+
currentRoute = previousRoute
|
|
438
|
+
window.history.back()
|
|
439
|
+
navigatingBack = false
|
|
440
|
+
state.navigating = false
|
|
441
|
+
}
|
|
442
|
+
return false
|
|
593
443
|
}
|
|
594
444
|
}
|
|
445
|
+
}
|
|
595
446
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
const keepAlive =
|
|
599
|
-
overrideOptions.keepAlive !== undefined
|
|
600
|
-
? overrideOptions.keepAlive
|
|
601
|
-
: route.options && route.options.keepAlive
|
|
447
|
+
const loadPage = async function (route, holder, props) {
|
|
448
|
+
let view = await route.component({ props }, holder, this)
|
|
602
449
|
|
|
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
|
|
450
|
+
// is the component a dynamic module?
|
|
451
|
+
if (view[Symbol.toStringTag] === 'Module') {
|
|
452
|
+
if (view.default && typeof view.default === 'function') {
|
|
453
|
+
view = view.default({ props }, holder, this)
|
|
454
|
+
} else {
|
|
455
|
+
Log.error("Dynamic import doesn't have a default export or default is not a function")
|
|
614
456
|
}
|
|
615
457
|
}
|
|
616
458
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
*/
|
|
622
|
-
if (route.options && (keepAlive !== true || navigatingBack === true)) {
|
|
623
|
-
view.destroy()
|
|
624
|
-
view = null
|
|
459
|
+
if (typeof view === 'function') {
|
|
460
|
+
// had to inline this because the tscompiler does not like LHS reassignments
|
|
461
|
+
// that also change the type of the variable in a variable union
|
|
462
|
+
view = /** @type {BlitsComponentFactory} */ (view)({ props }, holder, this)
|
|
625
463
|
}
|
|
626
464
|
|
|
627
|
-
|
|
628
|
-
route = null
|
|
465
|
+
return view
|
|
629
466
|
}
|
|
630
467
|
|
|
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()
|
|
468
|
+
const executeAfterHook = async function (hooks, hookName, parent, route, previousRoute) {
|
|
469
|
+
if (hooks && hooks[hookName]) {
|
|
470
|
+
try {
|
|
471
|
+
await hooks[hookName].call(
|
|
472
|
+
parent,
|
|
473
|
+
route, // to
|
|
474
|
+
previousRoute // from
|
|
475
|
+
)
|
|
476
|
+
} catch (error) {
|
|
477
|
+
Log.error(`Error or Rejected Promise in "${hookName}" Hook`, error)
|
|
648
478
|
}
|
|
649
|
-
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const executeTransition = async (transition, element, animate) => {
|
|
483
|
+
if (Array.isArray(transition)) {
|
|
484
|
+
for (let i = 0; i < transition.length; i++) {
|
|
485
|
+
i === transition.length - 1
|
|
486
|
+
? await setOrAnimate(element, transition[i], animate)
|
|
487
|
+
: setOrAnimate(element, transition[i], animate)
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
await setOrAnimate(element, transition, animate)
|
|
491
|
+
}
|
|
650
492
|
}
|
|
651
493
|
|
|
652
494
|
export const to = (path, data = {}, options = {}) => {
|
|
653
495
|
navigationData = data
|
|
654
496
|
overrideOptions = options
|
|
655
497
|
|
|
656
|
-
|
|
498
|
+
setHash(path)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export const toRouterView = (routerView, path, data = {}, options = {}) => {
|
|
502
|
+
navigationData = data
|
|
503
|
+
overrideOptions = options
|
|
504
|
+
|
|
505
|
+
setHash(path, routerView.name)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export const registerRouterView = (routerView) => {
|
|
509
|
+
routerViews.add(routerView)
|
|
510
|
+
singleRouterView = routerViews.size === 1 ? routerView : null
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export const unregisterRouterView = (routerView) => {
|
|
514
|
+
routerViews.delete(routerView)
|
|
515
|
+
singleRouterView = routerViews.size === 1 ? routerViews.values().next().value : null
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export const getRegisteredRouterView = (name) => {
|
|
519
|
+
for (const routerView of routerViews) {
|
|
520
|
+
if (routerView.name === name) return routerView
|
|
521
|
+
}
|
|
522
|
+
return null
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export const getRegisteredRouterViewsCount = () => routerViews.size
|
|
526
|
+
|
|
527
|
+
export const getSingleRegisteredRouterView = () => {
|
|
528
|
+
return singleRouterView
|
|
657
529
|
}
|
|
658
530
|
|
|
659
531
|
export const back = function () {
|
|
660
|
-
|
|
661
|
-
|
|
532
|
+
if (this.history === undefined) return
|
|
533
|
+
const route = this.history.pop()
|
|
534
|
+
if (route && this.currentRoute !== route) {
|
|
662
535
|
// set indicator that we are navigating back (to prevent adding page to history stack)
|
|
663
536
|
navigatingBack = true
|
|
664
537
|
navigatingBackTo = route
|
|
665
|
-
|
|
538
|
+
toRouterView(this, route.hash, route.data, route.options)
|
|
666
539
|
return true
|
|
667
540
|
}
|
|
668
541
|
|
|
669
|
-
const backtrack = (currentRoute && currentRoute.options.backtrack) || false
|
|
542
|
+
const backtrack = (this.currentRoute && this.currentRoute.options.backtrack) || false
|
|
670
543
|
|
|
671
544
|
// If we deeplink to a page without backtrack
|
|
672
545
|
// we we let the RouterView handle back
|
|
@@ -675,7 +548,7 @@ export const back = function () {
|
|
|
675
548
|
}
|
|
676
549
|
|
|
677
550
|
const hashEnd = /(\/:?[\w%\s-]+)$/
|
|
678
|
-
let path = currentRoute.path
|
|
551
|
+
let path = this.currentRoute.path
|
|
679
552
|
|
|
680
553
|
let level = path.split('/').length
|
|
681
554
|
|
|
@@ -690,10 +563,15 @@ export const back = function () {
|
|
|
690
563
|
}
|
|
691
564
|
// Construct new path to backtrack to
|
|
692
565
|
path = path.replace(hashEnd, '')
|
|
693
|
-
const route = matchHash(
|
|
566
|
+
const route = matchHash(
|
|
567
|
+
getHash(path),
|
|
568
|
+
this[symbols.parent][symbols.routes],
|
|
569
|
+
overrideOptions,
|
|
570
|
+
navigationData
|
|
571
|
+
)
|
|
694
572
|
|
|
695
573
|
if (route && backtrack) {
|
|
696
|
-
|
|
574
|
+
toRouterView(this, route.path, route.data, route.options)
|
|
697
575
|
return true
|
|
698
576
|
}
|
|
699
577
|
}
|