@lightningjs/blits 1.30.2 → 1.31.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.
Files changed (152) hide show
  1. package/CHANGELOG.md +1029 -1019
  2. package/LICENSE +202 -202
  3. package/NOTICE +12 -12
  4. package/README.md +33 -33
  5. package/bin/index.js +8 -8
  6. package/blits.d.ts +6 -6
  7. package/index.d.ts +1099 -1099
  8. package/index.js +28 -28
  9. package/package.json +57 -56
  10. package/public/assets/fps_sprite.base64.js +1 -1
  11. package/scripts/postpublish.js +59 -59
  12. package/scripts/prepublishOnly.js +84 -84
  13. package/scripts/prepush.js +78 -78
  14. package/src/announcer/announcer.js +162 -162
  15. package/src/announcer/speechSynthesis.js +107 -107
  16. package/src/application.js +100 -100
  17. package/src/component/base/announcer.js +27 -27
  18. package/src/component/base/events.js +69 -69
  19. package/src/component/base/index.js +33 -33
  20. package/src/component/base/methods.js +244 -244
  21. package/src/component/base/router.js +41 -41
  22. package/src/component/base/timeouts_intervals.js +99 -99
  23. package/src/component/base/utils.js +72 -72
  24. package/src/component/setup/computed.js +48 -48
  25. package/src/component/setup/index.js +62 -62
  26. package/src/component/setup/input.js +30 -30
  27. package/src/component/setup/methods.js +37 -37
  28. package/src/component/setup/props.js +62 -62
  29. package/src/component/setup/routes.js +42 -42
  30. package/src/component/setup/state.js +59 -59
  31. package/src/component/setup/watch.js +35 -35
  32. package/src/component.js +437 -437
  33. package/src/components/Circle.js +73 -73
  34. package/src/components/FPScounter.js +1510 -1510
  35. package/src/components/RouterView.js +81 -81
  36. package/src/components/Sprite.js +94 -94
  37. package/src/components/index.js +28 -28
  38. package/src/constants.js +21 -21
  39. package/src/engine.js +1 -1
  40. package/src/engines/L3/element.js +744 -744
  41. package/src/engines/L3/fontLoader.js +38 -38
  42. package/src/engines/L3/index.js +7 -7
  43. package/src/engines/L3/launch.js +123 -123
  44. package/src/engines/L3/shaderLoader.js +12 -12
  45. package/src/engines/index.js +1 -1
  46. package/src/focus.js +90 -90
  47. package/src/helpers/deepEqualArray.js +50 -50
  48. package/src/helpers/fetchJson.js +34 -34
  49. package/src/helpers/sequence.js +26 -26
  50. package/src/launch.js +123 -123
  51. package/src/lib/blitsfileconverter/blitsfileconverter.js +52 -52
  52. package/src/lib/codegenerator/generator.js +788 -788
  53. package/src/lib/colors/colors.js +92 -92
  54. package/src/lib/colors/htmlColors.js +168 -168
  55. package/src/lib/componentId.js +49 -49
  56. package/src/lib/eventListeners.js +123 -123
  57. package/src/lib/hooks.js +59 -59
  58. package/src/lib/lifecycle.js +85 -85
  59. package/src/lib/log.js +117 -117
  60. package/src/lib/precompiler/precompiler.js +73 -73
  61. package/src/lib/reactivity/effect.js +93 -93
  62. package/src/lib/reactivity/reactive.js +208 -208
  63. package/src/lib/reactivityguard/computedprops.js +218 -218
  64. package/src/lib/symbols.js +108 -108
  65. package/src/lib/templateparser/parser.js +386 -386
  66. package/src/lib/utils.js +27 -27
  67. package/src/plugin.js +27 -27
  68. package/src/plugins/appstate.js +26 -26
  69. package/src/plugins/index.js +20 -20
  70. package/src/plugins/language.js +152 -152
  71. package/src/plugins/log.js +25 -25
  72. package/src/plugins/theme.js +94 -94
  73. package/src/router/router.js +516 -514
  74. package/src/router/transitions/fadeIn.js +27 -27
  75. package/src/router/transitions/fadeInOut.js +33 -33
  76. package/src/router/transitions/index.js +21 -21
  77. package/src/router/transitions/slideIn.js +28 -28
  78. package/src/router/transitions/slideInOut.js +33 -33
  79. package/src/settings.js +40 -40
  80. package/vite/blitsFileConverter.js +67 -67
  81. package/vite/index.js +28 -28
  82. package/vite/msdfGenerator.js +269 -269
  83. package/vite/preCompiler.js +47 -42
  84. package/vite/reactivityGuard.js +45 -45
  85. package/vscode/data/template-attributes.json +589 -589
  86. package/.editorconfig +0 -7
  87. package/.eslintignore +0 -1
  88. package/.eslintrc.cjs +0 -65
  89. package/.github/workflows/comment-lint-results.yml +0 -125
  90. package/.github/workflows/comment-test-results.yml +0 -109
  91. package/.github/workflows/deploy-docs.yml +0 -27
  92. package/.github/workflows/run-blits-tests.yml +0 -142
  93. package/.github/workflows/run-lint.yml +0 -128
  94. package/.husky/pre-commit +0 -1
  95. package/.husky/pre-push +0 -1
  96. package/.vscode/extensions.json +0 -7
  97. package/.vscode/settings.json +0 -9
  98. package/COPYING +0 -1
  99. package/banner.png +0 -0
  100. package/docs/.nojekyll +0 -0
  101. package/docs/README.md +0 -19
  102. package/docs/_sidebar.md +0 -37
  103. package/docs/built-in/directives.md +0 -50
  104. package/docs/built-in/for-loop.md +0 -204
  105. package/docs/built-in/layout.md +0 -168
  106. package/docs/components/component_state.md +0 -90
  107. package/docs/components/computed_properties.md +0 -45
  108. package/docs/components/lifecycle_events.md +0 -42
  109. package/docs/components/methods.md +0 -45
  110. package/docs/components/props.md +0 -56
  111. package/docs/components/user_input.md +0 -186
  112. package/docs/components/utility_methods.md +0 -320
  113. package/docs/components/watchers.md +0 -34
  114. package/docs/essentials/application_root.md +0 -19
  115. package/docs/essentials/components.md +0 -68
  116. package/docs/essentials/displaying_images.md +0 -67
  117. package/docs/essentials/displaying_text.md +0 -135
  118. package/docs/essentials/element_attributes.md +0 -195
  119. package/docs/essentials/settings.md +0 -117
  120. package/docs/essentials/template_syntax.md +0 -83
  121. package/docs/getting_started/file_structure.md +0 -58
  122. package/docs/getting_started/getting_started.md +0 -45
  123. package/docs/getting_started/intro.md +0 -20
  124. package/docs/index.html +0 -47
  125. package/docs/performance/lazy-loading.md +0 -161
  126. package/docs/plugins/global_app_state.md +0 -98
  127. package/docs/plugins/language.md +0 -172
  128. package/docs/plugins/text-to-speech-announcer.md +0 -120
  129. package/docs/plugins/theme.md +0 -176
  130. package/docs/router/basics.md +0 -175
  131. package/docs/router/hooks.md +0 -70
  132. package/docs/router/transitions.md +0 -146
  133. package/docs/sidebar.json +0 -171
  134. package/docs/transitions_animations/transitions.md +0 -150
  135. package/jsconfig.json +0 -12
  136. package/src/component/setup/props.test.js +0 -238
  137. package/src/component.test.js +0 -388
  138. package/src/components/Circle.orig.js +0 -31
  139. package/src/components/FPScounter.orig.js +0 -121
  140. package/src/components/RouterView.orig.js +0 -54
  141. package/src/components/Sprite.orig.js +0 -58
  142. package/src/components/index.orig.js +0 -28
  143. package/src/engines/L3/element.test.js +0 -579
  144. package/src/helpers/deepEqualArray.test.js +0 -127
  145. package/src/lib/codegenerator/generator.test.js +0 -3801
  146. package/src/lib/colors/colors.test.js +0 -290
  147. package/src/lib/componentId.test.js +0 -84
  148. package/src/lib/eventListeners.test.js +0 -236
  149. package/src/lib/reactivity/effect.test.js +0 -35
  150. package/src/lib/reactivity/reactive.test.js +0 -27
  151. package/src/lib/templateparser/parser.test.js +0 -1411
  152. package/src/router/router.test.js +0 -473
