@lightningjs/blits 1.34.0 → 1.35.1
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 +18 -0
- package/index.d.ts +5 -1
- package/package.json +2 -1
- package/src/component/base/events.js +6 -2
- package/src/component/base/methods.js +0 -2
- package/src/focus.js +101 -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/storage/localCookie.js +3 -3
- package/src/router/router.js +12 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.35.1
|
|
4
|
+
|
|
5
|
+
_24 Jul 2025_
|
|
6
|
+
|
|
7
|
+
- (Re) added next tick when setting focus to prevent any potential race conditions
|
|
8
|
+
|
|
9
|
+
_22 Jul 2025_
|
|
10
|
+
|
|
11
|
+
## v1.35.0
|
|
12
|
+
|
|
13
|
+
- Added `byReference` param to `this.$emit()` to allow passing data without passing it by reference
|
|
14
|
+
- Refactored focus handling
|
|
15
|
+
- more performant
|
|
16
|
+
- more robust for reassigning focus programatically (i.e. without direct key handling)
|
|
17
|
+
- fixed some cases where `$hasFocus` flag wasn't reset upon unfocus
|
|
18
|
+
- add logging of active focus chain upon focus changes
|
|
19
|
+
- Improved overall test coverage to almost 80% (Blits Element, Focus, Theme plugin, Storage plugin)
|
|
20
|
+
|
|
3
21
|
_10 Jul 2025_
|
|
4
22
|
|
|
5
23
|
## v1.34.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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightningjs/blits",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.35.1",
|
|
4
4
|
"description": "Blits: The Lightning 3 App Development Framework",
|
|
5
5
|
"bin": "bin/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"jsdom": "24.0.0",
|
|
46
46
|
"lint-staged": "^15.5.0",
|
|
47
47
|
"prettier": "^2.5.1",
|
|
48
|
+
"sinon": "^21.0.0",
|
|
48
49
|
"tap-diff": "^0.1.1",
|
|
49
50
|
"tape": "^5.5.0"
|
|
50
51
|
},
|
|
@@ -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,
|
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,115 @@ 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
|
-
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])
|
|
61
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
|
+
setFocusTimeout = setTimeout(
|
|
76
|
+
() => setFocus(component, event),
|
|
77
|
+
this.hold === true ? Settings.get('holdTimeout', DEFAULT_HOLD_TIMEOUT_MS) : 0
|
|
78
|
+
)
|
|
62
79
|
},
|
|
63
80
|
input(key, event) {
|
|
64
81
|
if (state.navigating === true) return
|
|
65
|
-
|
|
66
|
-
const componentWithInputEvent =
|
|
67
|
-
|
|
68
|
-
if (componentWithInputEvent)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
|
|
83
|
+
const componentWithInputEvent = getComponentWithInputEvent(focusedComponent, key)
|
|
84
|
+
|
|
85
|
+
if (componentWithInputEvent === null) return
|
|
86
|
+
|
|
87
|
+
const inputEvents = componentWithInputEvent[symbols.inputEvents]
|
|
88
|
+
|
|
89
|
+
let cb
|
|
90
|
+
if (inputEvents[key]) {
|
|
91
|
+
cb = inputEvents[key].call(componentWithInputEvent, event)
|
|
92
|
+
} else if (inputEvents.any) {
|
|
93
|
+
cb = inputEvents.any.call(componentWithInputEvent, event)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (cb !== undefined) {
|
|
97
|
+
keyUpCallbacks.set(event.code, cb)
|
|
78
98
|
}
|
|
79
99
|
},
|
|
80
100
|
}
|
|
81
101
|
|
|
82
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Recursive function that retrieves the ancestors of a component
|
|
104
|
+
* @param {Array} components
|
|
105
|
+
* @returns array components
|
|
106
|
+
*/
|
|
107
|
+
const getAncestors = (components) => {
|
|
108
|
+
if (components[0].parent !== undefined) {
|
|
109
|
+
components.unshift(components[0].parent)
|
|
110
|
+
return getAncestors(components)
|
|
111
|
+
}
|
|
112
|
+
return components
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get the component in the ancestors chain that has a handler for a certain key code
|
|
117
|
+
* @param {Object} component
|
|
118
|
+
* @param {String} key
|
|
119
|
+
* @returns component
|
|
120
|
+
*/
|
|
121
|
+
const getComponentWithInputEvent = (component, key) => {
|
|
83
122
|
if (
|
|
84
|
-
|
|
85
|
-
(typeof
|
|
86
|
-
typeof
|
|
123
|
+
component[symbols.inputEvents] &&
|
|
124
|
+
(typeof component[symbols.inputEvents][key] === 'function' ||
|
|
125
|
+
typeof component[symbols.inputEvents].any === 'function')
|
|
87
126
|
) {
|
|
88
|
-
return
|
|
89
|
-
} else if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
127
|
+
return component
|
|
128
|
+
} else if (component.parent !== undefined) {
|
|
129
|
+
return getComponentWithInputEvent(component.parent, key)
|
|
130
|
+
} else return null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Set the focus to the Component
|
|
135
|
+
* @param {Object} component - The component fo focus
|
|
136
|
+
* @param {KeyboardEvent} event - Keyboard event
|
|
137
|
+
*/
|
|
138
|
+
const setFocus = (component, event) => {
|
|
139
|
+
Log.info(
|
|
140
|
+
'\nFocus chain:\n',
|
|
141
|
+
focusChain.map((c, index) => '\t'.repeat(index) + '↳ ' + c.componentId).join('\n')
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
focusedComponent = component
|
|
145
|
+
component.lifecycle.state = 'focus'
|
|
146
|
+
|
|
147
|
+
if (event instanceof KeyboardEvent) {
|
|
148
|
+
const internalEvent = new KeyboardEvent('keydown', event)
|
|
149
|
+
// @ts-ignore - this is an internal event
|
|
150
|
+
internalEvent[symbols.internalEvent] = true
|
|
151
|
+
document.dispatchEvent(internalEvent)
|
|
152
|
+
}
|
|
93
153
|
}
|
|
@@ -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'),
|
|
@@ -120,14 +120,14 @@ class localCookie {
|
|
|
120
120
|
// What about cookies we got from the server? ¯\_(ツ)_/¯
|
|
121
121
|
// They should have the HttpOnly flag, so we shouldn't be able to delete them
|
|
122
122
|
document.cookie.split(";").forEach( (c) => {
|
|
123
|
-
document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires
|
|
123
|
+
document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=;Max-Age=-99999999");
|
|
124
124
|
});
|
|
125
125
|
|
|
126
126
|
return //return undefined
|
|
127
127
|
}
|
|
128
128
|
_getCookieKeys() {
|
|
129
|
-
return document.cookie.split(";").map((item)=>item.split("=")[0])
|
|
129
|
+
return document.cookie.split(";").map((item)=>item.trim().split("=")[0])
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
export default localCookie
|
|
133
|
+
export default localCookie
|
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
|