@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.
@@ -15,7 +15,7 @@
15
15
  * SPDX-License-Identifier: Apache-2.0
16
16
  */
17
17
 
18
- import { default as fadeInFadeOutTransition } from './transitions/fadeInOut.js'
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
- let beforeHookOutput
313
- if (route.hooks.before) {
314
- try {
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
- if (history.length > 0) {
329
- preventHashChangeNavigation = true
330
- currentRoute = previousRoute
331
- window.history.back()
122
+ // early return when route not found
123
+ if (route === false) {
124
+ state.navigating = false
332
125
 
333
- navigatingBack = false
334
- state.navigating = false
335
- return
336
- }
337
- }
338
- // If the resolved result is an object, redirect if the path in the object was changed
339
- if (isObject(beforeHookOutput) === true && beforeHookOutput.path !== currentPath) {
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
- // add the previous route (technically still the current route at this point)
357
- // into the history stack when inHistory is true and we're not navigating back
358
- //
359
- // FIX: use truthy check instead of `!== undefined` because matchHash()
360
- // can return `false`, which survives `!== undefined` but has no `.options`.
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
- // a transition can be a function returning a dynamic transition object
371
- // based on current and previous route
372
- if (typeof route.transition === 'function') {
373
- route.transition = route.transition(previousRoute, route)
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
- /** @type {import('../engines/L3/element.js').BlitsElement} */
377
- let holder
378
-
379
- /** @type {RouteViewWithOptionalDefault|undefined|null} */
380
- let view
381
- let focus
382
- // when navigating back let's see if we're navigating back to a route that was kept alive
383
- if (navigatingBack === true && navigatingBackTo !== undefined) {
384
- view = navigatingBackTo.view
385
- focus = navigatingBackTo.focus
386
- navigatingBackTo = null
387
- }
388
- // merge props with potential route params, navigation data and route data to be injected into the component instance
389
- const props = {
390
- ...this[symbols.props],
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
- // see if the component of the previous route can be reused for the
396
- // current route
397
- if (
398
- previousRoute &&
399
- route.options.reuseComponent === true &&
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
- }
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
- // Announce route change if a message has been specified for this route
411
- if (route.announce) {
412
- if (typeof route.announce === 'string') {
413
- route.announce = {
414
- message: route.announce,
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
- // Update router state after announcements and final route resolution,
421
- // right before initializing or restoring the view
422
- state.path = route.path
423
- state.params = Object.keys(route.params).length === 0 ? null : route.params
424
- state.hash = route.hash
425
- state.data = null
426
- state.data = route.data || {}
427
-
428
- if (!view) {
429
- // create a holder element for the new view
430
- holder = stage.element({ parent: this[symbols.children][0] })
431
- holder.populate({})
432
- holder.set('w', '100%')
433
- holder.set('h', '100%')
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
- }
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
- if (typeof view === 'function') {
447
- // had to inline this because the tscompiler does not like LHS reassignments
448
- // that also change the type of the variable in a variable union
449
- view = /** @type {BlitsComponentFactory} */ (view)({ props }, holder, this)
450
- }
451
- } else {
452
- holder = view[symbols.holder]
453
-
454
- // Check, whether cached view holder's alpha prop is exists in transition or not
455
- let hasAlphaProp = false
456
- if (route.transition.before) {
457
- if (Array.isArray(route.transition.before)) {
458
- for (let i = 0; i < route.transition.before.length; i++) {
459
- if (route.transition.before[i].prop === 'alpha') {
460
- hasAlphaProp = true
461
- break
462
- }
463
- }
464
- } else if (route.transition.before.prop === 'alpha') {
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
- // set holder alpha when alpha prop is not exists in route transition
469
- if (hasAlphaProp === false) {
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
- // keep reference to the previous focus for storing in cache
480
- previousFocus = Focus.get()
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
- const children = this[symbols.children]
483
- this.activeView = children[children.length - 1]
285
+ // keep reference to the previous focus for storing in cache
286
+ previousFocus = Focus.get()
484
287
 
485
- // set focus to the view that we're routing to (unless explicitly disabling passing focus)
486
- if (route.options.passFocus !== false) {
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
- // apply before settings to holder element
491
- if (route.transition.before) {
492
- if (Array.isArray(route.transition.before)) {
493
- for (let i = 0; i < route.transition.before.length; i++) {
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
- let shouldAnimate = false
502
-
503
- // apply out out transition on previous view if available, unless
504
- // we're reusing the prvious page component
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
- // apply in transition
516
- if (route.transition.in) {
517
- if (Array.isArray(route.transition.in)) {
518
- for (let i = 0; i < route.transition.in.length; i++) {
519
- i === route.transition.in.length - 1
520
- ? await setOrAnimate(holder, route.transition.in[i], shouldAnimate)
521
- : setOrAnimate(holder, route.transition.in[i], shouldAnimate)
522
- }
523
- } else {
524
- await setOrAnimate(holder, route.transition.in, shouldAnimate)
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
- if (this[symbols.parent][symbols.routerHooks]) {
529
- const hooks = this[symbols.parent][symbols.routerHooks]
530
- if (hooks.afterEach) {
531
- try {
532
- await hooks.afterEach.call(
533
- this[symbols.parent],
534
- route, // to
535
- previousRoute // from
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
- if (route.hooks.after) {
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
- * Remove the currently active view
577
- *
578
- * @param {Route} route
579
- * @param {BlitsComponent} view
580
- * @param {Object} transition
581
- */
582
- const removeView = async (route, view, transition, navigatingBack) => {
583
- // apply out transition
584
- if (transition) {
585
- if (Array.isArray(transition)) {
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
- } else {
592
- await setOrAnimate(view[symbols.holder], transition)
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
- // Resolve effective keepAlive: runtime override from $router.to() takes precedence
597
- // over the static route config option
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
- // cache the page when it's as 'keepAlive' instead of destroying
604
- if (
605
- navigatingBack === false &&
606
- route.options &&
607
- keepAlive === true &&
608
- route.options.inHistory === true
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
- /* Destroy the view in the following cases:
618
- * 1. Navigating forward, and the previous route is not configured with "keep alive" set to true.
619
- * 2. Navigating back, and the previous route is configured with "keep alive" set to true.
620
- * 3. Navigating back, and the previous route is not configured with "keep alive" set to true.
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
- previousFocus = null
628
- route = null
465
+ return view
629
466
  }
630
467
 
631
- const setOrAnimate = (node, transition, shouldAnimate = true) => {
632
- return new Promise((resolve) => {
633
- if (shouldAnimate === true) {
634
- // resolve the promise in the transition end-callback
635
- // ("extending" end callback when one is already specified)
636
- let existingEndCallback = transition.end
637
- transition.end = (...args) => {
638
- existingEndCallback && existingEndCallback(args)
639
- // null the callback to enable memory cleanup
640
- existingEndCallback = null
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
- location.hash = path.replace(/^#/, '')
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
- const route = history.pop()
661
- if (route && currentRoute !== route) {
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
- to(route.hash, route.data, route.options)
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(getHash(path), this[symbols.parent][symbols.routes])
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
- to(route.path, route.data, route.options)
574
+ toRouterView(this, route.path, route.data, route.options)
697
575
  return true
698
576
  }
699
577
  }