@lightningjs/blits 1.33.0 → 1.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ _22 Jul 2025_
4
+
5
+ ## v1.35.0
6
+
7
+ - Added `byReference` param to `this.$emit()` to allow passing data without passing it by reference
8
+ - Refactored focus handling
9
+ - more performant
10
+ - more robust for reassigning focus programatically (i.e. without direct key handling)
11
+ - fixed some cases where `$hasFocus` flag wasn't reset upon unfocus
12
+ - add logging of active focus chain upon focus changes
13
+ - Improved overall test coverage to almost 80% (Blits Element, Focus, Theme plugin, Storage plugin)
14
+
15
+ _10 Jul 2025_
16
+
17
+ ## v1.34.0
18
+
19
+ - Added storage plugin
20
+ - Improved test coverage
21
+ - Fixed announcer plugin to work on RDK devices where early GC was happening
22
+ - Added check to throttle only the same input key in a row
23
+ - Updated docs on transitions
24
+ - Added warning when using `hasFocus` in component state
25
+
3
26
  _08 Jul 2025_
4
27
 
5
28
  ## v1.33.0
package/NOTICE CHANGED
@@ -10,3 +10,7 @@ within this component.
10
10
  This repository uses components from the Lightning-SDK. Any components from the Lightning-SDK are:
11
11
  Copyright (c) 2020 Metrological
12
12
  Licensed under the Apache License, Version 2.0
13
+
14
+ This repository uses components from WebPlatformForEmbedded
15
+ Copyright (c) 2019 Metrological
16
+ Licensed under the Apache License, Version 2.0
package/index.d.ts CHANGED
@@ -262,8 +262,12 @@ declare module '@lightningjs/blits' {
262
262
  * Emit events that other components can listen to
263
263
  * @param name - name of the event to be emitted
264
264
  * @param data - optional data to be passed along
265
+ * @param byReference - whether or not to pass the data by reference.
266
+ * The default behaviour is passing the data object by reference (`true`).
267
+ * When explicitely passing `false` the object will be recursively cloned
268
+ * and cleaned from any potential reactivity before emitting
265
269
  */
266
- $emit(name: string, data?: any): void;
270
+ $emit(name: string, data?: any, byReference?: boolean): void;
267
271
 
268
272
  /**
269
273
  * Set a timeout that is automatically cleaned upon component destroy
@@ -970,8 +974,10 @@ declare module '@lightningjs/blits' {
970
974
  /**
971
975
  * Input throttle time in milliseconds to prevent rapid successive inputs
972
976
  *
973
- * Within the throttle window, only one input will be processed immediately Subsequent
974
- * inputs are ignored until the scheduled input is processed.
977
+ * Within the throttle window, only one input will be processed immediately.
978
+ * Subsequent inputs _of the same key_ are ignored until the scheduled input is processed.
979
+ *
980
+ * Pressing a different key will be processed immediately.
975
981
  *
976
982
  * Set to `0` to disable input throttling.
977
983
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "1.33.0",
3
+ "version": "1.35.0",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -13,7 +13,8 @@
13
13
  "./blitsFileConverter": "./src/lib/blitsfileconverter/blitsfileconverter.js"
14
14
  },
15
15
  "scripts": {
16
- "test": "c8 npm run test:run",
16
+ "test": "c8 node scripts/runTests.js",
17
+ "test:ci": "c8 npm run test:run",
17
18
  "test:run": "node -r global-jsdom/register ./node_modules/.bin/tape '{,!(node_modules|packages)/**/}*.test.js' | tap-diff",
18
19
  "lint": "eslint '**/*.js'",
19
20
  "lint:fix": "eslint '**/*.js' --fix",
@@ -38,11 +39,13 @@
38
39
  "eslint": "^8.8.0",
39
40
  "eslint-config-prettier": "^8.3.0",
40
41
  "eslint-plugin-prettier": "^4.0.0",
42
+ "fast-glob": "^3.3.3",
41
43
  "global-jsdom": "24.0.0",
42
44
  "husky": "^9.1.7",
