@lightningjs/blits 1.43.1 → 1.44.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,8 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.44.0
4
+
5
+ _20 Nov 2025_
6
+
7
+ - Added `$input` method to facilitate key handling _without_ also passing focus
8
+ - Added support for passing `type` of image in the `src`-attribute (`regular`, `svg`, `compressed` for cases the file type can't be derived from the file name)
9
+ - Bumped Lightning renderer to v2.20.0 with improved support for compressed textures
10
+
11
+
12
+ ## v1.43.2
13
+
14
+ _13 Nov 2025_
15
+
16
+ - Fixed issue with endless loop in router when first page in history would fail an async before / beforeEach hook (introduced in v1.43.0)
17
+
3
18
  ## v1.43.1
4
19
 
5
- _12 Nov 2025_
20
+ _13 Nov 2025_
6
21
 
7
22
  - Added arguments of fpsUpdate and frameTick hooks to type definitions
8
23
 
package/index.d.ts CHANGED
@@ -431,6 +431,12 @@ declare module '@lightningjs/blits' {
431
431
  * Set focus to the Component, optionally pass a KeyboardEvent for instant event bubbling
432
432
  */
433
433
  $focus: (event?: KeyboardEvent) => void
434
+ /**
435
+ * Handle a keyboard event on this component without changing focus
436
+ * @param event - The keyboard event to handle
437
+ * @returns Returns true if this component or a parent component handled the event, false otherwise
438
+ */
439
+ $input: (event: KeyboardEvent) => boolean
434
440
  /**
435
441
  * @deprecated
436
442
  * Deprecated: use `this.$focus()` instead
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "1.43.1",
3
+ "version": "1.44.0",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "@lightningjs/msdf-generator": "^1.2.0",
58
- "@lightningjs/renderer": "^2.19.1"
58
+ "@lightningjs/renderer": "^2.20.0"
59
59
  },
60
60
  "repository": {
61
61
  "type": "git",
@@ -20,28 +20,16 @@ import { default as Focus, keyUpCallbacks } from './focus.js'
20
20
  import Settings from './settings.js'
21
21
 
22
22
  import symbols from './lib/symbols.js'
23
- import { DEFAULT_HOLD_TIMEOUT_MS } from './constants.js'
23
+ import { DEFAULT_HOLD_TIMEOUT_MS, DEFAULT_KEYMAP } from './constants.js'
24
24
 
25
- const Application = (config) => {
26
- const defaultKeyMap = {
27
- ArrowLeft: 'left',
28
- ArrowRight: 'right',
29
- ArrowUp: 'up',
30
- ArrowDown: 'down',
31
- Enter: 'enter',
32
- ' ': 'space',
33
- Backspace: 'back',
34
- Escape: 'escape',
35
- 37: 'left',
36
- 39: 'right',
37
- 38: 'up',
38
- 40: 'down',
39
- 13: 'enter',
40
- 32: 'space',
41
- 8: 'back',
42
- 27: 'escape',
43
- }
25
+ /**
26
+ * Merged keyMap (default + custom settings).
27
+ * Initialized once during application init.
28
+ * @type {Object<string, string>}
29
+ */
30
+ export let keyMap = {}
44
31
 
32
+ const Application = (config) => {
45
33
  config.hooks = config.hooks || {}
46
34
 
47
35
  let keyDownHandler
@@ -63,7 +51,8 @@ const Application = (config) => {
63
51
  if (announcerOptions && typeof announcerOptions === 'object') {
64
52
  this.$announcer.configure(announcerOptions)
65
53
  }
66
- const keyMap = { ...defaultKeyMap, ...Settings.get('keymap', {}) }
54
+ // Initialize merged keyMap once during application init
55
+ keyMap = { ...DEFAULT_KEYMAP, ...Settings.get('keymap', {}) }
67
56
 
68
57
  /** @type {number} Input throttle time in milliseconds (0 = disabled) */
69
58
  const throttleMs = Settings.get('inputThrottle', 0)
@@ -17,11 +17,13 @@
17
17
 
18
18
  import symbols from '../../lib/symbols.js'
19
19
  import Focus from '../../focus.js'
20
+ import { keyUpCallbacks, getComponentWithInputEvent } from '../../focus.js'
20
21
  import eventListeners from '../../lib/eventListeners.js'
21
22
  import { trigger } from '../../lib/reactivity/effect.js'
22
23
  import { Log } from '../../lib/log.js'
23
24
  import { removeGlobalEffects } from '../../lib/reactivity/effect.js'
24
25
  import { renderer } from '../../launch.js'
26
+ import { keyMap } from '../../application.js'
25
27
 
26
28
  export default {
27
29
  focus: {
@@ -51,6 +53,41 @@ export default {
51
53
  enumerable: true,
52
54
  configurable: false,
53
55
  },
56
+ $input: {
57
+ /**
58
+ * Handle a keyboard event on this component without changing focus
59
+ * @this {import('../../component').BlitsComponent}
60
+ * @param {KeyboardEvent} event - The keyboard event to handle
61
+ * @returns {boolean} - Returns true if this component or a parent component handled the event, false otherwise
62
+ */
63
+ value: function (event) {
64
+ if (event === null || event === undefined || event instanceof KeyboardEvent === false)
65
+ return false
66
+
67
+ const key = keyMap[event.key] || keyMap[event.keyCode] || event.key || event.keyCode
68
+
69
+ const componentWithInputEvent = getComponentWithInputEvent(this, key)
70
+ if (componentWithInputEvent === null) return false
71
+
72
+ const inputEvents = componentWithInputEvent[symbols.inputEvents] || {}
73
+
74
+ let cb
75
+ if (inputEvents[key]) {
76
+ cb = inputEvents[key].call(componentWithInputEvent, event)
77
+ } else if (inputEvents.any) {
78
+ cb = inputEvents.any.call(componentWithInputEvent, event)
79
+ }
80
+
81
+ if (cb !== undefined && event.code) {
82
+ keyUpCallbacks.set(event.code, cb)
83
+ }
84
+
85
+ return true
86
+ },
87
+ writable: false,
88
+ enumerable: true,
89
+ configurable: false,
90
+ },
54
91
  unfocus: {
55
92
  /**
56
93
  * @this {import('../../component').BlitsComponent}
package/src/constants.js CHANGED
@@ -1,5 +1,24 @@
1
1
  export const DEFAULT_HOLD_TIMEOUT_MS = 50
2
2
 
3
+ export const DEFAULT_KEYMAP = {
4
+ ArrowLeft: 'left',
5
+ ArrowRight: 'right',
6
+ ArrowUp: 'up',
7
+ ArrowDown: 'down',
8
+ Enter: 'enter',
9
+ ' ': 'space',
10
+ Backspace: 'back',
11
+ Escape: 'escape',
12
+ 37: 'left',
13
+ 39: 'right',
14
+ 38: 'up',
15
+ 40: 'down',
16
+ 13: 'enter',
17
+ 32: 'space',
18
+ 8: 'back',
19
+ 27: 'escape',
20
+ }
21
+
3
22
  export const SCREEN_RESOLUTIONS = {
4
23
  hd: 0.66666667,
5
24
  '720p': 0.66666667,
@@ -168,7 +168,7 @@ const isObjectString = (str) => {
168
168
  * @returns {object} The parsed object.
169
169
  */
170
170
  const parseToObject = (str) => {
171
- return JSON.parse(str.replace(/'/g, '"').replace(/([\w-_]+)\s*:/g, '"$1":'))
171
+ return JSON.parse(str.replace(/'/g, '"').replace(/([{,]\s*)([\w-_]+)(\s*:)/g, '$1"$2"$3'))
172
172
  }
173
173
 
174
174
  /**
@@ -276,7 +276,13 @@ const propsTransformer = {
276
276
  }
277
277
  },
278
278
  set src(v) {
279
- this.props['src'] = v
279
+ if (typeof v === 'object' || (isObjectString(v) === true && (v = parseToObject(v)))) {
280
+ this.props['src'] = v.src
281
+ this.props['imageType'] = v.type
282
+ } else {
283
+ this.props['src'] = v
284
+ }
285
+
280
286
  if (this.raw['color'] === undefined) {
281
287
  this.props['color'] = this.props['src'] ? 0xffffffff : 0x00000000
282
288
  }
package/src/focus.js CHANGED
@@ -118,7 +118,7 @@ const getAncestors = (components) => {
118
118
  * @param {String} key
119
119
  * @returns component
120
120
  */
121
- const getComponentWithInputEvent = (component, key) => {
121
+ export const getComponentWithInputEvent = (component, key) => {
122
122
  if (
123
123
  component[symbols.inputEvents] &&
124
124
  (typeof component[symbols.inputEvents][key] === 'function' ||
@@ -267,13 +267,15 @@ export const navigate = async function () {
267
267
  } catch (error) {
268
268
  Log.error('Error or Rejected Promise in "BeforeEach" Hook', error)
269
269
 
270
- preventHashChangeNavigation = true
271
- currentRoute = previousRoute
272
- window.history.back()
270
+ if (history.length > 0) {
271
+ preventHashChangeNavigation = true
272
+ currentRoute = previousRoute
273
+ window.history.back()
273
274
 
274
- navigatingBack = false
275
- state.navigating = false
276
- return
275
+ navigatingBack = false
276
+ state.navigating = false
277
+ return
278
+ }
277
279
  }
278
280
  // If the resolved result is an object, redirect if the path in the object was changed
279
281
  if (isObject(beforeEachResult) === true && beforeEachResult.path !== currentPath) {
@@ -282,7 +284,7 @@ export const navigate = async function () {
282
284
  return
283
285
  }
284
286
  // If the resolved result is false, cancel navigation
285
- if (beforeEachResult === false) {
287
+ if (beforeEachResult === false && history.length > 0) {
286
288
  preventHashChangeNavigation = true
287
289
  currentRoute = previousRoute
288
290
  window.history.back()
@@ -306,13 +308,15 @@ export const navigate = async function () {
306
308
  } catch (error) {
307
309
  Log.error('Error or Rejected Promise in "Before" Hook', error)
308
310
 
309
- preventHashChangeNavigation = true
310
- currentRoute = previousRoute
311
- window.history.back()
311
+ if (history.length > 0) {
312
+ preventHashChangeNavigation = true
313
+ currentRoute = previousRoute
314
+ window.history.back()
312
315
 
313
- navigatingBack = false
314
- state.navigating = false
315
- return
316
+ navigatingBack = false
317
+ state.navigating = false
318
+ return
319
+ }
316
320
  }
317
321
  // If the resolved result is an object, redirect if the path in the object was changed
318
322
  if (isObject(beforeHookOutput) === true && beforeHookOutput.path !== currentPath) {
@@ -321,7 +325,7 @@ export const navigate = async function () {
321
325
  return
322
326
  }
323
327
  // If the resolved result is false, cancel navigation
324
- if (beforeHookOutput === false) {
328
+ if (beforeHookOutput === false && history.length > 0) {
325
329
  preventHashChangeNavigation = true
326
330
  currentRoute = previousRoute
327
331
  window.history.back()