@lightningjs/blits 1.31.2 → 1.32.1-rc.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,12 +1,23 @@
1
1
  # Changelog
2
2
 
3
- _30 Jun 2024_
3
+ _02 Jul 2025_
4
+
5
+ ## v1.32.0
6
+
7
+ - Added global router hooks `init` and `error`
8
+ - Fixed issues with `this` scope in global router hooks
9
+ - Fixed issue with `data` in `beforeEach` route not being complete
10
+ - Added input throttling functionality
11
+ - Optimized performance of children retrieval in `<Layout>`
12
+ - Added documentation on global router hooks
13
+
14
+ _30 Jun 2025_
4
15
 
5
16
  ## v1.31.1
6
17
 
7
18
  - Upgraded renderer to v2.15.0
8
19
 
9
- _30 Jun 2024_
20
+ _30 Jun 2025_
10
21
 
11
22
  ## v1.31.1
12
23
 
package/index.d.ts CHANGED
@@ -481,14 +481,16 @@ declare module '@lightningjs/blits' {
481
481
  }
482
482
 
483
483
  export interface RouterHooks {
484
+ init?: () => Promise<> | void;
484
485
  beforeEach?: (to: Route, from: Route) => string | Route | Promise<string | Route> | void;
486
+ error?: (err: string) => string | Route | Promise<string | Route> | void;
485
487
  }
486
488
 