43
45
  "jsdom": "24.0.0",
44
46
  "lint-staged": "^15.5.0",
45
47
  "prettier": "^2.5.1",
48
+ "sinon": "^21.0.0",
46
49
  "tap-diff": "^0.1.1",
47
50
  "tape": "^5.5.0"
48
51
  },
@@ -146,6 +146,7 @@ const stop = () => {
146
146
  }
147
147
 
148
148
  const clear = () => {
149
+ isProcessing = false
149
150
  queue.length = 0
150
151
  }
151
152
 
@@ -21,9 +21,17 @@ const syn = window.speechSynthesis
21
21
 
22
22
  const isAndroid = /android/i.test((window.navigator || {}).userAgent || '')
23
23
 
24
+ const utterances = new Map() // Strong references with unique keys
25
+
24
26
  let initialized = false
25
27
  let infinityTimer = null
26
- const clear = () => infinityTimer && clearTimeout(infinityTimer)
28
+
29
+ const clear = () => {
30
+ if (infinityTimer) {
31
+ clearTimeout(infinityTimer)
32
+ infinityTimer = null
33
+ }
34
+ }
27
35
 
28
36
  const resumeInfinity = (target) => {
29
37
  if (!target || infinityTimer) {
@@ -31,7 +39,9 @@ const resumeInfinity = (target) => {
31
39
  }
32
40
 
33
41
  syn.pause()
34
- syn.resume()
42
+ setTimeout(() => {
43
+ syn.resume()
44
+ }, 0)
35
45
 
36
46
  infinityTimer = setTimeout(() => {
37
47
  resumeInfinity(target)
@@ -47,18 +57,20 @@ const defaultUtteranceProps = {
47
57
  }
48
58
 
49
59
  const initialize = () => {
50
- defaultUtteranceProps.voice = syn.getVoices()[0]
60
+ const voices = syn.getVoices()
61
+ defaultUtteranceProps.voice = voices[0] || null
51
62
  initialized = true
52
63
  }
53
64
 
54
65
  const speak = (options) => {
55
66
  const utterance = new SpeechSynthesisUtterance(options.message)
56
-
67
+ const id = Date.now() + Math.random() // Unique ID for tracking
57
68
  utterance.lang = options.lang || defaultUtteranceProps.lang
58
69
  utterance.pitch = options.pitch || defaultUtteranceProps.pitch
59
70
  utterance.rate = options.rate || defaultUtteranceProps.rate
60
71
  utterance.voice = options.voice || defaultUtteranceProps.voice
61
72
  utterance.volume = options.volume || defaultUtteranceProps.volume
73
+ utterances.set(id, utterance) // Strong reference
62
74
 
63
75
  if (isAndroid === false) {
64
76
  utterance.onstart = () => {
@@ -72,9 +84,14 @@ const speak = (options) => {
72
84
 
73
85
  return new Promise((resolve, reject) => {
74
86
  utterance.onend = () => {
87
+ clear()
88
+ utterances.delete(id) // Cleanup
75
89
  resolve()
76
90
  }
91
+
77
92
  utterance.onerror = (e) => {
93
+ clear()
94
+ utterances.delete(id) // Cleanup
78
95
  reject(e)
79
96
  }
80
97
 
@@ -48,6 +48,7 @@ const Application = (config) => {
48
48
  let keyUpHandler
49
49
  let holdTimeout
50
50
  let lastInputTime = 0
51
+ let lastInputKey = null
51
52
 
52
53
  config.hooks[symbols.destroy] = function () {
53
54
  document.removeEventListener('keydown', keyDownHandler)
@@ -66,8 +67,11 @@ const Application = (config) => {
66
67
  const currentTime = performance.now()
67
68
 
68
69
  const key = keyMap[e.key] || keyMap[e.keyCode] || e.key || e.keyCode
70
+ const sameKey = lastInputKey === key
71
+ lastInputKey = key
69
72
  // execute immediately when no throttle is specified or event is internal (bubbled up by focus manager)
70
- if (throttleMs === 0 || e[symbols.internalEvent] === true) {
73
+ // or key is different from the last used key
74
+ if (throttleMs === 0 || e[symbols.internalEvent] === true || sameKey === false) {
71
75
  return await processInput.call(this, e, key)
72
76
  }
73
77
 
@@ -23,12 +23,16 @@ export default {
23
23
  *
24
24
  * @param {string} event - The name of the event to emit.
25
25
  * @param {any} params - The parameters to pass to the event listeners.
26
+ * @param byReference - whether or not to pass the data by reference.
27
+ * The default behaviour is passing the data object by reference (`true`).
28
+ * When explicitely passing `false` the object will be recursively cloned
29
+ * and cleaned from any potential reactivity before emitting
26
30
  * @returns {boolean} True if all listeners executed, false otherwise.
27
31
  */
28
32
  $emit: {
29
- value: function (event, params) {
33
+ value: function (event, params, byReference = true) {
30
34
  // returning if all listeners executed
31
- return eventListeners.executeListeners(event, params)
35
+ return eventListeners.executeListeners(event, params, byReference)
32
36
  },
33
37
  writable: false,
34
38
  enumerable: true,
@@ -40,8 +40,6 @@ export default {
40
40
  * @this {import('../../component').BlitsComponent}
41
41
  */
42
42
  value: function (e) {
43
- if (this.eol === true) return
44
- this[symbols.state].hasFocus = true
45
43
  Focus.set(this, e)
46
44
  },
47
45
  writable: false,
@@ -28,8 +28,14 @@ export default (component, state = () => {}) => {
28
28
  })
29
29
 
30
30
  const stateKeys = Object.keys(state.apply(component) || {})
31
- // add built-in hasFocus key
32
- stateKeys.push(['hasFocus'])
31
+ if (stateKeys.indexOf('hasFocus') > -1) {
32
+ Log.warn(
33
+ 'State `hasFocus` already exists as a built-in Component variable (to indicate whether the component currently has focus). Avoid using your own `hasFocus` key in the Component state'
34
+ )
35
+ } else {
36
+ // add built-in hasFocus key
37
+ stateKeys.push(['hasFocus'])
38
+ }
33
39
  const stateKeysLength = stateKeys.length
34
40
 
35
41
  for (let i = 0; i < stateKeysLength; i++) {
package/src/focus.js CHANGED
@@ -19,6 +19,7 @@ import symbols from './lib/symbols.js'
19
19
  import { state } from './router/router.js'
20
20
  import Settings from './settings.js'
21
21
  import { DEFAULT_HOLD_TIMEOUT_MS } from './constants.js'
22
+ import { Log } from './lib/log.js'
22
23
 
23
24
  let focusedComponent = null
24
25
  let focusChain = []
@@ -38,56 +39,118 @@ export default {
38
39
  return focusedComponent
39
40
  },
40
41
  set(component, event) {
41
- if (component === focusedComponent) return
42
42
  clearTimeout(setFocusTimeout)
43
- focusedComponent && focusedComponent !== component.parent && focusedComponent.unfocus()
44
- focusChain.reverse().forEach((cmp) => cmp.unfocus())
45
- if (component !== focusedComponent) {
46
- setFocusTimeout = setTimeout(
47
- () => {
48
- focusedComponent = component
49
- focusedComponent.lifecycle.state = 'focus'
50
- if (event instanceof KeyboardEvent) {
51
- const internalEvent = new KeyboardEvent('keydown', event)
52
- // @ts-ignore - this is an internal event
53
- internalEvent[symbols.internalEvent] = true
54
- document.dispatchEvent(internalEvent)
55
- } else {
56
- focusChain = []
57
- }
58
- },
59
- this.hold ? Settings.get('holdTimeout', DEFAULT_HOLD_TIMEOUT_MS) : 0
60
- )
43
+
44
+ // early return if already focused
45
+ if (component === focusedComponent) return
46
+
47
+ if (focusChain.length === 0) {
48
+ focusChain = getAncestors([component])
49
+ }
50
+
51
+ // unfocus currently focused components
52
+ if (focusedComponent !== null) {
53
+ if (focusChain[focusChain.length - 1] === component.parent) {
54
+ focusChain.push(component)
55
+ } else {
56
+ const newFocusChain = getAncestors([component])
57
+ let i = focusChain.length
58
+ while (i--) {
59
+ // don't unfocus when part of the new focus chain
60
+ if (newFocusChain.indexOf(focusChain[i]) > -1) break
61
+ focusChain[i].lifecycle.state = 'unfocus'
62
+ }
63
+ focusChain = newFocusChain
64
+ }
65
+ }
66
+
67
+ // ensure that all components in the focus path have focus state
68
+ let i = 0
69
+ while (i < focusChain.length - 1) {
70
+ focusChain[i].lifecycle.state = 'focus'
71
+ i++
72
+ }
73
+
74
+ // and finally set focus to the leaf component
75
+ if (this.hold === true) {
76
+ setFocusTimeout = setTimeout(() => {
77
+ setFocus(component, event)
78
+ }, Settings.get('holdTimeout', DEFAULT_HOLD_TIMEOUT_MS))
79
+ } else {
80
+ setFocus(component, event)
61
81
  }
62
82
  },
63
83
  input(key, event) {
64
84
  if (state.navigating === true) return
65
- focusChain = walkChain([focusedComponent], key)
66
- const componentWithInputEvent = focusChain.shift()
67
-
68
- if (componentWithInputEvent) {
69
- let cb
70
- if (componentWithInputEvent[symbols.inputEvents][key]) {
71
- cb = componentWithInputEvent[symbols.inputEvents][key].call(componentWithInputEvent, event)
72
- } else if (componentWithInputEvent[symbols.inputEvents].any) {
73
- cb = componentWithInputEvent[symbols.inputEvents].any.call(componentWithInputEvent, event)
74
- }
75
- if (cb !== undefined) {
76
- keyUpCallbacks.set(event.code, cb)
77
- }
85
+
86
+ const componentWithInputEvent = getComponentWithInputEvent(focusedComponent, key)
87
+
88
+ if (componentWithInputEvent === null) return
89
+
90
+ const inputEvents = componentWithInputEvent[symbols.inputEvents]
91
+
92
+ let cb
93
+ if (inputEvents[key]) {
94
+ cb = inputEvents[key].call(componentWithInputEvent, event)
95
+ } else if (inputEvents.any) {
96
+ cb = inputEvents.any.call(componentWithInputEvent, event)
97
+ }
98
+
99
+ if (cb !== undefined) {
100
+ keyUpCallbacks.set(event.code, cb)
78
101
  }
79
102
  },
80
103
  }
81
104
 
82
- const walkChain = (components, key) => {
105
+ /**
106
+ * Recursive function that retrieves the ancestors of a component
107
+ * @param {Array} components
108
+ * @returns array components
109
+ */
110
+ const getAncestors = (components) => {
111
+ if (components[0].parent !== undefined) {
112
+ components.unshift(components[0].parent)
113
+ return getAncestors(components)
114
+ }
115
+ return components
116
+ }
117
+
118
+ /**
119
+ * Get the component in the ancestors chain that has a handler for a certain key code
120
+ * @param {Object} component
121
+ * @param {String} key
122
+ * @returns component
123
+ */
124
+ const getComponentWithInputEvent = (component, key) => {
83
125
  if (
84
- components[0][symbols.inputEvents] &&
85
- (typeof components[0][symbols.inputEvents][key] === 'function' ||
86
- typeof components[0][symbols.inputEvents].any === 'function')
126
+ component[symbols.inputEvents] &&
127
+ (typeof component[symbols.inputEvents][key] === 'function' ||
128
+ typeof component[symbols.inputEvents].any === 'function')
87
129
  ) {
88
- return components
89
- } else if (components[0].parent) {
90
- components.unshift(components[0].parent)
91
- return walkChain(components, key)
92
- } else return []
130
+ return component
131
+ } else if (component.parent !== undefined) {
132
+ return getComponentWithInputEvent(component.parent, key)
133
+ } else return null
134
+ }
135
+
136
+ /**
137
+ * Set the focus to the Component
138
+ * @param {Object} component - The component fo focus
139
+ * @param {KeyboardEvent} event - Keyboard event
140
+ */
141
+ const setFocus = (component, event) => {
142
+ Log.info(
143
+ '\nFocus chain:\n',
144
+ focusChain.map((c, index) => '\t'.repeat(index) + '↳ ' + c.componentId).join('\n')
145
+ )
146
+
147
+ focusedComponent = component
148
+ component.lifecycle.state = 'focus'
149
+
150
+ if (event instanceof KeyboardEvent) {
151
+ const internalEvent = new KeyboardEvent('keydown', event)
152
+ // @ts-ignore - this is an internal event
153
+ internalEvent[symbols.internalEvent] = true
154
+ document.dispatchEvent(internalEvent)
155
+ }
93
156
  }
@@ -15,9 +15,30 @@
15
15
  * SPDX-License-Identifier: Apache-2.0
16
16
  */
17
17
 
18
+ import symbols from './symbols.js'
19
+ import { getRaw } from './reactivity/reactive.js'
20
+
18
21
  const eventsMap = new Map()
19
22
  const callbackCache = new Map()
20
23
 
24
+ function isProxy(obj) {
25
+ return obj && typeof obj === 'object' && obj[symbols.isProxy] === true
26
+ }
27
+
28
+ function deepUnproxyClone(obj) {
29
+ if (obj === null || typeof obj !== 'object') return obj
30
+
31
+ const raw = isProxy(obj) ? getRaw(obj) : obj
32
+ const result = Array.isArray(raw) ? [] : {}
33
+
34
+ for (const key of Object.keys(raw)) {
35
+ const value = raw[key]
36
+ result[key] = value && typeof value === 'object' ? deepUnproxyClone(value) : value
37
+ }
38
+
39
+ return result
40
+ }
41
+
21
42
  export default {
22
43
  /**
23
44
  * Registers a listener callback for a specific event on a component.
@@ -70,9 +91,16 @@ export default {
70
91
  *
71
92
  * @param {string} event - The name of the event to emit.
72
93
  * @param {any} params - The parameters to pass to the event listeners.
94
+ * @param byReference - whether or not to pass the data by reference.
95
+ * The default behaviour is passing the data object by reference (`true`).
96
+ * When explicitely passing `false` the object will be recursively cloned
97
+ * and cleaned from any potential reactivity before emitting
73
98
  * @returns {boolean} True if all listeners executed without stopping propagation, false if any listener returned false.
74
99
  */
75
- executeListeners(event, params) {
100
+ executeListeners(event, params, byReference) {
101
+ if (byReference === false) {
102
+ params = deepUnproxyClone(params)
103
+ }
76
104
  const componentsMap = eventsMap.get(event)
77
105
  if (componentsMap === undefined || componentsMap.size === 0) {
78
106
  return true // No listeners, so execution can be considered successful
@@ -70,12 +70,14 @@ export default {
70
70
  * @param {string} v - The new lifecycle state.
71
71
  */
72
72
  set state(v) {
73
- if (states.indexOf(v) > -1 && (v !== this.current || v === 'focus')) {
73
+ if (states.indexOf(v) > -1 && v !== this.current) {
74
74
  Log.debug(
75
75
  `Setting lifecycle state from ${this.current} to ${v} for ${this.component.componentId}`
76
76
  )
77
77
  this.previous = this.current
78
78
  this.current = v
79
+ if (this.current === 'focus') this.component[symbols.state].hasFocus = true
80
+ if (this.current === 'unfocus') this.component[symbols.state].hasFocus = false
79
81
  // emit 'private' hook
80
82
  privateEmit(v, this.component[symbols.identifier], this.component)
81
83
  // emit 'public' hook
@@ -46,6 +46,10 @@ const reactiveProxy = (original, _parent = null, _key, global) => {
46
46
 
47
47
  const handler = {
48
48
  get(target, key, receiver) {
49
+ // return the original object instead of the proxied
50
+ if (key === symbols.isProxy) {
51
+ return true
52
+ }
49
53
  // return the original object instead of the proxied
50
54
  if (key === symbols.raw) {
51
55
  return original
@@ -16,6 +16,7 @@
16
16
  * @property {symbol} inputEvents
17
17
  * @property {symbol} internalEvent
18
18
  * @property {symbol} intervals
19
+ * @property {symbol} isProxy
19
20
  * @property {symbol} launched
20
21
  * @property {symbol} level
21
22
  * @property {symbol} methodKeys
@@ -67,6 +68,7 @@ export default {
67
68
  inputEvents: Symbol('inputEvents'),
68
69
  internalEvent: Symbol('internalEvent'),
69
70
  intervals: Symbol('intervals'),
71
+ isProxy: Symbol('isProxy'),
70
72
  launched: Symbol('launched'),
71
73
  level: Symbol('level'),
72
74
  methodKeys: Symbol('methodKeys'),
@@ -18,3 +18,4 @@
18
18
  export { default as language } from './language.js'
19
19
  export { default as theme } from './theme.js'
20
20
  export { default as appState } from './appstate.js'
21
+ export { default as storage } from './storage/storage.js'
@@ -0,0 +1,133 @@
1
+ /*
2
+ * If not stated otherwise in this file or this component's LICENSE file the
3
+ * following copyright and licenses apply:
4
+ *
5
+ * Copyright 2019 Metrological
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the License);
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+
20
+ // LocalStorage and Cookie storage library
21
+ class localCookie {
22
+ constructor(options) {
23
+ options = options || {}
24
+ this.forceCookies = options.forceCookies || false
25
+
26
+ if (this._checkIfLocalStorageWorks() === true && options.forceCookies !== true)
27
+ return {
28
+ getItem: this._getItemLocalStorage,
29
+ setItem: this._setItemLocalStorage,
30
+ removeItem: this._removeItemLocalStorage,
31
+ clear: this._clearLocalStorage,
32
+ keys: this._getLocalStorageKeys,
33
+ }
34
+ else
35
+ return {
36
+ getItem: this._getItemCookie,
37
+ setItem: this._setItemCookie,
38
+ removeItem: this._removeItemCookie,
39
+ clear: this._clearCookies,
40
+ keys: this._getCookieKeys,
41
+ }
42
+ }
43
+
44
+ // Checks if local storage is available and works
45
+ _checkIfLocalStorageWorks() {
46
+ if (typeof localStorage !== 'undefined') {
47
+ try {
48
+ localStorage.setItem('feature_test', 'yes')
49
+ if (localStorage.getItem('feature_test') === 'yes') {
50
+ localStorage.removeItem('feature_test')
51
+ // localStorage is enabled
52
+ return true
53
+ } else {
54
+ // localStorage is disabled
55
+ return false
56
+ }
57
+ } catch (e) {
58
+ // localStorage is disabled
59
+ return false
60
+ }
61
+ } else {
62
+ // localStorage is not available
63
+ return false
64
+ }
65
+ }
66
+
67
+ /*
68
+ * Local Storage APIs
69
+ */
70
+ _getItemLocalStorage(key) {
71
+ return window.localStorage.getItem(key)
72
+ }
73
+
74
+ _setItemLocalStorage(key, value) {
75
+ return window.localStorage.setItem(key, value)
76
+ }
77
+
78
+ _removeItemLocalStorage(key) {
79
+ return window.localStorage.removeItem(key)
80
+ }
81
+
82
+ _clearLocalStorage() {
83
+ return window.localStorage.clear()
84
+ }
85
+
86
+ _getLocalStorageKeys() {
87
+ return Object.keys(window.localStorage)
88
+ }
89
+
90
+ /*
91
+ * Cookie APIs
92
+ */
93
+ _getItemCookie(key) {
94
+ /* eslint-disable */
95
+ function escape(s) { return s.replace(/([.*+?\^${}()|\[\]\/\\])/g, '\\$1'); };
96
+ var match = document.cookie.match(RegExp('(?:^|;\\s*)' + escape(key) + '=([^;]*)'));
97
+
98
+ //if value in key of the entire cookie is empty, its deleted
99
+ if (match && match[1] === '')
100
+ match[1] = null
101
+
102
+ return match ? match[1] : null;
103
+ }
104
+
105
+ _setItemCookie(key, value) {
106
+ var now = new Date()
107
+ var expires = new Date(now.getTime() +1000*60*60*24*365*5) // 5 years!
108
+ document.cookie = `${key}=${value}; expires=${expires.toUTCString()};`
109
+ return //return undefined just like localStorage
110
+ }
111
+
112
+ _removeItemCookie(key) {
113
+ // to delete an k/v set the key to empty
114
+ document.cookie = `${key}=;Max-Age=-99999999;`
115
+ return //return undefined just like localStorage
116
+ }
117
+
118
+ _clearCookies() {
119
+ // empty all key/values in cookies
120
+ // What about cookies we got from the server? ¯\_(ツ)_/¯
121
+ // They should have the HttpOnly flag, so we shouldn't be able to delete them
122
+ document.cookie.split(";").forEach( (c) => {
123
+ document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=;Max-Age=-99999999");
124
+ });
125
+
126
+ return //return undefined
127
+ }
128
+ _getCookieKeys() {
129
+ return document.cookie.split(";").map((item)=>item.trim().split("=")[0])
130
+ }
131
+ }
132
+
133
+ export default localCookie
@@ -0,0 +1,49 @@
1
+ /*
2
+ * Copyright 2024 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 localCookie from './localCookie.js'
19
+
20
+ let lc = new localCookie()
21
+
22
+ export default {
23
+ name: 'storage',
24
+ plugin() {
25
+ return {
26
+ get(key) {
27
+ try {
28
+ return JSON.parse(lc.getItem(key))
29
+ } catch (e) {
30
+ return null
31
+ }
32
+ },
33
+ set(key, value) {
34
+ try {
35
+ lc.setItem(key, JSON.stringify(value))
36
+ return true
37
+ } catch (e) {
38
+ return false
39
+ }
40
+ },
41
+ remove(key) {
42
+ lc.removeItem(key)
43
+ },
44
+ clear() {
45
+ lc.clear()
46
+ },
47
+ }
48
+ },
49
+ }
@@ -132,7 +132,7 @@ export const matchHash = (path, routes = []) => {
132
132
  let matchingRoute = false
133
133
  let i = 0
134
134
  while (!matchingRoute && i < routes.length) {
135
- const route = { ...routes[i] }
135
+ const route = routes[i]
136
136
  route.path = normalizePath(route.path)
137
137
  if (route.path === path) {
138
138
  route.params = {}
@@ -206,20 +206,20 @@ export const navigate = async function () {
206
206
  let previousRoute = currentRoute //? Object.assign({}, currentRoute) : undefined
207
207
  const { hash, path, queryParams } = getHash()
208
208
  let route = matchHash(path, this.parent[symbols.routes])
209
- const queryParamsData = {}
210
- const queryParamsEntries = [...queryParams.entries()]
211
- for (let i = 0; i < queryParamsEntries.length; i++) {
212
- queryParamsData[queryParamsEntries[i][0]] = queryParamsEntries[i][1]
213
- }
214
-
215
- route.data = {
216
- ...route.data,
217
- ...navigationData,
218
- ...queryParamsData,
219
- }
220
209
 
221
210
  currentRoute = route
222
211
  if (route) {
212
+ const queryParamsData = {}
213
+ const queryParamsEntries = [...queryParams.entries()]
214
+ for (let i = 0; i < queryParamsEntries.length; i++) {
215
+ queryParamsData[queryParamsEntries[i][0]] = queryParamsEntries[i][1]
216
+ }
217
+
218
+ route.data = {
219
+ ...navigationData,
220
+ ...route.data,
221
+ ...queryParamsData,
222
+ }
223
223
  // Adding the location hash to the route if it exists.
224
224
  if (hash !== null) {
225
225
  route.hash = hash