@lightningjs/blits 1.31.2 → 1.32.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.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 &&
@@ -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)
@@ -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]
@@ -686,9 +684,6 @@ const Element = {
686
684
  this.config = null
687
685
  delete this.config
688
686
 
689
- this.counter = null
690
- delete this.counter
691
-
692
687
  this.props.raw = {}
693
688
  this.props.element = null
694
689
  this.props.props = null
@@ -714,9 +709,20 @@ const Element = {
714
709
  return this.node && this.node.parent
715
710
  },
716
711
  get children() {
717
- return this.component[symbols.getChildren]().filter((child) => {
718
- return child.parent === (this[symbols.isSlot] ? this.node.children[0] : this.node)
719
- })
712
+ const allChildren = this.component[symbols.getChildren]()
713
+ const directChildren = []
714
+ const l = allChildren.length
715
+ for (let i = 0; i < l; i++) {
716
+ const child = allChildren[i]
717
+ if (
718
+ child !== undefined &&
719
+ child.parent === (this[symbols.isSlot] ? this.node.children[0] : this.node)
720
+ ) {
721
+ directChildren.push(child)
722
+ }
723
+ }
724
+
725
+ return directChildren
720
726
  },
721
727
  }
722
728
 
@@ -739,6 +745,5 @@ export default (config, component) => {
739
745
  scheduledTransitions: {},
740
746
  config,
741
747
  component,
742
- counter: counter++,
743
748
  })
744
749
  }
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
  }
@@ -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