@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 +23 -0
- package/NOTICE +4 -0
- package/index.d.ts +9 -3
- package/package.json +5 -2
- package/src/announcer/announcer.js +1 -0
- package/src/announcer/speechSynthesis.js +21 -4
- package/src/application.js +5 -1
- package/src/component/base/events.js +6 -2
- package/src/component/base/methods.js +0 -2
- package/src/component/setup/state.js +8 -2
- package/src/focus.js +104 -41
- package/src/lib/eventListeners.js +29 -1
- package/src/lib/lifecycle.js +3 -1
- package/src/lib/reactivity/reactive.js +4 -0
- package/src/lib/symbols.js +2 -0
- package/src/plugins/index.js +1 -0
- package/src/plugins/storage/localCookie.js +133 -0
- package/src/plugins/storage/storage.js +49 -0
- package/src/router/router.js +12 -12
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
|
|
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.
|
|
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
|
|
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
|
},
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/src/application.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
@@ -28,8 +28,14 @@ export default (component, state = () => {}) => {
|
|
|
28
28
|
})
|
|
29
29
|
|
|
30
30
|
const stateKeys = Object.keys(state.apply(component) || {})
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
if (component
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
66
|
-
const componentWithInputEvent =
|
|
67
|
-
|
|
68
|
-
if (componentWithInputEvent)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
(typeof
|
|
86
|
-
typeof
|
|
126
|
+
component[symbols.inputEvents] &&
|
|
127
|
+
(typeof component[symbols.inputEvents][key] === 'function' ||
|
|
128
|
+
typeof component[symbols.inputEvents].any === 'function')
|
|
87
129
|
) {
|
|
88
|
-
return
|
|
89
|
-
} else if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
package/src/lib/lifecycle.js
CHANGED
|
@@ -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 &&
|
|
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
|
package/src/lib/symbols.js
CHANGED
|
@@ -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'),
|
package/src/plugins/index.js
CHANGED
|
@@ -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
|
+
}
|
package/src/router/router.js
CHANGED
|
@@ -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 =
|
|
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
|