487
- export interface RouterConfig {
489
+ export interface RouterConfig<P extends Props, S, M, C> {
488
490
  /**
489
491
  * Register hooks for the router
490
492
  */
491
- hooks?: RouterHooks,
493
+ hooks?: RouterHooks & ComponentContext<P, S, M, C>,
492
494
 
493
495
  /**
494
496
  * Routes definition
@@ -511,7 +513,7 @@ declare module '@lightningjs/blits' {
511
513
  /**
512
514
  * Router Configuration
513
515
  */
514
- router?: RouterConfig,
516
+ router?: RouterConfig<P, S, M, C>,
515
517
  routes?: never
516
518
  }
517
519
  |
@@ -965,6 +967,17 @@ declare module '@lightningjs/blits' {
965
967
  * Defaults to `50` (ms)
966
968
  */
967
969
  holdTimeout?: number,
970
+ /**
971
+ * Input throttle time in milliseconds to prevent rapid successive inputs
972
+ *
973
+ * Within the throttle window, only one input will be processed immediately Subsequent
974
+ * inputs are ignored until the scheduled input is processed.
975
+ *
976
+ * Set to `0` to disable input throttling.
977
+ *
978
+ * Defaults to `0` (disabled)
979
+ */
980
+ inputThrottle?: number,
968
981
  /**
969
982
  * Custom canvas object used to render the App to.
970
983
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "1.31.2",
3
+ "version": "1.32.1-rc.0",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -47,6 +47,7 @@ const Application = (config) => {
47
47
  let keyDownHandler
48
48
  let keyUpHandler
49
49
  let holdTimeout
50
+ let lastInputTime = 0
50
51
 
51
52
  config.hooks[symbols.destroy] = function () {
52
53
  document.removeEventListener('keydown', keyDownHandler)
@@ -58,8 +59,27 @@ const Application = (config) => {
58
59
  this.$announcer.toggle(Settings.get('announcer', false))
59
60
  const keyMap = { ...defaultKeyMap, ...Settings.get('keymap', {}) }
60
61
 
62
+ /** @type {number} Input throttle time in milliseconds (0 = disabled) */
63
+ const throttleMs = Settings.get('inputThrottle', 0)
64
+
61
65
  keyDownHandler = async (e) => {
66
+ const currentTime = performance.now()
67
+
62
68
  const key = keyMap[e.key] || keyMap[e.keyCode] || e.key || e.keyCode
69
+ // execute immediately when no throttle is specified or event is internal (bubbled up by focus manager)
70
+ if (throttleMs === 0 || e[symbols.internalEvent] === true) {
71
+ return await processInput.call(this, e, key)
72
+ }
73
+
74
+ if (currentTime - lastInputTime < throttleMs) {
75
+ return
76
+ }
77
+
78
+ lastInputTime = currentTime
79
+ await processInput.call(this, e, key)
80
+ }
81
+
82
+ const processInput = async function (e, key) {
63
83
  // intercept key press if specified in main Application component
64
84
  if (
65
85
  this[symbols.inputEvents] !== undefined &&
@@ -50,16 +50,17 @@ export default () =>
50
50
  },
51
51
  effects: [
52
52
  function anonymous(component, elms, context, components, rootComponent, effect) {
53
- elms[0].set('color', component.color)
53
+ if (elms[0] !== undefined) elms[0].set('color', component.color)
54
54
  },
55
55
  function anonymous(component, elms, context, components, rootComponent, effect) {
56
- elms[0].set('w', component.size)
56
+ if (elms[0] !== undefined) elms[0].set('w', component.size)
57
57
  },
58
58
  function anonymous(component, elms, context, components, rootComponent, effect) {
59
- elms[0].set('h', component.size)
59
+ if (elms[0] !== undefined) elms[0].set('h', component.size)
60
60
  },
61
61
  function anonymous(component, elms, context, components, rootComponent, effect) {
62
- elms[0].set('effects', [component.shader('radius', { radius: component.radius })])
62
+ if (elms[0] !== undefined)
63
+ elms[0].set('effects', [component.shader('radius', { radius: component.radius })])
63
64
  },
64
65
  ],
65
66
  context: {},
@@ -1366,84 +1366,84 @@ export default () =>
1366
1366
  effects: [
1367
1367
  function anonymous(component, elms, context, components, rootComponent, effect) {
1368
1368
  if (typeof skip6 === 'undefined' || skip6.indexOf('frame') === -1)
1369
- elms[6].set('frame', component.fps[0])
1369
+ if (elms[6] !== undefined) elms[6].set('frame', component.fps[0])
1370
1370
  },
1371
1371
  function anonymous(component, elms, context, components, rootComponent, effect) {
1372
1372
  elms[7][Symbol.for('props')]['frame'] = component.fps[0]
1373
1373
  },
1374
1374
  function anonymous(component, elms, context, components, rootComponent, effect) {
1375
1375
  if (typeof skip8 === 'undefined' || skip8.indexOf('frame') === -1)
1376
- elms[8].set('frame', component.fps[1])
1376
+ if (elms[8] !== undefined) elms[8].set('frame', component.fps[1])
1377
1377
  },
1378
1378
  function anonymous(component, elms, context, components, rootComponent, effect) {
1379
1379
  elms[9][Symbol.for('props')]['frame'] = component.fps[1]
1380
1380
  },
1381
1381
  function anonymous(component, elms, context, components, rootComponent, effect) {
1382
1382
  if (typeof skip10 === 'undefined' || skip10.indexOf('frame') === -1)
1383
- elms[10].set('frame', component.fps[2])
1383
+ if (elms[10] !== undefined) elms[10].set('frame', component.fps[2])
1384
1384
  },
1385
1385
  function anonymous(component, elms, context, components, rootComponent, effect) {
1386
1386
  elms[11][Symbol.for('props')]['frame'] = component.fps[2]
1387
1387
  },
1388
1388
  function anonymous(component, elms, context, components, rootComponent, effect) {
1389
1389
  if (typeof skip16 === 'undefined' || skip16.indexOf('frame') === -1)
1390
- elms[16].set('frame', component.avgFps[0])
1390
+ if (elms[16] !== undefined) elms[16].set('frame', component.avgFps[0])
1391
1391
  },
1392
1392
  function anonymous(component, elms, context, components, rootComponent, effect) {
1393
1393
  elms[17][Symbol.for('props')]['frame'] = component.avgFps[0]
1394
1394
  },
1395
1395
  function anonymous(component, elms, context, components, rootComponent, effect) {
1396
1396
  if (typeof skip18 === 'undefined' || skip18.indexOf('frame') === -1)
1397
- elms[18].set('frame', component.avgFps[1])
1397
+ if (elms[18] !== undefined) elms[18].set('frame', component.avgFps[1])
1398
1398
  },
1399
1399
  function anonymous(component, elms, context, components, rootComponent, effect) {
1400
1400
  elms[19][Symbol.for('props')]['frame'] = component.avgFps[1]
1401
1401
  },
1402
1402
  function anonymous(component, elms, context, components, rootComponent, effect) {
1403
1403
  if (typeof skip20 === 'undefined' || skip20.indexOf('frame') === -1)
1404
- elms[20].set('frame', component.avgFps[2])
1404
+ if (elms[20] !== undefined) elms[20].set('frame', component.avgFps[2])
1405
1405
  },
1406
1406
  function anonymous(component, elms, context, components, rootComponent, effect) {
1407
1407
  elms[21][Symbol.for('props')]['frame'] = component.avgFps[2]
1408
1408
  },
1409
1409
  function anonymous(component, elms, context, components, rootComponent, effect) {
1410
1410
  if (typeof skip26 === 'undefined' || skip26.indexOf('frame') === -1)
1411
- elms[26].set('frame', component.minFps[0])
1411
+ if (elms[26] !== undefined) elms[26].set('frame', component.minFps[0])
1412
1412
  },
1413
1413
  function anonymous(component, elms, context, components, rootComponent, effect) {
1414
1414
  elms[27][Symbol.for('props')]['frame'] = component.minFps[0]
1415
1415
  },
1416
1416
  function anonymous(component, elms, context, components, rootComponent, effect) {
1417
1417
  if (typeof skip28 === 'undefined' || skip28.indexOf('frame') === -1)
1418
- elms[28].set('frame', component.minFps[1])
1418
+ if (elms[28] !== undefined) elms[28].set('frame', component.minFps[1])
1419
1419
  },
1420
1420
  function anonymous(component, elms, context, components, rootComponent, effect) {
1421
1421
  elms[29][Symbol.for('props')]['frame'] = component.minFps[1]
1422
1422
  },
1423
1423
  function anonymous(component, elms, context, components, rootComponent, effect) {
1424
1424
  if (typeof skip30 === 'undefined' || skip30.indexOf('frame') === -1)
1425
- elms[30].set('frame', component.minFps[2])
1425
+ if (elms[30] !== undefined) elms[30].set('frame', component.minFps[2])
1426
1426
  },
1427
1427
  function anonymous(component, elms, context, components, rootComponent, effect) {
1428
1428
  elms[31][Symbol.for('props')]['frame'] = component.minFps[2]
1429
1429
  },
1430
1430
  function anonymous(component, elms, context, components, rootComponent, effect) {
1431
1431
  if (typeof skip36 === 'undefined' || skip36.indexOf('frame') === -1)
1432
- elms[36].set('frame', component.maxFps[0])
1432
+ if (elms[36] !== undefined) elms[36].set('frame', component.maxFps[0])
1433
1433
  },
1434
1434
  function anonymous(component, elms, context, components, rootComponent, effect) {
1435
1435
  elms[37][Symbol.for('props')]['frame'] = component.maxFps[0]
1436
1436
  },
1437
1437
  function anonymous(component, elms, context, components, rootComponent, effect) {
1438
1438
  if (typeof skip38 === 'undefined' || skip38.indexOf('frame') === -1)
1439
- elms[38].set('frame', component.maxFps[1])
1439
+ if (elms[38] !== undefined) elms[38].set('frame', component.maxFps[1])
1440
1440
  },
1441
1441
  function anonymous(component, elms, context, components, rootComponent, effect) {
1442
1442
  elms[39][Symbol.for('props')]['frame'] = component.maxFps[1]
1443
1443
  },
1444
1444
  function anonymous(component, elms, context, components, rootComponent, effect) {
1445
1445
  if (typeof skip40 === 'undefined' || skip40.indexOf('frame') === -1)
1446
- elms[40].set('frame', component.maxFps[2])
1446
+ if (elms[40] !== undefined) elms[40].set('frame', component.maxFps[2])
1447
1447
  },
1448
1448
  function anonymous(component, elms, context, components, rootComponent, effect) {
1449
1449
  elms[41][Symbol.for('props')]['frame'] = component.maxFps[2]
@@ -17,6 +17,7 @@
17
17
 
18
18
  import Component from '../component.js'
19
19
  import Router from '../router/router.js'
20
+ import symbols from '../lib/symbols.js'
20
21
 
21
22
  let hashchangeHandler = null
22
23
 
@@ -58,7 +59,10 @@ export default () =>
58
59
  }
59
60
  },
60
61
  hooks: {
61
- ready() {
62
+ async ready() {
63
+ if (this.parent[symbols.routerHooks] && this.parent[symbols.routerHooks].init) {
64
+ await this.parent[symbols.routerHooks].init.apply(this.parent)
65
+ }
62
66
  hashchangeHandler = () => Router.navigate.apply(this)
63
67
  Router.navigate.apply(this)
64
68
  window.addEventListener('hashchange', hashchangeHandler)
@@ -52,10 +52,10 @@ export default () =>
52
52
  },
53
53
  effects: [
54
54
  function anonymous(component, elms, context, components, rootComponent, effect) {
55
- elms[0].set('texture', component.texture)
55
+ if (elms[0] !== undefined) elms[0].set('texture', component.texture)
56
56
  },
57
57
  function anonymous(component, elms, context, components, rootComponent, effect) {
58
- elms[0].set('color', component.color)
58
+ if (elms[0] !== undefined) elms[0].set('color', component.color)
59
59
  },
60
60
  ],
61
61
  context: {},
@@ -22,9 +22,6 @@ import { Log } from '../../lib/log.js'
22
22
  import symbols from '../../lib/symbols.js'
23
23
  import Settings from '../../settings.js'
24
24
 
25
- // temporary counter to work around shader caching issues
26
- let counter = 0
27
-
28
25
  /**
29
26
  * Creates a padding object from a value and direction.
30
27
  * @param {number|object|string|undefined} padding - The padding value.
@@ -90,10 +87,11 @@ const layoutFn = function (config) {
90
87
 
91
88
  const children = this.node.children
92
89
  const childrenLength = children.length
90
+ const elementChildren = this.children
93
91
  let otherDimension = 0
94
92
  const gap = config.gap || 0
95
93
  for (let i = 0; i < childrenLength; i++) {
96
- if (this.children[i] !== undefined && this.children[i].props.raw.show === false) {
94
+ if (elementChildren[i] !== undefined && elementChildren[i].props.raw.show === false) {
97
95
  continue
98
96
  }
99
97
  const node = children[i]
@@ -548,6 +546,7 @@ const Element = {
548
546
  */
549
547
  set(prop, value) {
550
548
  if (value === undefined) return
549
+ if (this.props === undefined) return
551
550
  // @ts-ignore
552
551
  if (this.props.raw[prop] === value) return
553
552
  // @ts-ignore
@@ -686,9 +685,6 @@ const Element = {
686
685
  this.config = null
687
686
  delete this.config
688
687
 
689
- this.counter = null
690
- delete this.counter
691
-
692
688
  this.props.raw = {}
693
689
  this.props.element = null
694
690
  this.props.props = null
@@ -714,9 +710,20 @@ const Element = {
714
710
  return this.node && this.node.parent
715
711
  },
716
712
  get children() {
717
- return this.component[symbols.getChildren]().filter((child) => {
718
- return child.parent === (this[symbols.isSlot] ? this.node.children[0] : this.node)
719
- })
713
+ const allChildren = this.component[symbols.getChildren]()
714
+ const directChildren = []
715
+ const l = allChildren.length
716
+ for (let i = 0; i < l; i++) {
717
+ const child = allChildren[i]
718
+ if (
719
+ child !== undefined &&
720
+ child.parent === (this[symbols.isSlot] ? this.node.children[0] : this.node)
721
+ ) {
722
+ directChildren.push(child)
723
+ }
724
+ }
725
+
726
+ return directChildren
720
727
  },
721
728
  }
722
729
 
@@ -739,6 +746,5 @@ export default (config, component) => {
739
746
  scheduledTransitions: {},
740
747
  config,
741
748
  component,
742
- counter: counter++,
743
749
  })
744
750
  }
package/src/focus.js CHANGED
@@ -48,7 +48,10 @@ export default {
48
48
  focusedComponent = component
49
49
  focusedComponent.lifecycle.state = 'focus'
50
50
  if (event instanceof KeyboardEvent) {
51
- document.dispatchEvent(new KeyboardEvent('keydown', event))
51
+ const internalEvent = new KeyboardEvent('keydown', event)
52
+ // @ts-ignore - this is an internal event
53
+ internalEvent[symbols.internalEvent] = true
54
+ document.dispatchEvent(internalEvent)
52
55
  } else {
53
56
  focusChain = []
54
57
  }
@@ -162,11 +162,14 @@ const generateElementCode = function (
162
162
  this.effectsCode.push(`
163
163
  if(typeof skip${counter} === 'undefined' ||
164
164
  skip${counter}.indexOf('${key.substring(1)}') === -1)
165
- ${elm}.set('${key.substring(1)}', ${interpolate(templateObject[key], options.component)})
165
+ if(${elm} !== undefined) ${elm}.set('${key.substring(1)}', ${interpolate(
166
+ templateObject[key],
167
+ options.component
168
+ )})
166
169
  `)
167
170
  } else {
168
171
  this.effectsCode.push(`
169
- ${elm}.set('${key.substring(1)}', ${interpolate(
172
+ if(${elm} !== undefined) ${elm}.set('${key.substring(1)}', ${interpolate(
170
173
  templateObject[key],
171
174
  options.component
172
175
  )})
@@ -13,6 +13,7 @@
13
13
  * @property {symbol} index
14
14
  * @property {symbol} init
15
15
  * @property {symbol} inputEvents
16
+ * @property {symbol} internalEvent
16
17
  * @property {symbol} intervals
17
18
  * @property {symbol} launched
18
19
  * @property {symbol} level
@@ -62,6 +63,7 @@ export default {
62
63
  index: Symbol('index'),
63
64
  init: Symbol('init'),
64
65
  inputEvents: Symbol('inputEvents'),
66
+ internalEvent: Symbol('internalEvent'),
65
67
  intervals: Symbol('intervals'),
66
68
  launched: Symbol('launched'),
67
69
  level: Symbol('level'),
@@ -207,18 +207,29 @@ export const navigate = async function () {
207
207
  const { hash, path, queryParams } = getHash()
208
208
  let route = matchHash(path, this.parent[symbols.routes])
209
209
 
210
- // Adding the location hash to the route if it exists.
211
- if (hash !== null) {
212
- route.hash = hash
210
+ const queryParamsData = {}
211
+ const queryParamsEntries = [...queryParams.entries()]
212
+ for (let i = 0; i < queryParamsEntries.length; i++) {
213
+ queryParamsData[queryParamsEntries[i][0]] = queryParamsEntries[i][1]
214
+ }
215
+
216
+ route.data = {
217
+ ...navigationData,
218
+ ...route.data,
219
+ ...queryParamsData,
213
220
  }
214
221
 
215
222
  currentRoute = route
216
223
  if (route) {
224
+ // Adding the location hash to the route if it exists.
225
+ if (hash !== null) {
226
+ route.hash = hash
227
+ }
217
228
  let beforeEachResult
218
229
  if (this.parent[symbols.routerHooks]) {
219
230
  const hooks = this.parent[symbols.routerHooks]
220
231
  if (hooks.beforeEach) {
221
- beforeEachResult = await hooks.beforeEach(route, previousRoute)
232
+ beforeEachResult = await hooks.beforeEach.call(this.parent, route, previousRoute)
222
233
  if (isString(beforeEachResult)) {
223
234
  to(beforeEachResult)
224
235
  return
@@ -258,7 +269,6 @@ export const navigate = async function () {
258
269
 
259
270
  /** @type {import('../engines/L3/element.js').BlitsElement} */
260
271
  let holder
261
- let routeData
262
272
 
263
273
  let { view, focus } = cacheMap.get(route.hash) || {}
264
274
 
@@ -279,23 +289,11 @@ export const navigate = async function () {
279
289
  holder.set('w', '100%')
280
290
  holder.set('h', '100%')
281
291
 
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
292
  // merge props with potential route params, navigation data and route data to be injected into the component instance
295
293
  const props = {
296
294
  ...this[symbols.props],
297
295
  ...route.params,
298
- ...routeData,
296
+ ...route.data,
299
297
  }
300
298
 
301
299
  view = await route.component({ props }, holder, this)
@@ -369,7 +367,7 @@ export const navigate = async function () {
369
367
  state.path = route.path
370
368
  state.params = route.params
371
369
  state.hash = hash
372
- state.data = routeData
370
+ state.data = route.data
373
371
 
374
372
  // apply in transition
375
373
  if (route.transition.in) {
@@ -383,10 +381,14 @@ export const navigate = async function () {
383
381
  await setOrAnimate(holder, route.transition.in, shouldAnimate)
384
382
  }
385
383
  }
386
-
387
- this.activeView = this[symbols.children][this[symbols.children].length - 1]
384
+ const children = this[symbols.children]
385
+ this.activeView = children[children.length - 1]
388
386
  } else {
389
387
  Log.error(`Route ${hash} not found`)
388
+ const routerHooks = this.parent[symbols.routerHooks]
389
+ if (routerHooks && typeof routerHooks.error === 'function') {
390
+ routerHooks.error.call(this.parent, `Route ${hash} not found`)
391
+ }
390
392
  }
391
393
  }
392
394