@@ -1,514 +1,516 @@
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
+ previousFocus = null
438
+ }
439
+
440
+ const setOrAnimate = (node, transition, shouldAnimate = true) => {
441
+ return new Promise((resolve) => {
442
+ if (shouldAnimate === true) {
443
+ // resolve the promise in the transition end-callback
444
+ // ("extending" end callback when one is already specified)
445
+ let existingEndCallback = transition.end
446
+ transition.end = (...args) => {
447
+ existingEndCallback && existingEndCallback(args)
448
+ // null the callback to enable memory cleanup
449
+ existingEndCallback = null
450
+ resolve()
451
+ }
452
+ node.set(transition.prop, { transition })
453
+ } else {
454
+ node.set(transition.prop, transition.value)
455
+ resolve()
456
+ }
457
+ })
458
+ }
459
+
460
+ export const to = (location, data = {}, options = {}) => {
461
+ navigationData = data
462
+ overrideOptions = options
463
+
464
+ window.location.hash = location
465
+ }
466
+
467
+ export const back = function () {
468
+ const route = history.pop()
469
+ if (route && currentRoute !== route) {
470
+ // set indicator that we are navigating back (to prevent adding page to history stack)
471
+ navigatingBack = true
472
+
473
+ to(route.hash, route.data, route.options)
474
+ return true
475
+ }
476
+
477
+ const backtrack = (currentRoute && currentRoute.options.backtrack) || false
478
+
479
+ // If we deeplink to a page without backtrack
480
+ // we we let the RouterView handle back
481
+ if (backtrack === false) {
482
+ return false
483
+ }
484
+
485
+ const hashEnd = /(\/:?[\w%\s-]+)$/
486
+ let path = currentRoute.path
487
+
488
+ let level = path.split('/').length
489
+
490
+ // On root return
491
+ if (level <= 1) {
492
+ return false
493
+ }
494
+
495
+ while (level--) {
496
+ if (!hashEnd.test(path)) {
497
+ return false
498
+ }
499
+ // Construct new path to backtrack to
500
+ path = path.replace(hashEnd, '')
501
+ const route = matchHash(path, this.parent[symbols.routes])
502
+
503
+ if (route && backtrack) {
504
+ to(route.path)
505
+ return true
506
+ }
507
+ }
508
+
509
+ return false
510
+ }
511
+
512
+ export default {
513
+ navigate,
514
+ to,
515
+ back,
516
+ }