@lightningjs/blits 1.49.0 → 1.51.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 +18 -2
- package/index.d.ts +19 -0
- package/package.json +3 -2
- package/src/announcer/announcer.js +5 -3
- package/src/application.js +98 -3
- package/src/component/base/methods.js +1 -2
- package/src/component/setup/state.js +2 -0
- package/src/component.js +35 -28
- package/src/components/FPScounter.js +1406 -1391
- package/src/components/RouterView.js +1 -1
- package/src/components/Sprite.js +35 -2
- package/src/components/index.js +0 -2
- package/src/engines/L3/element.js +9 -0
- package/src/engines/L3/launch.js +3 -1
- package/src/{focus.js → focus/focus.js} +6 -18
- package/src/focus/helpers.js +29 -0
- package/src/focus/hover.js +93 -0
- package/src/launch.js +1 -0
- package/src/lib/codegenerator/generator.js +4 -0
- package/src/lib/lifecycle.js +7 -0
- package/src/router/router.js +29 -6
- package/vscode/data/template-attributes.json +335 -86
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import Component from '../component.js'
|
|
19
19
|
import Router, { state as routerState } from '../router/router.js'
|
|
20
20
|
import symbols from '../lib/symbols.js'
|
|
21
|
-
import Focus from '../focus.js'
|
|
21
|
+
import Focus from '../focus/focus.js'
|
|
22
22
|
|
|
23
23
|
let hashchangeHandler = null
|
|
24
24
|
|
package/src/components/Sprite.js
CHANGED
|
@@ -85,8 +85,7 @@ export default () =>
|
|
|
85
85
|
],
|
|
86
86
|
context: {},
|
|
87
87
|
},
|
|
88
|
-
props: ['image', 'map', 'frame', 'color', 'effects'],
|
|
89
|
-
|
|
88
|
+
props: ['image', 'map', 'frame', 'color', 'effects', '@loaded', '@error'],
|
|
90
89
|
state() {
|
|
91
90
|
return {
|
|
92
91
|
spriteTexture: null,
|
|
@@ -94,6 +93,33 @@ export default () =>
|
|
|
94
93
|
}
|
|
95
94
|
},
|
|
96
95
|
|
|
96
|
+
hooks: {
|
|
97
|
+
ready() {
|
|
98
|
+
const loaded = this['@loaded']
|
|
99
|
+
if (loaded && typeof loaded === 'function') {
|
|
100
|
+
const cb = (payload) => loaded({ w: payload?.w, h: payload?.h }, this[symbols.wrapper])
|
|
101
|
+
this._loadedCb = cb
|
|
102
|
+
if (this.spriteTexture !== undefined && this.spriteTexture !== null) {
|
|
103
|
+
this.spriteTexture.on('loaded', cb)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const error = this['@error']
|
|
107
|
+
if (error && typeof error === 'function') {
|
|
108
|
+
const cb = (payload) => error(payload, this[symbols.wrapper])
|
|
109
|
+
this._failedCb = cb
|
|
110
|
+
if (this.spriteTexture !== undefined && this.spriteTexture !== null) {
|
|
111
|
+
this.spriteTexture.on('failed', cb)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
destroy() {
|
|
116
|
+
if (this.spriteTexture !== undefined && this.spriteTexture !== null) {
|
|
117
|
+
if (this._loadedCb) this.spriteTexture.off('loaded', this._loadedCb)
|
|
118
|
+
if (this._failedCb) this.spriteTexture.off('failed', this._failedCb)
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
|
|
97
123
|
computed: {
|
|
98
124
|
texture() {
|
|
99
125
|
// If no image - nothing to render
|
|
@@ -113,10 +139,17 @@ export default () =>
|
|
|
113
139
|
this.spriteTexture === null ||
|
|
114
140
|
this.currentSrc !== this.image
|
|
115
141
|
) {
|
|
142
|
+
const prevTexture = this.spriteTexture
|
|
143
|
+
if (prevTexture !== undefined && prevTexture !== null) {
|
|
144
|
+
if (this._loadedCb) prevTexture.off('loaded', this._loadedCb)
|
|
145
|
+
if (this._failedCb) prevTexture.off('failed', this._failedCb)
|
|
146
|
+
}
|
|
116
147
|
this.spriteTexture = renderer.createTexture('ImageTexture', {
|
|
117
148
|
src: this.image,
|
|
118
149
|
})
|
|
119
150
|
this.currentSrc = this.image
|
|
151
|
+
if (this._loadedCb) this.spriteTexture.on('loaded', this._loadedCb)
|
|
152
|
+
if (this._failedCb) this.spriteTexture.on('failed', this._failedCb)
|
|
120
153
|
}
|
|
121
154
|
|
|
122
155
|
// Resolve frame data from sprite map
|
package/src/components/index.js
CHANGED
|
@@ -18,11 +18,9 @@
|
|
|
18
18
|
import Circle from './Circle.js'
|
|
19
19
|
import RouterView from './RouterView.js'
|
|
20
20
|
import Sprite from './Sprite.js'
|
|
21
|
-
import FPScounter from './FPScounter.js'
|
|
22
21
|
|
|
23
22
|
export default () => ({
|
|
24
23
|
Circle: Circle(),
|
|
25
24
|
RouterView: RouterView(),
|
|
26
25
|
Sprite: Sprite(),
|
|
27
|
-
FPScounter: FPScounter(),
|
|
28
26
|
})
|
|
@@ -22,6 +22,8 @@ import { Log } from '../../lib/log.js'
|
|
|
22
22
|
import symbols from '../../lib/symbols.js'
|
|
23
23
|
import Settings from '../../settings.js'
|
|
24
24
|
|
|
25
|
+
const holderComponentMap = new WeakMap()
|
|
26
|
+
|
|
25
27
|
/**
|
|
26
28
|
* Creates a padding object from a value and direction.
|
|
27
29
|
* @param {number|object|string|undefined} padding - The padding value.
|
|
@@ -512,6 +514,9 @@ const propsTransformer = {
|
|
|
512
514
|
this.props['data'] = v
|
|
513
515
|
}
|
|
514
516
|
},
|
|
517
|
+
set holder(v) {
|
|
518
|
+
this.props['interactive'] = v
|
|
519
|
+
},
|
|
515
520
|
}
|
|
516
521
|
|
|
517
522
|
const Element = {
|
|
@@ -555,6 +560,10 @@ const Element = {
|
|
|
555
560
|
? renderer.createTextNode({ ...textDefaults, ...this.props.props })
|
|
556
561
|
: renderer.createNode(this.props.props)
|
|
557
562
|
|
|
563
|
+
if (this.props['holder'] === true) {
|
|
564
|
+
holderComponentMap.set(this.node, this.component)
|
|
565
|
+
}
|
|
566
|
+
|
|
558
567
|
if (props['@loaded'] !== undefined && typeof props['@loaded'] === 'function') {
|
|
559
568
|
this.node.on('loaded', (el, { type, dimensions }) => {
|
|
560
569
|
props['@loaded']({ w: dimensions.width, h: dimensions.height, type }, this)
|
package/src/engines/L3/launch.js
CHANGED
|
@@ -118,7 +118,9 @@ export default (App, target, settings = {}) => {
|
|
|
118
118
|
|
|
119
119
|
shaderLoader()
|
|
120
120
|
fontLoader()
|
|
121
|
-
|
|
121
|
+
// Defer app initialization until after the current call stack is cleared,
|
|
122
|
+
// allowing the renderer to finish setting up before the app starts creating components
|
|
123
|
+
Promise.resolve().then(initApp)
|
|
122
124
|
|
|
123
125
|
return renderer
|
|
124
126
|
}
|
|
@@ -15,11 +15,12 @@
|
|
|
15
15
|
* SPDX-License-Identifier: Apache-2.0
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import symbols from '
|
|
19
|
-
import { state } from '
|
|
20
|
-
import Settings from '
|
|
21
|
-
import { DEFAULT_HOLD_TIMEOUT_MS } from '
|
|
22
|
-
import { Log } from '
|
|
18
|
+
import symbols from '../lib/symbols.js'
|
|
19
|
+
import { state } from '../router/router.js'
|
|
20
|
+
import Settings from '../settings.js'
|
|
21
|
+
import { DEFAULT_HOLD_TIMEOUT_MS } from '../constants.js'
|
|
22
|
+
import { Log } from '../lib/log.js'
|
|
23
|
+
import { getAncestors } from './helpers.js'
|
|
23
24
|
|
|
24
25
|
let focusedComponent = null
|
|
25
26
|
let focusChain = []
|
|
@@ -99,19 +100,6 @@ export default {
|
|
|
99
100
|
},
|
|
100
101
|
}
|
|
101
102
|
|
|
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
103
|
/**
|
|
116
104
|
* Get the component in the ancestors chain that has a handler for a certain key code
|
|
117
105
|
* @param {Object} component
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 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
|
+
/**
|
|
19
|
+
* Recursive function that retrieves the ancestors of a component
|
|
20
|
+
* @param {Array} components
|
|
21
|
+
* @returns array components
|
|
22
|
+
*/
|
|
23
|
+
export const getAncestors = (components) => {
|
|
24
|
+
if (components[0].parent !== undefined) {
|
|
25
|
+
components.unshift(components[0].parent)
|
|
26
|
+
return getAncestors(components)
|
|
27
|
+
}
|
|
28
|
+
return components
|
|
29
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 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 { Log } from '../lib/log.js'
|
|
19
|
+
import { getAncestors } from './helpers.js'
|
|
20
|
+
|
|
21
|
+
let hoveredComponent = null
|
|
22
|
+
let hoverChain = []
|
|
23
|
+
|
|
24
|
+
export default {
|
|
25
|
+
get() {
|
|
26
|
+
return hoveredComponent
|
|
27
|
+
},
|
|
28
|
+
set(component) {
|
|
29
|
+
if (component === undefined || component.eol === true) return
|
|
30
|
+
|
|
31
|
+
// early return if already hovered
|
|
32
|
+
if (component === hoveredComponent) return
|
|
33
|
+
|
|
34
|
+
if (hoveredComponent === null) {
|
|
35
|
+
hoverChain = getAncestors([component])
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// unhover currently hovered components
|
|
39
|
+
if (hoveredComponent !== null) {
|
|
40
|
+
if (hoverChain[hoverChain.length - 1] === component.parent) {
|
|
41
|
+
hoverChain.push(component)
|
|
42
|
+
} else {
|
|
43
|
+
const newhoverChain = getAncestors([component])
|
|
44
|
+
let i = hoverChain.length
|
|
45
|
+
while (i--) {
|
|
46
|
+
// don't unhover when part of the new hover chain
|
|
47
|
+
if (newhoverChain.indexOf(hoverChain[i]) > -1) break
|
|
48
|
+
// skip if component is destroyed
|
|
49
|
+
if (hoverChain[i].eol !== true) {
|
|
50
|
+
hoverChain[i].lifecycle.state = 'unhover'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
hoverChain = newhoverChain
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ensure that all components in the hover path have hover state
|
|
58
|
+
let i = 0
|
|
59
|
+
while (i < hoverChain.length - 1) {
|
|
60
|
+
// skip if component is destroyed
|
|
61
|
+
if (hoverChain[i].eol !== true) {
|
|
62
|
+
hoverChain[i].lifecycle.state = 'hover'
|
|
63
|
+
}
|
|
64
|
+
i++
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setHover(component)
|
|
68
|
+
},
|
|
69
|
+
clear() {
|
|
70
|
+
if (hoveredComponent === null) return
|
|
71
|
+
for (let i = 0; i < hoverChain.length; i++) {
|
|
72
|
+
if (hoverChain[i].eol !== true) {
|
|
73
|
+
hoverChain[i].lifecycle.state = 'unhover'
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
hoveredComponent = null
|
|
77
|
+
hoverChain = []
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Set the hover to the Component
|
|
83
|
+
* @param {Object} component - The component to hover
|
|
84
|
+
*/
|
|
85
|
+
const setHover = (component) => {
|
|
86
|
+
Log.info(
|
|
87
|
+
'\nHover chain:\n',
|
|
88
|
+
hoverChain.map((c, index) => '\t'.repeat(index) + '↳ ' + c.componentId).join('\n')
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
hoveredComponent = component
|
|
92
|
+
component.lifecycle.state = 'hover'
|
|
93
|
+
}
|
package/src/launch.js
CHANGED
|
@@ -89,6 +89,7 @@ async function rendererVersion() {
|
|
|
89
89
|
* @property {number} [textureProcessingTimeLimit] - Max time renderer can process textures in a frame
|
|
90
90
|
* @property {Partial<import('@lightningjs/renderer').RendererMainSettings>} [advanced] - Advanced renderer settings
|
|
91
91
|
* @property {boolean} [announcer] - Whether or not the announcer should be activated on initialization
|
|
92
|
+
* @property {boolean} [enableMouse] - Enable mouse support (hover and click-to-focus). Defaults to false.
|
|
92
93
|
* @property {number} [maxFPS] - Maximum FPS
|
|
93
94
|
*/
|
|
94
95
|
|
|
@@ -174,6 +174,10 @@ const generateElementCode = function (
|
|
|
174
174
|
renderCode.push(`elementConfigs[${counter}][Symbol.for('isSlot')] = true`)
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
if (options.holder) {
|
|
178
|
+
renderCode.push(`elementConfigs[${counter}]['holder'] = true`)
|
|
179
|
+
}
|
|
180
|
+
|
|
177
181
|
Object.keys(templateObject).forEach((key) => {
|
|
178
182
|
if (key === 'slot') {
|
|
179
183
|
renderCode.push(`
|
package/src/lib/lifecycle.js
CHANGED
|
@@ -31,6 +31,8 @@ const states = [
|
|
|
31
31
|
'ready', // fired when component instantiated, reactivity setup done and template spawned
|
|
32
32
|
'focus', // fired when receiving focus (can occur multiple times)
|
|
33
33
|
'unfocus', // fired when losing focus (can occur multiple times)
|
|
34
|
+
'hover', // fired when receiving focus (can occur multiple times)
|
|
35
|
+
'unhover', // fired when losing focus (can occur multiple times)
|
|
34
36
|
'destroy', // fired when component is destroyed and removed
|
|
35
37
|
'attach', // fired when entering the viewport margin and attached to the render tree
|
|
36
38
|
'detach', // fired when leaving the viewport margin and detached from the render tree
|
|
@@ -85,11 +87,16 @@ export default {
|
|
|
85
87
|
// emit 'public' hook
|
|
86
88
|
emit(v, this.component[symbols.identifier], this.component)
|
|
87
89
|
// update the built-in hasFocus state variable
|
|
90
|
+
if (v === 'focus') this.component[symbols.state].hasFocus = true
|
|
91
|
+
if (v === 'unfocus') this.component[symbols.state].hasFocus = false
|
|
88
92
|
if (v === 'focus' || v === 'unfocus') {
|
|
89
93
|
if (inspectorEnabled === null) {
|
|
90
94
|
inspectorEnabled = Settings.get('inspector', false)
|
|
91
95
|
}
|
|
92
96
|
}
|
|
97
|
+
// update the built-in isHovered state variable
|
|
98
|
+
if (v === 'hover') this.component[symbols.state].isHovered = true
|
|
99
|
+
if (v === 'unhover') this.component[symbols.state].isHovered = false
|
|
93
100
|
if (v === 'focus') {
|
|
94
101
|
this.component[symbols.state].hasFocus = true
|
|
95
102
|
if (
|
package/src/router/router.js
CHANGED
|
@@ -21,7 +21,7 @@ import { reactive } from '../lib/reactivity/reactive.js'
|
|
|
21
21
|
import symbols from '../lib/symbols.js'
|
|
22
22
|
import { Log } from '../lib/log.js'
|
|
23
23
|
import { stage } from '../launch.js'
|
|
24
|
-
import Focus from '../focus.js'
|
|
24
|
+
import Focus from '../focus/focus.js'
|
|
25
25
|
import Announcer from '../announcer/announcer.js'
|
|
26
26
|
import Settings from '../settings.js'
|
|
27
27
|
|
|
@@ -216,12 +216,17 @@ const defaultOptions = {
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
const makeRouteObject = (route, overrides) => {
|
|
219
|
+
// FIX: exclude keepAlive from the destination route options. Unlike other
|
|
220
|
+
// overrides, keepAlive applies to the route being LEFT, not the destination.
|
|
221
|
+
// It is consumed by removeView() instead.
|
|
222
|
+
const { keepAlive: _keepAlive, ...destOverrides } = overrideOptions // eslint-disable-line no-unused-vars
|
|
223
|
+
|
|
219
224
|
const cleanRoute = {
|
|
220
225
|
hash: overrides.hash,
|
|
221
226
|
path: route.path,
|
|
222
227
|
component: route.component,
|
|
223
228
|
transition: 'transition' in route ? route.transition : fadeInFadeOutTransition,
|
|
224
|
-
options: { ...defaultOptions, ...route.options, ...
|
|
229
|
+
options: { ...defaultOptions, ...route.options, ...destOverrides },
|
|
225
230
|
announce: route.announce || false,
|
|
226
231
|
hooks: route.hooks || {},
|
|
227
232
|
data: { ...route.data, ...navigationData, ...overrides.queryParams },
|
|
@@ -342,8 +347,12 @@ export const navigate = async function () {
|
|
|
342
347
|
|
|
343
348
|
// add the previous route (technically still the current route at this point)
|
|
344
349
|
// into the history stack when inHistory is true and we're not navigating back
|
|
350
|
+
//
|
|
351
|
+
// FIX: use truthy check instead of `!== undefined` because matchHash()
|
|
352
|
+
// can return `false`, which survives `!== undefined` but has no `.options`.
|
|
345
353
|
if (
|
|
346
|
-
previousRoute
|
|
354
|
+
previousRoute &&
|
|
355
|
+
previousRoute.options &&
|
|
347
356
|
previousRoute.options.inHistory === true &&
|
|
348
357
|
navigatingBack === false
|
|
349
358
|
) {
|
|
@@ -485,7 +494,8 @@ export const navigate = async function () {
|
|
|
485
494
|
|
|
486
495
|
// apply out out transition on previous view if available, unless
|
|
487
496
|
// we're reusing the prvious page component
|
|
488
|
-
|
|
497
|
+
// FIX: truthy guard — previousRoute can be `false` (see history-push comment above).
|
|
498
|
+
if (previousRoute && reuse === false) {
|
|
489
499
|
// only animate when there is a previous route
|
|
490
500
|
shouldAnimate = true
|
|
491
501
|
const oldView = this[symbols.children].splice(1, 1).pop()
|
|
@@ -542,6 +552,12 @@ export const navigate = async function () {
|
|
|
542
552
|
}
|
|
543
553
|
}
|
|
544
554
|
|
|
555
|
+
// Clear module-level variables after removeView has consumed them.
|
|
556
|
+
// Placed here so it executes for all navigation flows, not only when
|
|
557
|
+
// previousRoute exists and reuse is false.
|
|
558
|
+
overrideOptions = {}
|
|
559
|
+
navigationData = {}
|
|
560
|
+
|
|
545
561
|
// reset navigating indicators
|
|
546
562
|
navigatingBack = false
|
|
547
563
|
state.navigating = false
|
|
@@ -569,11 +585,18 @@ const removeView = async (route, view, transition, navigatingBack) => {
|
|
|
569
585
|
}
|
|
570
586
|
}
|
|
571
587
|
|
|
588
|
+
// Resolve effective keepAlive: runtime override from $router.to() takes precedence
|
|
589
|
+
// over the static route config option
|
|
590
|
+
const keepAlive =
|
|
591
|
+
overrideOptions.keepAlive !== undefined
|
|
592
|
+
? overrideOptions.keepAlive
|
|
593
|
+
: route.options && route.options.keepAlive
|
|
594
|
+
|
|
572
595
|
// cache the page when it's as 'keepAlive' instead of destroying
|
|
573
596
|
if (
|
|
574
597
|
navigatingBack === false &&
|
|
575
598
|
route.options &&
|
|
576
|
-
|
|
599
|
+
keepAlive === true &&
|
|
577
600
|
route.options.inHistory === true
|
|
578
601
|
) {
|
|
579
602
|
const historyItem = history[history.length - 1]
|
|
@@ -588,7 +611,7 @@ const removeView = async (route, view, transition, navigatingBack) => {
|
|
|
588
611
|
* 2. Navigating back, and the previous route is configured with "keep alive" set to true.
|
|
589
612
|
* 3. Navigating back, and the previous route is not configured with "keep alive" set to true.
|
|
590
613
|
*/
|
|
591
|
-
if (route.options && (
|
|
614
|
+
if (route.options && (keepAlive !== true || navigatingBack === true)) {
|
|
592
615
|
view.destroy()
|
|
593
616
|
view = null
|
|
594
617
|
}
|