@lightningjs/blits 1.30.0 → 1.30.2
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/.editorconfig +7 -7
- package/.eslintignore +1 -1
- package/.eslintrc.cjs +65 -65
- package/.github/workflows/comment-lint-results.yml +124 -124
- package/.github/workflows/comment-test-results.yml +108 -108
- package/.github/workflows/deploy-docs.yml +27 -27
- package/.github/workflows/run-blits-tests.yml +141 -141
- package/.github/workflows/run-lint.yml +127 -127
- package/.husky/pre-commit +1 -1
- package/.vscode/extensions.json +7 -7
- package/.vscode/settings.json +9 -9
- package/CHANGELOG.md +1019 -1005
- package/COPYING +1 -0
- package/LICENSE +202 -202
- package/NOTICE +12 -12
- package/README.md +33 -33
- package/bin/index.js +8 -8
- package/blits.d.ts +6 -6
- package/docs/README.md +19 -19
- package/docs/_sidebar.md +37 -37
- package/docs/built-in/directives.md +50 -50
- package/docs/built-in/for-loop.md +203 -203
- package/docs/built-in/layout.md +168 -168
- package/docs/components/component_state.md +90 -90
- package/docs/components/computed_properties.md +45 -45
- package/docs/components/lifecycle_events.md +42 -42
- package/docs/components/methods.md +45 -45
- package/docs/components/props.md +56 -56
- package/docs/components/user_input.md +186 -186
- package/docs/components/utility_methods.md +320 -320
- package/docs/components/watchers.md +34 -34
- package/docs/essentials/application_root.md +19 -19
- package/docs/essentials/components.md +68 -68
- package/docs/essentials/displaying_images.md +67 -67
- package/docs/essentials/displaying_text.md +135 -135
- package/docs/essentials/element_attributes.md +195 -195
- package/docs/essentials/settings.md +117 -117
- package/docs/essentials/template_syntax.md +83 -83
- package/docs/getting_started/file_structure.md +58 -58
- package/docs/getting_started/getting_started.md +45 -45
- package/docs/getting_started/intro.md +20 -20
- package/docs/index.html +47 -47
- package/docs/performance/lazy-loading.md +161 -161
- package/docs/plugins/global_app_state.md +98 -98
- package/docs/plugins/language.md +172 -172
- package/docs/plugins/text-to-speech-announcer.md +119 -119
- package/docs/plugins/theme.md +176 -176
- package/docs/router/basics.md +175 -175
- package/docs/router/hooks.md +70 -70
- package/docs/router/transitions.md +146 -146
- package/docs/sidebar.json +171 -171
- package/docs/transitions_animations/transitions.md +150 -150
- package/index.d.ts +1099 -1099
- package/index.js +28 -28
- package/jsconfig.json +12 -12
- package/package.json +56 -56
- package/public/assets/fps_sprite.base64.js +1 -1
- package/scripts/postpublish.js +59 -59
- package/scripts/prepublishOnly.js +84 -84
- package/scripts/prepush.js +78 -78
- package/src/announcer/announcer.js +162 -162
- package/src/announcer/speechSynthesis.js +107 -107
- package/src/application.js +100 -100
- package/src/component/base/announcer.js +27 -27
- package/src/component/base/events.js +69 -69
- package/src/component/base/index.js +33 -33
- package/src/component/base/methods.js +244 -244
- package/src/component/base/router.js +41 -41
- package/src/component/base/timeouts_intervals.js +99 -99
- package/src/component/base/utils.js +72 -72
- package/src/component/setup/computed.js +48 -48
- package/src/component/setup/index.js +62 -62
- package/src/component/setup/input.js +30 -30
- package/src/component/setup/methods.js +37 -37
- package/src/component/setup/props.js +62 -62
- package/src/component/setup/props.test.js +238 -238
- package/src/component/setup/routes.js +42 -42
- package/src/component/setup/state.js +59 -59
- package/src/component/setup/watch.js +35 -35
- package/src/component.js +437 -437
- package/src/component.test.js +388 -388
- package/src/components/Circle.js +73 -73
- package/src/components/Circle.orig.js +31 -31
- package/src/components/FPScounter.js +1510 -1510
- package/src/components/FPScounter.orig.js +121 -121
- package/src/components/RouterView.js +81 -81
- package/src/components/RouterView.orig.js +54 -54
- package/src/components/Sprite.js +94 -94
- package/src/components/Sprite.orig.js +58 -58
- package/src/components/index.js +28 -28
- package/src/components/index.orig.js +28 -28
- package/src/constants.js +21 -21
- package/src/engine.js +1 -1
- package/src/engines/L3/element.js +744 -744
- package/src/engines/L3/element.test.js +579 -579
- package/src/engines/L3/fontLoader.js +38 -38
- package/src/engines/L3/index.js +7 -7
- package/src/engines/L3/launch.js +123 -123
- package/src/engines/L3/shaderLoader.js +12 -12
- package/src/engines/index.js +1 -1
- package/src/focus.js +90 -90
- package/src/helpers/deepEqualArray.js +50 -50
- package/src/helpers/deepEqualArray.test.js +127 -127
- package/src/helpers/fetchJson.js +34 -34
- package/src/helpers/sequence.js +26 -26
- package/src/launch.js +123 -123
- package/src/lib/blitsfileconverter/blitsfileconverter.js +52 -52
- package/src/lib/codegenerator/generator.js +788 -788
- package/src/lib/codegenerator/generator.test.js +3801 -3801
- package/src/lib/colors/colors.js +92 -92
- package/src/lib/colors/colors.test.js +290 -290
- package/src/lib/colors/htmlColors.js +168 -168
- package/src/lib/componentId.js +49 -49
- package/src/lib/componentId.test.js +84 -84
- package/src/lib/eventListeners.js +123 -123
- package/src/lib/eventListeners.test.js +236 -236
- package/src/lib/hooks.js +59 -59
- package/src/lib/lifecycle.js +85 -85
- package/src/lib/log.js +117 -117
- package/src/lib/precompiler/precompiler.js +73 -73
- package/src/lib/reactivity/effect.js +93 -93
- package/src/lib/reactivity/effect.test.js +35 -35
- package/src/lib/reactivity/reactive.js +208 -208
- package/src/lib/reactivity/reactive.test.js +27 -27
- package/src/lib/reactivityguard/computedprops.js +218 -218
- package/src/lib/symbols.js +108 -108
- package/src/lib/templateparser/parser.js +386 -386
- package/src/lib/templateparser/parser.test.js +1411 -1411
- package/src/lib/utils.js +27 -27
- package/src/plugin.js +27 -27
- package/src/plugins/appstate.js +26 -26
- package/src/plugins/index.js +20 -20
- package/src/plugins/language.js +152 -152
- package/src/plugins/log.js +25 -25
- package/src/plugins/theme.js +94 -94
- package/src/router/router.js +514 -514
- package/src/router/router.test.js +473 -473
- package/src/router/transitions/fadeIn.js +27 -27
- package/src/router/transitions/fadeInOut.js +33 -33
- package/src/router/transitions/index.js +21 -21
- package/src/router/transitions/slideIn.js +28 -28
- package/src/router/transitions/slideInOut.js +33 -33
- package/src/settings.js +40 -40
- package/vite/blitsFileConverter.js +67 -67
- package/vite/index.js +28 -28
- package/vite/msdfGenerator.js +269 -269
- package/vite/preCompiler.js +42 -42
- package/vite/reactivityGuard.js +45 -45
- package/vscode/data/template-attributes.json +589 -589
package/src/router/router.js
CHANGED
|
@@ -1,514 +1,514 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright 2023 Comcast Cable Communications Management, LLC
|
|
3
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
* you may not use this file except in compliance with the License.
|
|
5
|
-
* You may obtain a copy of the License at
|
|
6
|
-
*
|
|
7
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
*
|
|
9
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
* See the License for the specific language governing permissions and
|
|
13
|
-
* limitations under the License.
|
|
14
|
-
*
|
|
15
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { default as fadeInFadeOutTransition } from './transitions/fadeInOut.js'
|
|
19
|
-
import { reactive } from '../lib/reactivity/reactive.js'
|
|
20
|
-
|
|
21
|
-
import symbols from '../lib/symbols.js'
|
|
22
|
-
import { Log } from '../lib/log.js'
|
|
23
|
-
import { stage } from '../launch.js'
|
|
24
|
-
import Focus from '../focus.js'
|
|
25
|
-
import Announcer from '../announcer/announcer.js'
|
|
26
|
-
import Settings from '../settings.js'
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @typedef {import('../component.js').BlitsComponentFactory} BlitsComponentFactory - The component of the route
|
|
30
|
-
* @typedef {import('../component.js').BlitsComponent} BlitsComponent - The element of the route
|
|
31
|
-
* @typedef {import('../engines/L3/element.js').BlitsElement} BlitsElement - The element of the route
|
|
32
|
-
*
|
|
33
|
-
* @typedef {Object} Route
|
|
34
|
-
* @property {string} path - The path of the route
|
|
35
|
-
* @property {string} hash - The hash of the route
|
|
36
|
-
* @property {Object} params - The params of the route
|
|
37
|
-
* @property {Object} data - The data of the route
|
|
38
|
-
* @property {Object} options - The options of the route
|
|
39
|
-
* @property {Object} hooks - The hooks of the route
|
|
40
|
-
* @property {Object} transition - The transition of the route
|
|
41
|
-
* @property {Object} announce - The announce of the route
|
|
42
|
-
* @property {(options: Object, parentEl: BlitsElement, parentComponent: BlitsComponent, rootComponent?: BlitsComponent) => BlitsComponent} component - The component factory of the route
|
|
43
|
-
*
|
|
44
|
-
* @typedef {Object} Hash
|
|
45
|
-
* @property {string} path - The path of the hash
|
|
46
|
-
* @property {URLSearchParams} queryParams - The query params of the hash
|
|
47
|
-
* @property {string} hash - The hash
|
|
48
|
-
*
|
|
49
|
-
*/
|
|
50
|
-
|
|
51
|
-
/** @type {Route} */
|
|
52
|
-
export let currentRoute
|
|
53
|
-
export const state = reactive(
|
|
54
|
-
{
|
|
55
|
-
path: '',
|
|
56
|
-
navigating: false,
|
|
57
|
-
data: null,
|
|
58
|
-
params: null,
|
|
59
|
-
hash: '',
|
|
60
|
-
},
|
|
61
|
-
Settings.get('reactivityMode'),
|
|
62
|
-
true
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
// Changed from WeakMap to Map to allow for caching of views by the url hash.
|
|
66
|
-
// We are manually doing the cleanup of the cache when the route is not marked as keepAlive.
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* @typedef {Object} CacheMapEntry
|
|
70
|
-
* @property {BlitsComponent|BlitsComponentFactory} view - The view of the route
|
|
71
|
-
* @property {BlitsComponent} focus - The focus of the route
|
|
72
|
-
*/
|
|
73
|
-
|
|
74
|
-
/** @type {Map<String, CacheMapEntry>} */
|
|
75
|
-
const cacheMap = new Map()
|
|
76
|
-
const history = []
|
|
77
|
-
|
|
78
|
-
let overrideOptions = {}
|
|
79
|
-
let navigationData = {}
|
|
80
|
-
let navigatingBack = false
|
|
81
|
-
let previousFocus
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Get the current hash
|
|
85
|
-
* @returns {Hash}
|
|
86
|
-
*/
|
|
87
|
-
export const getHash = () => {
|
|
88
|
-
const hashParts = (document.location.hash || '/').replace(/^#/, '').split('?')
|
|
89
|
-
return {
|
|
90
|
-
path: hashParts[0],
|
|
91
|
-
queryParams: new URLSearchParams(hashParts[1]),
|
|
92
|
-
hash: document.location.hash,
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const normalizePath = (path) => {
|
|
97
|
-
return (
|
|
98
|
-
path
|
|
99
|
-
// remove leading and trailing slashes
|
|
100
|
-
.replace(/^\/+|\/+$/g, '')
|
|
101
|
-
.toLowerCase()
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Check if a value is an object
|
|
107
|
-
* @param {any} v
|
|
108
|
-
* @returns {boolean} True if v is an object
|
|
109
|
-
*/
|
|
110
|
-
const isObject = (v) => typeof v === 'object' && v !== null
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Check if a value is a function
|
|
114
|
-
* @param {any} v
|
|
115
|
-
* @returns {boolean} True if v is a string
|
|
116
|
-
*/
|
|
117
|
-
const isString = (v) => typeof v === 'string'
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Match a path to a route
|
|
121
|
-
*
|
|
122
|
-
* @param {string} path
|
|
123
|
-
* @param {Route[]} routes
|
|
124
|
-
* @returns {Route}
|
|
125
|
-
*/
|
|
126
|
-
export const matchHash = (path, routes = []) => {
|
|
127
|
-
// remove trailing slashes
|
|
128
|
-
const originalPath = path.replace(/^\/+|\/+$/g, '')
|
|
129
|
-
path = normalizePath(path)
|
|
130
|
-
|
|
131
|
-
/** @type {boolean|Route} */
|
|
132
|
-
let matchingRoute = false
|
|
133
|
-
let i = 0
|
|
134
|
-
while (!matchingRoute && i < routes.length) {
|
|
135
|
-
const route = routes[i]
|
|
136
|
-
route.path = normalizePath(route.path)
|
|
137
|
-
if (route.path === path) {
|
|
138
|
-
route.params = {}
|
|
139
|
-
matchingRoute = route
|
|
140
|
-
} else if (route.path.indexOf(':') > -1) {
|
|
141
|
-
// match dynamic route parts
|
|
142
|
-
const dynamicRouteParts = [...route.path.matchAll(/:([^\s/]+)/gi)]
|
|
143
|
-
|
|
144
|
-
// construct a regex for the route with dynamic parts
|
|
145
|
-
let dynamicRoutePartsRegex = route.path
|
|
146
|
-
dynamicRouteParts.reverse().forEach((part) => {
|
|
147
|
-
dynamicRoutePartsRegex =
|
|
148
|
-
dynamicRoutePartsRegex.substring(0, part.index) +
|
|
149
|
-
'([^\\s/]+)' +
|
|
150
|
-
dynamicRoutePartsRegex.substring(part.index + part[0].length)
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
dynamicRoutePartsRegex = '^' + dynamicRoutePartsRegex
|
|
154
|
-
|
|
155
|
-
// test if the constructed regex matches the path
|
|
156
|
-
const match = originalPath.match(new RegExp(`${dynamicRoutePartsRegex}`, 'i'))
|
|
157
|
-
|
|
158
|
-
if (match) {
|
|
159
|
-
// map the route params to a params object
|
|
160
|
-
route.params = dynamicRouteParts.reverse().reduce((acc, part, index) => {
|
|
161
|
-
acc[part[1]] = match[index + 1]
|
|
162
|
-
return acc
|
|
163
|
-
}, {})
|
|
164
|
-
matchingRoute = route
|
|
165
|
-
}
|
|
166
|
-
} else if (route.path.endsWith('*')) {
|
|
167
|
-
const regex = new RegExp(route.path.replace(/\/?\*/, '/?([^\\s]*)'), 'i')
|
|
168
|
-
const match = path.match(regex)
|
|
169
|
-
|
|
170
|
-
if (match) {
|
|
171
|
-
if (match[1]) route.params = { path: match[1] }
|
|
172
|
-
matchingRoute = route
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
i++
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (matchingRoute) {
|
|
179
|
-
matchingRoute.options = { ...matchingRoute.options, ...overrideOptions }
|
|
180
|
-
if (!matchingRoute.data) {
|
|
181
|
-
matchingRoute.data = {}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// @ts-ignore - Remove me when we have a better way to handle this
|
|
186
|
-
return matchingRoute
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Navigate to a route
|
|
191
|
-
*
|
|
192
|
-
* This isn't the prettiest way to do this, but it works. The reason is that extends
|
|
193
|
-
* only works for Classes or Factory functions. As such we need to use this
|
|
194
|
-
* @typedef {BlitsComponent & {
|
|
195
|
-
* activeView: BlitsComponent
|
|
196
|
-
* }} RouterViewComponent
|
|
197
|
-
*
|
|
198
|
-
* @this {RouterViewComponent} this
|
|
199
|
-
* @returns {Promise<void>}
|
|
200
|
-
*/
|
|
201
|
-
export const navigate = async function () {
|
|
202
|
-
Announcer.stop()
|
|
203
|
-
Announcer.clear()
|
|
204
|
-
state.navigating = true
|
|
205
|
-
if (this.parent[symbols.routes]) {
|
|
206
|
-
let previousRoute = currentRoute //? Object.assign({}, currentRoute) : undefined
|
|
207
|
-
const { hash, path, queryParams } = getHash()
|
|
208
|
-
let route = matchHash(path, this.parent[symbols.routes])
|
|
209
|
-
|
|
210
|
-
// Adding the location hash to the route if it exists.
|
|
211
|
-
if (hash !== null) {
|
|
212
|
-
route.hash = hash
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
currentRoute = route
|
|
216
|
-
if (route) {
|
|
217
|
-
let beforeEachResult
|
|
218
|
-
if (this.parent[symbols.routerHooks]) {
|
|
219
|
-
const hooks = this.parent[symbols.routerHooks]
|
|
220
|
-
if (hooks.beforeEach) {
|
|
221
|
-
beforeEachResult = await hooks.beforeEach(route, previousRoute)
|
|
222
|
-
if (isString(beforeEachResult)) {
|
|
223
|
-
to(beforeEachResult)
|
|
224
|
-
return
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
// If the resolved result is an object, assign it to the target route object
|
|
229
|
-
route = isObject(beforeEachResult) ? beforeEachResult : route
|
|
230
|
-
|
|
231
|
-
let beforeHookOutput
|
|
232
|
-
if (route.hooks) {
|
|
233
|
-
if (route.hooks.before) {
|
|
234
|
-
beforeHookOutput = await route.hooks.before.call(this.parent, route, previousRoute)
|
|
235
|
-
if (isString(beforeHookOutput)) {
|
|
236
|
-
currentRoute = previousRoute
|
|
237
|
-
to(beforeHookOutput)
|
|
238
|
-
return
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
route = isObject(beforeHookOutput) ? beforeHookOutput : route
|
|
243
|
-
// add the previous route (technically still the current route at this point)
|
|
244
|
-
// into the history stack, unless navigating back or inHistory flag of route is false
|
|
245
|
-
if (navigatingBack === false && previousRoute && previousRoute.options.inHistory === true) {
|
|
246
|
-
history.push(previousRoute)
|
|
247
|
-
}
|
|
248
|
-
// apply default transition if none specified
|
|
249
|
-
if (!('transition' in route)) {
|
|
250
|
-
/** @ts-ignore */
|
|
251
|
-
route.transition = fadeInFadeOutTransition
|
|
252
|
-
}
|
|
253
|
-
// a transition can be a function returning a dynamic transition object
|
|
254
|
-
// based on current and previous route
|
|
255
|
-
if (typeof route.transition === 'function') {
|
|
256
|
-
route.transition = route.transition(previousRoute, route)
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/** @type {import('../engines/L3/element.js').BlitsElement} */
|
|
260
|
-
let holder
|
|
261
|
-
let routeData
|
|
262
|
-
|
|
263
|
-
let { view, focus } = cacheMap.get(route.hash) || {}
|
|
264
|
-
|
|
265
|
-
// Announce route change if a message has been specified for this route
|
|
266
|
-
if (route.announce) {
|
|
267
|
-
if (typeof route.announce === 'string') {
|
|
268
|
-
route.announce = {
|
|
269
|
-
message: route.announce,
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
Announcer.speak(route.announce.message, route.announce.politeness)
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (!view) {
|
|
276
|
-
// create a holder element for the new view
|
|
277
|
-
holder = stage.element({ parent: this[symbols.children][0] })
|
|
278
|
-
holder.populate({})
|
|
279
|
-
holder.set('w', '100%')
|
|
280
|
-
holder.set('h', '100%')
|
|
281
|
-
|
|
282
|
-
const queryParamsData = {}
|
|
283
|
-
const queryParamsEntries = [...queryParams.entries()]
|
|
284
|
-
for (let i = 0; i < queryParamsEntries.length; i++) {
|
|
285
|
-
queryParamsData[queryParamsEntries[i][0]] = queryParamsEntries[i][1]
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
routeData = {
|
|
289
|
-
...navigationData,
|
|
290
|
-
...route.data,
|
|
291
|
-
...queryParamsData,
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// merge props with potential route params, navigation data and route data to be injected into the component instance
|
|
295
|
-
const props = {
|
|
296
|
-
...this[symbols.props],
|
|
297
|
-
...route.params,
|
|
298
|
-
...routeData,
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
view = await route.component({ props }, holder, this)
|
|
302
|
-
|
|
303
|
-
if (view[Symbol.toStringTag] === 'Module') {
|
|
304
|
-
if (view.default && typeof view.default === 'function') {
|
|
305
|
-
view = view.default({ props }, holder, this)
|
|
306
|
-
} else {
|
|
307
|
-
Log.error("Dynamic import doesn't have a default export or default is not a function")
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
if (typeof view === 'function') {
|
|
311
|
-
// had to inline this because the tscompiler does not like LHS reassignments
|
|
312
|
-
// that also change the type of the variable in a variable union
|
|
313
|
-
view = /** @type {BlitsComponentFactory} */ (view)({ props }, holder, this)
|
|
314
|
-
}
|
|
315
|
-
} else {
|
|
316
|
-
holder = view[symbols.holder]
|
|
317
|
-
|
|
318
|
-
// Check, whether cached view holder's alpha prop is exists in transition or not
|
|
319
|
-
let hasAlphaProp = false
|
|
320
|
-
if (route.transition.before) {
|
|
321
|
-
if (Array.isArray(route.transition.before)) {
|
|
322
|
-
for (let i = 0; i < route.transition.before.length; i++) {
|
|
323
|
-
if (route.transition.before[i].prop === 'alpha') {
|
|
324
|
-
hasAlphaProp = true
|
|
325
|
-
break
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
} else if (route.transition.before.prop === 'alpha') {
|
|
329
|
-
hasAlphaProp = true
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
// set holder alpha when alpha prop is not exists in route transition
|
|
333
|
-
if (hasAlphaProp === false) {
|
|
334
|
-
holder.set('alpha', 1)
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
this[symbols.children].push(view)
|
|
339
|
-
|
|
340
|
-
// keep reference to the previous focus for storing in cache
|
|
341
|
-
previousFocus = Focus.get()
|
|
342
|
-
|
|
343
|
-
// set focus to the view that we're routing to
|
|
344
|
-
focus ? focus.$focus() : /** @type {BlitsComponent} */ (view).$focus()
|
|
345
|
-
|
|
346
|
-
// apply before settings to holder element
|
|
347
|
-
if (route.transition.before) {
|
|
348
|
-
if (Array.isArray(route.transition.before)) {
|
|
349
|
-
for (let i = 0; i < route.transition.before.length; i++) {
|
|
350
|
-
holder.set(route.transition.before[i].prop, route.transition.before[i].value)
|
|
351
|
-
}
|
|
352
|
-
} else {
|
|
353
|
-
holder.set(route.transition.before.prop, route.transition.before.value)
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
let shouldAnimate = false
|
|
358
|
-
|
|
359
|
-
// apply out out transition on previous view
|
|
360
|
-
if (previousRoute) {
|
|
361
|
-
// only animate when there is a previous route
|
|
362
|
-
shouldAnimate = true
|
|
363
|
-
const oldView = this[symbols.children].splice(1, 1).pop()
|
|
364
|
-
if (oldView) {
|
|
365
|
-
removeView(previousRoute, oldView, route.transition.out)
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
state.path = route.path
|
|
370
|
-
state.params = route.params
|
|
371
|
-
state.hash = hash
|
|
372
|
-
state.data = routeData
|
|
373
|
-
|
|
374
|
-
// apply in transition
|
|
375
|
-
if (route.transition.in) {
|
|
376
|
-
if (Array.isArray(route.transition.in)) {
|
|
377
|
-
for (let i = 0; i < route.transition.in.length; i++) {
|
|
378
|
-
i === route.transition.length - 1
|
|
379
|
-
? await setOrAnimate(holder, route.transition.in[i], shouldAnimate)
|
|
380
|
-
: setOrAnimate(holder, route.transition.in[i], shouldAnimate)
|
|
381
|
-
}
|
|
382
|
-
} else {
|
|
383
|
-
await setOrAnimate(holder, route.transition.in, shouldAnimate)
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
this.activeView = this[symbols.children][this[symbols.children].length - 1]
|
|
388
|
-
} else {
|
|
389
|
-
Log.error(`Route ${hash} not found`)
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// reset navigating indicators
|
|
394
|
-
navigatingBack = false
|
|
395
|
-
state.navigating = false
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* Remove the currently active view
|
|
400
|
-
*
|
|
401
|
-
* @param {Route} route
|
|
402
|
-
* @param {BlitsComponent} view
|
|
403
|
-
* @param {Object} transition
|
|
404
|
-
*/
|
|
405
|
-
const removeView = async (route, view, transition) => {
|
|
406
|
-
// apply out transition
|
|
407
|
-
if (transition) {
|
|
408
|
-
if (Array.isArray(transition)) {
|
|
409
|
-
for (let i = 0; i < transition.length; i++) {
|
|
410
|
-
i === transition.length - 1
|
|
411
|
-
? await setOrAnimate(view[symbols.holder], transition[i])
|
|
412
|
-
: setOrAnimate(view[symbols.holder], transition[i])
|
|
413
|
-
}
|
|
414
|
-
} else {
|
|
415
|
-
await setOrAnimate(view[symbols.holder], transition)
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// cache the page when it's as 'keepAlive' instead of destroying
|
|
420
|
-
if (navigatingBack === false && route.options && route.options.keepAlive === true) {
|
|
421
|
-
cacheMap.set(route.hash, { view: view, focus: previousFocus })
|
|
422
|
-
} else if (navigatingBack === true) {
|
|
423
|
-
// remove the previous route from the cache when navigating back
|
|
424
|
-
// cacheMap.delete will not throw an error if the route is not in the cache
|
|
425
|
-
cacheMap.delete(route.hash)
|
|
426
|
-
}
|
|
427
|
-
/* Destroy the view in the following cases:
|
|
428
|
-
* 1. Navigating forward, and the previous route is not configured with "keep alive" set to true.
|
|
429
|
-
* 2. Navigating back, and the previous route is configured with "keep alive" set to true.
|
|
430
|
-
* 3. Navigating back, and the previous route is not configured with "keep alive" set to true.
|
|
431
|
-
*/
|
|
432
|
-
if (route.options && (route.options.keepAlive !== true || navigatingBack === true)) {
|
|
433
|
-
view.destroy()
|
|
434
|
-
view = null
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const setOrAnimate = (node, transition, shouldAnimate = true) => {
|
|
439
|
-
return new Promise((resolve) => {
|
|
440
|
-
if (shouldAnimate === true) {
|
|
441
|
-
// resolve the promise in the transition end-callback
|
|
442
|
-
// ("extending" end callback when one is already specified)
|
|
443
|
-
let existingEndCallback = transition.end
|
|
444
|
-
transition.end = (...args) => {
|
|
445
|
-
existingEndCallback && existingEndCallback(args)
|
|
446
|
-
// null the callback to enable memory cleanup
|
|
447
|
-
existingEndCallback = null
|
|
448
|
-
resolve()
|
|
449
|
-
}
|
|
450
|
-
node.set(transition.prop, { transition })
|
|
451
|
-
} else {
|
|
452
|
-
node.set(transition.prop, transition.value)
|
|
453
|
-
resolve()
|
|
454
|
-
}
|
|
455
|
-
})
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
export const to = (location, data = {}, options = {}) => {
|
|
459
|
-
navigationData = data
|
|
460
|
-
overrideOptions = options
|
|
461
|
-
|
|
462
|
-
window.location.hash = location
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
export const back = function () {
|
|
466
|
-
const route = history.pop()
|
|
467
|
-
if (route && currentRoute !== route) {
|
|
468
|
-
// set indicator that we are navigating back (to prevent adding page to history stack)
|
|
469
|
-
navigatingBack = true
|
|
470
|
-
|
|
471
|
-
to(route.hash, route.data, route.options)
|
|
472
|
-
return true
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
const backtrack = (currentRoute && currentRoute.options.backtrack) || false
|
|
476
|
-
|
|
477
|
-
// If we deeplink to a page without backtrack
|
|
478
|
-
// we we let the RouterView handle back
|
|
479
|
-
if (backtrack === false) {
|
|
480
|
-
return false
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const hashEnd = /(\/:?[\w%\s-]+)$/
|
|
484
|
-
let path = currentRoute.path
|
|
485
|
-
|
|
486
|
-
let level = path.split('/').length
|
|
487
|
-
|
|
488
|
-
// On root return
|
|
489
|
-
if (level <= 1) {
|
|
490
|
-
return false
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
while (level--) {
|
|
494
|
-
if (!hashEnd.test(path)) {
|
|
495
|
-
return false
|
|
496
|
-
}
|
|
497
|
-
// Construct new path to backtrack to
|
|
498
|
-
path = path.replace(hashEnd, '')
|
|
499
|
-
const route = matchHash(path, this.parent[symbols.routes])
|
|
500
|
-
|
|
501
|
-
if (route && backtrack) {
|
|
502
|
-
to(route.path)
|
|
503
|
-
return true
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
return false
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
export default {
|
|
511
|
-
navigate,
|
|
512
|
-
to,
|
|
513
|
-
back,
|
|
514
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2023 Comcast Cable Communications Management, LLC
|
|
3
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License.
|
|
5
|
+
* You may obtain a copy of the License at
|
|
6
|
+
*
|
|
7
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
* See the License for the specific language governing permissions and
|
|
13
|
+
* limitations under the License.
|
|
14
|
+
*
|
|
15
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { default as fadeInFadeOutTransition } from './transitions/fadeInOut.js'
|
|
19
|
+
import { reactive } from '../lib/reactivity/reactive.js'
|
|
20
|
+
|
|
21
|
+
import symbols from '../lib/symbols.js'
|
|
22
|
+
import { Log } from '../lib/log.js'
|
|
23
|
+
import { stage } from '../launch.js'
|
|
24
|
+
import Focus from '../focus.js'
|
|
25
|
+
import Announcer from '../announcer/announcer.js'
|
|
26
|
+
import Settings from '../settings.js'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {import('../component.js').BlitsComponentFactory} BlitsComponentFactory - The component of the route
|
|
30
|
+
* @typedef {import('../component.js').BlitsComponent} BlitsComponent - The element of the route
|
|
31
|
+
* @typedef {import('../engines/L3/element.js').BlitsElement} BlitsElement - The element of the route
|
|
32
|
+
*
|
|
33
|
+
* @typedef {Object} Route
|
|
34
|
+
* @property {string} path - The path of the route
|
|
35
|
+
* @property {string} hash - The hash of the route
|
|
36
|
+
* @property {Object} params - The params of the route
|
|
37
|
+
* @property {Object} data - The data of the route
|
|
38
|
+
* @property {Object} options - The options of the route
|
|
39
|
+
* @property {Object} hooks - The hooks of the route
|
|
40
|
+
* @property {Object} transition - The transition of the route
|
|
41
|
+
* @property {Object} announce - The announce of the route
|
|
42
|
+
* @property {(options: Object, parentEl: BlitsElement, parentComponent: BlitsComponent, rootComponent?: BlitsComponent) => BlitsComponent} component - The component factory of the route
|
|
43
|
+
*
|
|
44
|
+
* @typedef {Object} Hash
|
|
45
|
+
* @property {string} path - The path of the hash
|
|
46
|
+
* @property {URLSearchParams} queryParams - The query params of the hash
|
|
47
|
+
* @property {string} hash - The hash
|
|
48
|
+
*
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/** @type {Route} */
|
|
52
|
+
export let currentRoute
|
|
53
|
+
export const state = reactive(
|
|
54
|
+
{
|
|
55
|
+
path: '',
|
|
56
|
+
navigating: false,
|
|
57
|
+
data: null,
|
|
58
|
+
params: null,
|
|
59
|
+
hash: '',
|
|
60
|
+
},
|
|
61
|
+
Settings.get('reactivityMode'),
|
|
62
|
+
true
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
// Changed from WeakMap to Map to allow for caching of views by the url hash.
|
|
66
|
+
// We are manually doing the cleanup of the cache when the route is not marked as keepAlive.
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @typedef {Object} CacheMapEntry
|
|
70
|
+
* @property {BlitsComponent|BlitsComponentFactory} view - The view of the route
|
|
71
|
+
* @property {BlitsComponent} focus - The focus of the route
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/** @type {Map<String, CacheMapEntry>} */
|
|
75
|
+
const cacheMap = new Map()
|
|
76
|
+
const history = []
|
|
77
|
+
|
|
78
|
+
let overrideOptions = {}
|
|
79
|
+
let navigationData = {}
|
|
80
|
+
let navigatingBack = false
|
|
81
|
+
let previousFocus
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get the current hash
|
|
85
|
+
* @returns {Hash}
|
|
86
|
+
*/
|
|
87
|
+
export const getHash = () => {
|
|
88
|
+
const hashParts = (document.location.hash || '/').replace(/^#/, '').split('?')
|
|
89
|
+
return {
|
|
90
|
+
path: hashParts[0],
|
|
91
|
+
queryParams: new URLSearchParams(hashParts[1]),
|
|
92
|
+
hash: document.location.hash,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const normalizePath = (path) => {
|
|
97
|
+
return (
|
|
98
|
+
path
|
|
99
|
+
// remove leading and trailing slashes
|
|
100
|
+
.replace(/^\/+|\/+$/g, '')
|
|
101
|
+
.toLowerCase()
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if a value is an object
|
|
107
|
+
* @param {any} v
|
|
108
|
+
* @returns {boolean} True if v is an object
|
|
109
|
+
*/
|
|
110
|
+
const isObject = (v) => typeof v === 'object' && v !== null
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if a value is a function
|
|
114
|
+
* @param {any} v
|
|
115
|
+
* @returns {boolean} True if v is a string
|
|
116
|
+
*/
|
|
117
|
+
const isString = (v) => typeof v === 'string'
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Match a path to a route
|
|
121
|
+
*
|
|
122
|
+
* @param {string} path
|
|
123
|
+
* @param {Route[]} routes
|
|
124
|
+
* @returns {Route}
|
|
125
|
+
*/
|
|
126
|
+
export const matchHash = (path, routes = []) => {
|
|
127
|
+
// remove trailing slashes
|
|
128
|
+
const originalPath = path.replace(/^\/+|\/+$/g, '')
|
|
129
|
+
path = normalizePath(path)
|
|
130
|
+
|
|
131
|
+
/** @type {boolean|Route} */
|
|
132
|
+
let matchingRoute = false
|
|
133
|
+
let i = 0
|
|
134
|
+
while (!matchingRoute && i < routes.length) {
|
|
135
|
+
const route = routes[i]
|
|
136
|
+
route.path = normalizePath(route.path)
|
|
137
|
+
if (route.path === path) {
|
|
138
|
+
route.params = {}
|
|
139
|
+
matchingRoute = route
|
|
140
|
+
} else if (route.path.indexOf(':') > -1) {
|
|
141
|
+
// match dynamic route parts
|
|
142
|
+
const dynamicRouteParts = [...route.path.matchAll(/:([^\s/]+)/gi)]
|
|
143
|
+
|
|
144
|
+
// construct a regex for the route with dynamic parts
|
|
145
|
+
let dynamicRoutePartsRegex = route.path
|
|
146
|
+
dynamicRouteParts.reverse().forEach((part) => {
|
|
147
|
+
dynamicRoutePartsRegex =
|
|
148
|
+
dynamicRoutePartsRegex.substring(0, part.index) +
|
|
149
|
+
'([^\\s/]+)' +
|
|
150
|
+
dynamicRoutePartsRegex.substring(part.index + part[0].length)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
dynamicRoutePartsRegex = '^' + dynamicRoutePartsRegex
|
|
154
|
+
|
|
155
|
+
// test if the constructed regex matches the path
|
|
156
|
+
const match = originalPath.match(new RegExp(`${dynamicRoutePartsRegex}`, 'i'))
|
|
157
|
+
|
|
158
|
+
if (match) {
|
|
159
|
+
// map the route params to a params object
|
|
160
|
+
route.params = dynamicRouteParts.reverse().reduce((acc, part, index) => {
|
|
161
|
+
acc[part[1]] = match[index + 1]
|
|
162
|
+
return acc
|
|
163
|
+
}, {})
|
|
164
|
+
matchingRoute = route
|
|
165
|
+
}
|
|
166
|
+
} else if (route.path.endsWith('*')) {
|
|
167
|
+
const regex = new RegExp(route.path.replace(/\/?\*/, '/?([^\\s]*)'), 'i')
|
|
168
|
+
const match = path.match(regex)
|
|
169
|
+
|
|
170
|
+
if (match) {
|
|
171
|
+
if (match[1]) route.params = { path: match[1] }
|
|
172
|
+
matchingRoute = route
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
i++
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (matchingRoute) {
|
|
179
|
+
matchingRoute.options = { ...matchingRoute.options, ...overrideOptions }
|
|
180
|
+
if (!matchingRoute.data) {
|
|
181
|
+
matchingRoute.data = {}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// @ts-ignore - Remove me when we have a better way to handle this
|
|
186
|
+
return matchingRoute
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Navigate to a route
|
|
191
|
+
*
|
|
192
|
+
* This isn't the prettiest way to do this, but it works. The reason is that extends
|
|
193
|
+
* only works for Classes or Factory functions. As such we need to use this
|
|
194
|
+
* @typedef {BlitsComponent & {
|
|
195
|
+
* activeView: BlitsComponent
|
|
196
|
+
* }} RouterViewComponent
|
|
197
|
+
*
|
|
198
|
+
* @this {RouterViewComponent} this
|
|
199
|
+
* @returns {Promise<void>}
|
|
200
|
+
*/
|
|
201
|
+
export const navigate = async function () {
|
|
202
|
+
Announcer.stop()
|
|
203
|
+
Announcer.clear()
|
|
204
|
+
state.navigating = true
|
|
205
|
+
if (this.parent[symbols.routes]) {
|
|
206
|
+
let previousRoute = currentRoute //? Object.assign({}, currentRoute) : undefined
|
|
207
|
+
const { hash, path, queryParams } = getHash()
|
|
208
|
+
let route = matchHash(path, this.parent[symbols.routes])
|
|
209
|
+
|
|
210
|
+
// Adding the location hash to the route if it exists.
|
|
211
|
+
if (hash !== null) {
|
|
212
|
+
route.hash = hash
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
currentRoute = route
|
|
216
|
+
if (route) {
|
|
217
|
+
let beforeEachResult
|
|
218
|
+
if (this.parent[symbols.routerHooks]) {
|
|
219
|
+
const hooks = this.parent[symbols.routerHooks]
|
|
220
|
+
if (hooks.beforeEach) {
|
|
221
|
+
beforeEachResult = await hooks.beforeEach(route, previousRoute)
|
|
222
|
+
if (isString(beforeEachResult)) {
|
|
223
|
+
to(beforeEachResult)
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// If the resolved result is an object, assign it to the target route object
|
|
229
|
+
route = isObject(beforeEachResult) ? beforeEachResult : route
|
|
230
|
+
|
|
231
|
+
let beforeHookOutput
|
|
232
|
+
if (route.hooks) {
|
|
233
|
+
if (route.hooks.before) {
|
|
234
|
+
beforeHookOutput = await route.hooks.before.call(this.parent, route, previousRoute)
|
|
235
|
+
if (isString(beforeHookOutput)) {
|
|
236
|
+
currentRoute = previousRoute
|
|
237
|
+
to(beforeHookOutput)
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
route = isObject(beforeHookOutput) ? beforeHookOutput : route
|
|
243
|
+
// add the previous route (technically still the current route at this point)
|
|
244
|
+
// into the history stack, unless navigating back or inHistory flag of route is false
|
|
245
|
+
if (navigatingBack === false && previousRoute && previousRoute.options.inHistory === true) {
|
|
246
|
+
history.push(previousRoute)
|
|
247
|
+
}
|
|
248
|
+
// apply default transition if none specified
|
|
249
|
+
if (!('transition' in route)) {
|
|
250
|
+
/** @ts-ignore */
|
|
251
|
+
route.transition = fadeInFadeOutTransition
|
|
252
|
+
}
|
|
253
|
+
// a transition can be a function returning a dynamic transition object
|
|
254
|
+
// based on current and previous route
|
|
255
|
+
if (typeof route.transition === 'function') {
|
|
256
|
+
route.transition = route.transition(previousRoute, route)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** @type {import('../engines/L3/element.js').BlitsElement} */
|
|
260
|
+
let holder
|
|
261
|
+
let routeData
|
|
262
|
+
|
|
263
|
+
let { view, focus } = cacheMap.get(route.hash) || {}
|
|
264
|
+
|
|
265
|
+
// Announce route change if a message has been specified for this route
|
|
266
|
+
if (route.announce) {
|
|
267
|
+
if (typeof route.announce === 'string') {
|
|
268
|
+
route.announce = {
|
|
269
|
+
message: route.announce,
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
Announcer.speak(route.announce.message, route.announce.politeness)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!view) {
|
|
276
|
+
// create a holder element for the new view
|
|
277
|
+
holder = stage.element({ parent: this[symbols.children][0] })
|
|
278
|
+
holder.populate({})
|
|
279
|
+
holder.set('w', '100%')
|
|
280
|
+
holder.set('h', '100%')
|
|
281
|
+
|
|
282
|
+
const queryParamsData = {}
|
|
283
|
+
const queryParamsEntries = [...queryParams.entries()]
|
|
284
|
+
for (let i = 0; i < queryParamsEntries.length; i++) {
|
|
285
|
+
queryParamsData[queryParamsEntries[i][0]] = queryParamsEntries[i][1]
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
routeData = {
|
|
289
|
+
...navigationData,
|
|
290
|
+
...route.data,
|
|
291
|
+
...queryParamsData,
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// merge props with potential route params, navigation data and route data to be injected into the component instance
|
|
295
|
+
const props = {
|
|
296
|
+
...this[symbols.props],
|
|
297
|
+
...route.params,
|
|
298
|
+
...routeData,
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
view = await route.component({ props }, holder, this)
|
|
302
|
+
|
|
303
|
+
if (view[Symbol.toStringTag] === 'Module') {
|
|
304
|
+
if (view.default && typeof view.default === 'function') {
|
|
305
|
+
view = view.default({ props }, holder, this)
|
|
306
|
+
} else {
|
|
307
|
+
Log.error("Dynamic import doesn't have a default export or default is not a function")
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (typeof view === 'function') {
|
|
311
|
+
// had to inline this because the tscompiler does not like LHS reassignments
|
|
312
|
+
// that also change the type of the variable in a variable union
|
|
313
|
+
view = /** @type {BlitsComponentFactory} */ (view)({ props }, holder, this)
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
holder = view[symbols.holder]
|
|
317
|
+
|
|
318
|
+
// Check, whether cached view holder's alpha prop is exists in transition or not
|
|
319
|
+
let hasAlphaProp = false
|
|
320
|
+
if (route.transition.before) {
|
|
321
|
+
if (Array.isArray(route.transition.before)) {
|
|
322
|
+
for (let i = 0; i < route.transition.before.length; i++) {
|
|
323
|
+
if (route.transition.before[i].prop === 'alpha') {
|
|
324
|
+
hasAlphaProp = true
|
|
325
|
+
break
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} else if (route.transition.before.prop === 'alpha') {
|
|
329
|
+
hasAlphaProp = true
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// set holder alpha when alpha prop is not exists in route transition
|
|
333
|
+
if (hasAlphaProp === false) {
|
|
334
|
+
holder.set('alpha', 1)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
this[symbols.children].push(view)
|
|
339
|
+
|
|
340
|
+
// keep reference to the previous focus for storing in cache
|
|
341
|
+
previousFocus = Focus.get()
|
|
342
|
+
|
|
343
|
+
// set focus to the view that we're routing to
|
|
344
|
+
focus ? focus.$focus() : /** @type {BlitsComponent} */ (view).$focus()
|
|
345
|
+
|
|
346
|
+
// apply before settings to holder element
|
|
347
|
+
if (route.transition.before) {
|
|
348
|
+
if (Array.isArray(route.transition.before)) {
|
|
349
|
+
for (let i = 0; i < route.transition.before.length; i++) {
|
|
350
|
+
holder.set(route.transition.before[i].prop, route.transition.before[i].value)
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
holder.set(route.transition.before.prop, route.transition.before.value)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let shouldAnimate = false
|
|
358
|
+
|
|
359
|
+
// apply out out transition on previous view
|
|
360
|
+
if (previousRoute) {
|
|
361
|
+
// only animate when there is a previous route
|
|
362
|
+
shouldAnimate = true
|
|
363
|
+
const oldView = this[symbols.children].splice(1, 1).pop()
|
|
364
|
+
if (oldView) {
|
|
365
|
+
removeView(previousRoute, oldView, route.transition.out)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
state.path = route.path
|
|
370
|
+
state.params = route.params
|
|
371
|
+
state.hash = hash
|
|
372
|
+
state.data = routeData
|
|
373
|
+
|
|
374
|
+
// apply in transition
|
|
375
|
+
if (route.transition.in) {
|
|
376
|
+
if (Array.isArray(route.transition.in)) {
|
|
377
|
+
for (let i = 0; i < route.transition.in.length; i++) {
|
|
378
|
+
i === route.transition.length - 1
|
|
379
|
+
? await setOrAnimate(holder, route.transition.in[i], shouldAnimate)
|
|
380
|
+
: setOrAnimate(holder, route.transition.in[i], shouldAnimate)
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
await setOrAnimate(holder, route.transition.in, shouldAnimate)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
this.activeView = this[symbols.children][this[symbols.children].length - 1]
|
|
388
|
+
} else {
|
|
389
|
+
Log.error(`Route ${hash} not found`)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// reset navigating indicators
|
|
394
|
+
navigatingBack = false
|
|
395
|
+
state.navigating = false
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Remove the currently active view
|
|
400
|
+
*
|
|
401
|
+
* @param {Route} route
|
|
402
|
+
* @param {BlitsComponent} view
|
|
403
|
+
* @param {Object} transition
|
|
404
|
+
*/
|
|
405
|
+
const removeView = async (route, view, transition) => {
|
|
406
|
+
// apply out transition
|
|
407
|
+
if (transition) {
|
|
408
|
+
if (Array.isArray(transition)) {
|
|
409
|
+
for (let i = 0; i < transition.length; i++) {
|
|
410
|
+
i === transition.length - 1
|
|
411
|
+
? await setOrAnimate(view[symbols.holder], transition[i])
|
|
412
|
+
: setOrAnimate(view[symbols.holder], transition[i])
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
await setOrAnimate(view[symbols.holder], transition)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// cache the page when it's as 'keepAlive' instead of destroying
|
|
420
|
+
if (navigatingBack === false && route.options && route.options.keepAlive === true) {
|
|
421
|
+
cacheMap.set(route.hash, { view: view, focus: previousFocus })
|
|
422
|
+
} else if (navigatingBack === true) {
|
|
423
|
+
// remove the previous route from the cache when navigating back
|
|
424
|
+
// cacheMap.delete will not throw an error if the route is not in the cache
|
|
425
|
+
cacheMap.delete(route.hash)
|
|
426
|
+
}
|
|
427
|
+
/* Destroy the view in the following cases:
|
|
428
|
+
* 1. Navigating forward, and the previous route is not configured with "keep alive" set to true.
|
|
429
|
+
* 2. Navigating back, and the previous route is configured with "keep alive" set to true.
|
|
430
|
+
* 3. Navigating back, and the previous route is not configured with "keep alive" set to true.
|
|
431
|
+
*/
|
|
432
|
+
if (route.options && (route.options.keepAlive !== true || navigatingBack === true)) {
|
|
433
|
+
view.destroy()
|
|
434
|
+
view = null
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const setOrAnimate = (node, transition, shouldAnimate = true) => {
|
|
439
|
+
return new Promise((resolve) => {
|
|
440
|
+
if (shouldAnimate === true) {
|
|
441
|
+
// resolve the promise in the transition end-callback
|
|
442
|
+
// ("extending" end callback when one is already specified)
|
|
443
|
+
let existingEndCallback = transition.end
|
|
444
|
+
transition.end = (...args) => {
|
|
445
|
+
existingEndCallback && existingEndCallback(args)
|
|
446
|
+
// null the callback to enable memory cleanup
|
|
447
|
+
existingEndCallback = null
|
|
448
|
+
resolve()
|
|
449
|
+
}
|
|
450
|
+
node.set(transition.prop, { transition })
|
|
451
|
+
} else {
|
|
452
|
+
node.set(transition.prop, transition.value)
|
|
453
|
+
resolve()
|
|
454
|
+
}
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export const to = (location, data = {}, options = {}) => {
|
|
459
|
+
navigationData = data
|
|
460
|
+
overrideOptions = options
|
|
461
|
+
|
|
462
|
+
window.location.hash = location
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export const back = function () {
|
|
466
|
+
const route = history.pop()
|
|
467
|
+
if (route && currentRoute !== route) {
|
|
468
|
+
// set indicator that we are navigating back (to prevent adding page to history stack)
|
|
469
|
+
navigatingBack = true
|
|
470
|
+
|
|
471
|
+
to(route.hash, route.data, route.options)
|
|
472
|
+
return true
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const backtrack = (currentRoute && currentRoute.options.backtrack) || false
|
|
476
|
+
|
|
477
|
+
// If we deeplink to a page without backtrack
|
|
478
|
+
// we we let the RouterView handle back
|
|
479
|
+
if (backtrack === false) {
|
|
480
|
+
return false
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const hashEnd = /(\/:?[\w%\s-]+)$/
|
|
484
|
+
let path = currentRoute.path
|
|
485
|
+
|
|
486
|
+
let level = path.split('/').length
|
|
487
|
+
|
|
488
|
+
// On root return
|
|
489
|
+
if (level <= 1) {
|
|
490
|
+
return false
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
while (level--) {
|
|
494
|
+
if (!hashEnd.test(path)) {
|
|
495
|
+
return false
|
|
496
|
+
}
|
|
497
|
+
// Construct new path to backtrack to
|
|
498
|
+
path = path.replace(hashEnd, '')
|
|
499
|
+
const route = matchHash(path, this.parent[symbols.routes])
|
|
500
|
+
|
|
501
|
+
if (route && backtrack) {
|
|
502
|
+
to(route.path)
|
|
503
|
+
return true
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return false
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export default {
|
|
511
|
+
navigate,
|
|
512
|
+
to,
|
|
513
|
+
back,
|
|
514
|
+
}
|