@lightningjs/blits 2.0.0-beta.1 → 2.0.0-beta.4

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +2 -0
  3. package/element-test-full.txt +0 -0
  4. package/index.d.ts +106 -13
  5. package/package.json +25 -4
  6. package/scripts/prepublishOnly.js +13 -4
  7. package/scripts/prepush.js +21 -1
  8. package/src/announcer/announcer.js +50 -4
  9. package/src/announcer/speechSynthesis.js +156 -37
  10. package/src/application.js +95 -3
  11. package/src/component/base/methods.js +5 -2
  12. package/src/component/base/router.js +6 -0
  13. package/src/component/base/timeouts_intervals.js +56 -0
  14. package/src/component/setup/routes.js +5 -0
  15. package/src/component/setup/state.js +2 -0
  16. package/src/component.js +64 -24
  17. package/src/components/Circle.js +5 -3
  18. package/src/components/FPScounter.js +1591 -1577
  19. package/src/components/RouterView.js +141 -124
  20. package/src/components/Sprite.js +29 -8
  21. package/src/components/index.js +0 -2
  22. package/src/engines/L3/element.js +101 -7
  23. package/src/engines/L3/launch.js +4 -1
  24. package/src/{focus.js → focus/focus.js} +6 -18
  25. package/src/focus/helpers.js +31 -0
  26. package/src/focus/hover.js +94 -0
  27. package/src/launch.js +1 -0
  28. package/src/lib/codegenerator/generator.js +44 -8
  29. package/src/lib/lifecycle.js +34 -3
  30. package/src/lib/log.js +4 -4
  31. package/src/lib/precompiler/precompiler.js +23 -14
  32. package/src/lib/reactivity/effect.js +3 -2
  33. package/src/lib/reactivityguard/computedprops.js +86 -15
  34. package/src/lib/shaders/shaders.js +2 -1
  35. package/src/lib/symbols.js +2 -0
  36. package/src/plugins/appstate.d.ts +26 -0
  37. package/src/plugins/index.d.ts +28 -0
  38. package/src/plugins/language.d.ts +37 -0
  39. package/src/plugins/storage/storage.d.ts +31 -0
  40. package/src/plugins/theme.d.ts +43 -0
  41. package/src/plugins/theme.js +30 -3
  42. package/src/router/router.js +78 -14
  43. package/test-output.txt +0 -0
  44. package/vite/preCompiler.js +15 -5
  45. package/vscode/data/template-attributes.json +335 -86
@@ -19,33 +19,61 @@ import { Log } from '../lib/log.js'
19
19
 
20
20
  const syn = window.speechSynthesis
21
21
 
22
- const isAndroid = /android/i.test((window.navigator || {}).userAgent || '')
23
-
24
- const utterances = new Map() // Strong references with unique keys
22
+ const utterances = new Map() // id -> { utterance, timer, ignoreResume }
25
23
 
26
24
  let initialized = false
27
- let infinityTimer = null
28
25
 
29
- const clear = () => {
30
- if (infinityTimer) {
31
- clearTimeout(infinityTimer)
32
- infinityTimer = null
26
+ const clear = (id) => {
27
+ const state = utterances.get(id)
28
+ if (state === undefined) {
29
+ return
30
+ }
31
+ if (state.timer !== null) {
32
+ clearTimeout(state.timer)
33
+ state.timer = null
33
34
  }
35
+ utterances.delete(id)
34
36
  }
35
37
 
36
- const resumeInfinity = (target) => {
37
- if (!target || infinityTimer) {
38
- return clear()
38
+ const startKeepAlive = (id) => {
39
+ const state = utterances.get(id)
40
+
41
+ // utterance status: utterance was removed (cancelled or finished)
42
+ if (state == undefined) {
43
+ return
44
+ }
45
+
46
+ // Clear existing timer for this specific utterance
47
+ if (state.timer !== null) {
48
+ clearTimeout(state.timer)
49
+ state.timer = null
50
+ }
51
+
52
+ // syn status: syn might be undefined or cancelled
53
+ if (!syn) {
54
+ clear(id)
55
+ return
39
56
  }
40
57
 
41
58
  syn.pause()
42
59
  setTimeout(() => {
43
- syn.resume()
60
+ // utterance status: utterance might have been removed during setTimeout
61
+ const currentState = utterances.get(id)
62
+ if (currentState) {
63
+ currentState.ignoreResume = true
64
+ syn.resume()
65
+ }
44
66
  }, 0)
45
67
 
46
- infinityTimer = setTimeout(() => {
47
- resumeInfinity(target)
48
- }, 5000)
68
+ // Check if utterance still exists before scheduling next cycle
69
+ if (utterances.has(id) === true) {
70
+ state.timer = setTimeout(() => {
71
+ // Double-check utterance still exists before resuming
72
+ if (utterances.has(id) === true) {
73
+ startKeepAlive(id)
74
+ }
75
+ }, 5000)
76
+ }
49
77
  }
50
78
 
51
79
  const defaultUtteranceProps = {
@@ -57,45 +85,124 @@ const defaultUtteranceProps = {
57
85
  }
58
86
 
59
87
  const initialize = () => {
88
+ // syn api check: syn might not have getVoices method
89
+ if (!syn || typeof syn.getVoices !== 'function') {
90
+ initialized = false
91
+ return
92
+ }
93
+
60
94
  const voices = syn.getVoices()
61
95
  defaultUtteranceProps.voice = voices[0] || null
62
96
  initialized = true
63
97
  }
64
98
 
65
- const speak = (options) => {
66
- const utterance = new SpeechSynthesisUtterance(options.message)
99
+ const waitForSynthReady = (timeoutMs = 2000, checkIntervalMs = 100) => {
100
+ return new Promise((resolve) => {
101
+ if (!syn) {
102
+ Log.debug('SpeechSynthesis - syn unavailable')
103
+ resolve()
104
+ return
105
+ }
106
+
107
+ if (!syn.speaking && !syn.pending) {
108
+ Log.debug('SpeechSynthesis - ready immediately')
109
+ resolve()
110
+ return
111
+ }
112
+
113
+ Log.debug('SpeechSynthesis - waiting for ready state...')
114
+
115
+ const startTime = Date.now()
116
+
117
+ const intervalId = setInterval(() => {
118
+ const elapsed = Date.now() - startTime
119
+ const isReady = !syn.speaking && !syn.pending
120
+
121
+ if (isReady) {
122
+ Log.debug(`SpeechSynthesis - ready after ${elapsed}ms`)
123
+ clearInterval(intervalId)
124
+ resolve()
125
+ } else if (elapsed >= timeoutMs) {
126
+ Log.debug(`SpeechSynthesis - timeout after ${elapsed}ms, forcing ready`, {
127
+ speaking: syn.speaking,
128
+ pending: syn.pending,
129
+ })
130
+ clearInterval(intervalId)
131
+ resolve()
132
+ }
133
+ }, checkIntervalMs)
134
+ })
135
+ }
136
+
137
+ const speak = async (options) => {
138
+ // options check: missing required options
139
+ if (!options || !options.message) {
140
+ return Promise.reject({ error: 'Missing message' })
141
+ }
142
+
143
+ // options check: missing or invalid id
67
144
  const id = options.id
145
+ if (id === undefined || id === null) {
146
+ return Promise.reject({ error: 'Missing id' })
147
+ }
148
+
149
+ // utterance status: utterance with same id already exists
150
+ if (utterances.has(id)) {
151
+ clear(id)
152
+ }
153
+
154
+ // Wait for engine to be ready
155
+ await waitForSynthReady()
156
+
157
+ const utterance = new SpeechSynthesisUtterance(options.message)
68
158
  utterance.lang = options.lang || defaultUtteranceProps.lang
69
159
  utterance.pitch = options.pitch || defaultUtteranceProps.pitch
70
160
  utterance.rate = options.rate || defaultUtteranceProps.rate
71
161
  utterance.voice = options.voice || defaultUtteranceProps.voice
72
162
  utterance.volume = options.volume || defaultUtteranceProps.volume
73
- utterances.set(id, utterance) // Strong reference
74
-
75
- if (isAndroid === false) {
76
- utterance.onstart = () => {
77
- resumeInfinity(utterance)
78
- }
79
163
 
80
- utterance.onresume = () => {
81
- resumeInfinity(utterance)
82
- }
83
- }
164
+ utterances.set(id, { utterance, timer: null, ignoreResume: false })
84
165
 
85
166
  return new Promise((resolve, reject) => {
86
- utterance.onend = () => {
87
- clear()
88
- utterances.delete(id) // Cleanup
89
- resolve()
167
+ utterance.onend = (result) => {
168
+ clear(id)
169
+ resolve(result)
90
170
  }
91
171
 
92
172
  utterance.onerror = (e) => {
93
- clear()
94
- utterances.delete(id) // Cleanup
95
- reject(e)
173
+ Log.warn('SpeechSynthesisUtterance error:', e)
174
+ clear(id)
175
+ resolve()
96
176
  }
97
177
 
98
- syn.speak(utterance)
178
+ if (options.enableUtteranceKeepAlive === true) {
179
+ utterance.onstart = () => {
180
+ // utterances status: check if utterance still exists
181
+ if (utterances.has(id)) {
182
+ startKeepAlive(id)
183
+ }
184
+ }
185
+
186
+ utterance.onresume = () => {
187
+ const state = utterances.get(id)
188
+ // utterance status: utterance might have been removed
189
+ if (!state) return
190
+
191
+ if (state.ignoreResume === true) {
192
+ state.ignoreResume = false
193
+ return
194
+ }
195
+
196
+ startKeepAlive(id)
197
+ }
198
+ }
199
+ // handle error: syn.speak might throw
200
+ try {
201
+ syn.speak(utterance)
202
+ } catch (error) {
203
+ clear(id)
204
+ reject(error)
205
+ }
99
206
  })
100
207
  }
101
208
 
@@ -113,8 +220,20 @@ export default {
113
220
  },
114
221
  cancel() {
115
222
  if (syn !== undefined) {
116
- syn.cancel()
117
- clear()
223
+ // timers: clear all timers before cancelling
224
+ for (const id of utterances.keys()) {
225
+ clear(id)
226
+ }
227
+
228
+ // handle errors: syn.cancel might throw
229
+ try {
230
+ syn.cancel()
231
+ } catch (error) {
232
+ Log.error('Error cancelling speech synthesis:', error)
233
+ }
234
+
235
+ // utterances status: ensure all utterances are cleaned up
236
+ utterances.clear()
118
237
  }
119
238
  },
120
239
  // @todo
@@ -15,12 +15,14 @@
15
15
  * SPDX-License-Identifier: Apache-2.0
16
16
  */
17
17
 
18
- import Component from './component.js'
19
- import { default as Focus, keyUpCallbacks } from './focus.js'
20
- import Settings from './settings.js'
18
+ import { default as Component, componentMap } from './component.js'
19
+ import { default as Focus, keyUpCallbacks } from './focus/focus.js'
20
+ import Hover from './focus/hover.js'
21
21
 
22
+ import Settings from './settings.js'
22
23
  import symbols from './lib/symbols.js'
23
24
  import { DEFAULT_HOLD_TIMEOUT_MS, DEFAULT_KEYMAP } from './constants.js'
25
+ import { renderer } from './launch.js'
24
26
 
25
27
  /**
26
28
  * Merged keyMap (default + custom settings).
@@ -34,13 +36,24 @@ const Application = (config) => {
34
36
 
35
37
  let keyDownHandler
36
38
  let keyUpHandler
39
+ let mouseMoveHandler
40
+ let mouseClickHandler
41
+ let updateCanvasRect
37
42
  let holdTimeout
38
43
  let lastInputTime = 0
39
44
  let lastInputKey = null
45
+ let mouseListenersAdded = false
40
46
 
41
47
  config.hooks[symbols.destroy] = function () {
42
48
  document.removeEventListener('keydown', keyDownHandler)
43
49
  document.removeEventListener('keyup', keyUpHandler)
50
+ if (mouseListenersAdded) {
51
+ document.removeEventListener('mousemove', mouseMoveHandler)
52
+ document.removeEventListener('click', mouseClickHandler)
53
+ window.removeEventListener('resize', updateCanvasRect)
54
+ window.removeEventListener('scroll', updateCanvasRect)
55
+ Hover.clear()
56
+ }
44
57
  }
45
58
 
46
59
  config.hooks[symbols.init] = function () {
@@ -57,6 +70,8 @@ const Application = (config) => {
57
70
  /** @type {number} Input throttle time in milliseconds (0 = disabled) */
58
71
  const throttleMs = Settings.get('inputThrottle', 0)
59
72
 
73
+ const mouseEnabled = Settings.get('enableMouse', false)
74
+
60
75
  keyDownHandler = async (e) => {
61
76
  const currentTime = performance.now()
62
77
 
@@ -89,6 +104,7 @@ const Application = (config) => {
89
104
  }
90
105
 
91
106
  Focus.input(key, e)
107
+ if (mouseEnabled === true) Hover.clear()
92
108
  clearTimeout(holdTimeout)
93
109
  holdTimeout = setTimeout(
94
110
  () => {
@@ -108,8 +124,84 @@ const Application = (config) => {
108
124
  Focus.hold = false
109
125
  }
110
126
 
127
+ let lastMoved = 0
128
+ let currentNode = undefined
129
+ let currentComponent = undefined
130
+ let canvasRect = null
131
+
132
+ updateCanvasRect = () => {
133
+ if (renderer.canvas !== undefined) {
134
+ canvasRect = renderer.canvas.getBoundingClientRect()
135
+ }
136
+ }
137
+
138
+ // limit the amount of move events per time frame
139
+ mouseMoveHandler = (e) => {
140
+ if (e.timeStamp - lastMoved < 100) return
141
+ lastMoved = e.timeStamp
142
+
143
+ this.$emit('mouse::move', e)
144
+
145
+ if (canvasRect === null && renderer.canvas != null) {
146
+ updateCanvasRect()
147
+ }
148
+ if (canvasRect === null) {
149
+ return
150
+ }
151
+
152
+ const stage = renderer.stage
153
+ if (stage == null) {
154
+ return
155
+ }
156
+
157
+ // Convert viewport → canvas display coordinates
158
+ const x = e.clientX - canvasRect.left
159
+ const y = e.clientY - canvasRect.top
160
+
161
+ const node = stage.getNodeFromPosition({ x, y })
162
+
163
+ if (node === null) {
164
+ currentComponent = undefined
165
+ Hover.clear()
166
+ return
167
+ }
168
+ if (node === currentNode) return
169
+
170
+ currentNode = node
171
+ currentComponent = componentMap.get(currentNode)
172
+
173
+ if (currentComponent === undefined) {
174
+ Hover.clear()
175
+ return
176
+ }
177
+ Hover.set(currentComponent)
178
+ }
179
+
180
+ mouseClickHandler = () => {
181
+ if (currentComponent === undefined || currentComponent.eol === true) return
182
+
183
+ const e = new KeyboardEvent('keydown', {
184
+ key: 'Enter',
185
+ code: 'Enter',
186
+ keyCode: 13,
187
+ bubbles: true,
188
+ cancelable: true,
189
+ composed: true,
190
+ })
191
+ currentComponent.$focus()
192
+ currentComponent.$input(e)
193
+ }
194
+
111
195
  document.addEventListener('keydown', keyDownHandler)
112
196
  document.addEventListener('keyup', keyUpHandler)
197
+ if (mouseEnabled === true) {
198
+ updateCanvasRect()
199
+ document.addEventListener('mousemove', mouseMoveHandler)
200
+ document.addEventListener('click', mouseClickHandler)
201
+ window.addEventListener('resize', updateCanvasRect)
202
+ window.addEventListener('scroll', updateCanvasRect)
203
+ mouseListenersAdded = true
204
+ }
113
205
 
114
206
  // next tick
115
207
  setTimeout(() => Focus.set(this))
@@ -16,8 +16,7 @@
16
16
  */
17
17
 
18
18
  import symbols from '../../lib/symbols.js'
19
- import Focus from '../../focus.js'
20
- import { keyUpCallbacks, getComponentWithInputEvent } from '../../focus.js'
19
+ import { default as Focus, keyUpCallbacks, getComponentWithInputEvent } from '../../focus/focus.js'
21
20
  import eventListeners from '../../lib/eventListeners.js'
22
21
  import { trigger } from '../../lib/reactivity/effect.js'
23
22
  import { Log } from '../../lib/log.js'
@@ -100,6 +99,8 @@ export default {
100
99
  // pass focus to the parent so we don't get lost in focus limbo
101
100
  if (this.$hasFocus === true) this[symbols.parent].$focus()
102
101
 
102
+ // @todo - is this really necessary?
103
+ // This cause an issue with auto sizing of parent (and required an extra eol check there)
103
104
  for (let key in this[symbols.state]) {
104
105
  if (Array.isArray(this[symbols.state][key])) {
105
106
  this[symbols.state][key] = []
@@ -108,6 +109,8 @@ export default {
108
109
 
109
110
  this.$clearTimeouts()
110
111
  this.$clearIntervals()
112
+ this.$clearDebounces()
113
+
111
114
  eventListeners.removeListeners(this)
112
115
 
113
116
  const rendererEventListenersLength = this[symbols.rendererEventListeners].length
@@ -23,6 +23,12 @@ export default {
23
23
  value: {
24
24
  to,
25
25
  back,
26
+ get backNavigation() {
27
+ return state.backNavigation !== false
28
+ },
29
+ set backNavigation(enabled) {
30
+ state.backNavigation = enabled !== false
31
+ },
26
32
  get currentRoute() {
27
33
  return currentRoute
28
34
  },
@@ -96,4 +96,60 @@ export default {
96
96
  enumerable: true,
97
97
  configurable: false,
98
98
  },
99
+ $debounce: {
100
+ value: function (name, fn, ms, ...params) {
101
+ // early exit when component is marked as end of life
102
+ if (this.eol === true) return
103
+
104
+ // clear existing debounce for this name if it exists
105
+ const existing = this[symbols.debounces].get(name)
106
+ if (existing !== undefined) {
107
+ this.$clearTimeout(existing)
108
+ this[symbols.debounces].delete(name)
109
+ }
110
+
111
+ // create new timeout
112
+ const timeoutId = setTimeout(() => {
113
+ this[symbols.debounces].delete(name)
114
+ this.$clearTimeout(timeoutId)
115
+ fn.apply(this, params)
116
+ }, ms)
117
+
118
+ // track timeout in timeouts array for automatic cleanup
119
+ this[symbols.timeouts].push(timeoutId)
120
+
121
+ // store timeoutId per name to enable replace behavior and lifecycle cleanup
122
+ this[symbols.debounces].set(name, timeoutId)
123
+
124
+ return timeoutId
125
+ },
126
+ writable: false,
127
+ enumerable: true,
128
+ configurable: false,
129
+ },
130
+ $clearDebounce: {
131
+ value: function (name) {
132
+ const existing = this[symbols.debounces].get(name)
133
+ if (existing !== undefined) {
134
+ this.$clearTimeout(existing)
135
+ this[symbols.debounces].delete(name)
136
+ }
137
+ },
138
+ writable: false,
139
+ enumerable: true,
140
+ configurable: false,
141
+ },
142
+ $clearDebounces: {
143
+ value: function () {
144
+ // clear all timeouts associated with debounces
145
+ const timeoutIds = Array.from(this[symbols.debounces].values())
146
+ for (let i = 0; i < timeoutIds.length; i++) {
147
+ this.$clearTimeout(timeoutIds[i])
148
+ }
149
+ this[symbols.debounces].clear()
150
+ },
151
+ writable: false,
152
+ enumerable: true,
153
+ configurable: false,
154
+ },
99
155
  }
@@ -16,12 +16,17 @@
16
16
  */
17
17
 
18
18
  import symbols from '../../lib/symbols.js'
19
+ import { state as routerState } from '../../router/router.js'
19
20
 
20
21
  export default (component, data) => {
21
22
  let routes = data
22
23
  if (Array.isArray(data) === false) {
23
24
  component[symbols.routerHooks] = data.hooks
24
25
  routes = data.routes
26
+ // Set initial backNavigation value if provided in router config
27
+ if (data.backNavigation !== undefined) {
28
+ routerState.backNavigation = data.backNavigation !== false
29
+ }
25
30
  }
26
31
  component[symbols.routes] = []
27
32
 
@@ -36,6 +36,8 @@ export default (component, state = () => {}) => {
36
36
  // add built-in $hasFocus key
37
37
  stateKeys.push('$hasFocus')
38
38
  }
39
+ // add built-in isHovered key
40
+ stateKeys.push(['isHovered'])
39
41
  const stateKeysLength = stateKeys.length
40
42
 
41
43
  for (let i = 0; i < stateKeysLength; i++) {
package/src/component.js CHANGED
@@ -43,6 +43,9 @@ const required = (name) => {
43
43
  throw new Error(`Parameter ${name} is required`)
44
44
  }
45
45
 
46
+ // map that holds component to their holder node
47
+ export const componentMap = new WeakMap()
48
+
46
49
  /**
47
50
  * @typedef {function} BlitsComponentFactory
48
51
  * @param {object} opts - The options for the component instance
@@ -217,6 +220,10 @@ const Component = (name = required('name'), config = required('config')) => {
217
220
  // such as rotation and scale to components)
218
221
  this[symbols.holder] = parentEl
219
222
 
223
+ if (parentEl !== undefined && parentEl.node !== undefined) {
224
+ componentMap.set(parentEl.node, this)
225
+ }
226
+
220
227
  // generate an internal id (simple counter)
221
228
  this[symbols.id] = createInternalId()
222
229
 
@@ -229,6 +236,9 @@ const Component = (name = required('name'), config = required('config')) => {
229
236
  // create an empty array for storing intervals created by this component (via this.$setInterval)
230
237
  this[symbols.intervals] = []
231
238
 
239
+ // create a Map for storing debounced functions (via this.$debounce)
240
+ this[symbols.debounces] = new Map()
241
+
232
242
  // apply the state function (passing in the this reference to utilize configured props)
233
243
  // and store a reference to this original state
234
244
  this[symbols.originalState] =
@@ -236,6 +246,9 @@ const Component = (name = required('name'), config = required('config')) => {
236
246
  // add $hasFocus key in
237
247
  this[symbols.originalState]['$hasFocus'] = false
238
248
 
249
+ // add isHovered key in
250
+ this[symbols.originalState]['isHovered'] = false
251
+
239
252
  // generate a reactive state (using the result of previously execute state function)
240
253
  // and store it
241
254
  this[symbols.state] = reactive(this[symbols.originalState], Settings.get('reactivityMode'))
@@ -265,35 +278,59 @@ const Component = (name = required('name'), config = required('config')) => {
265
278
  this[symbols.slots] = this[symbols.children].filter((child) => child[symbols.isSlot])
266
279
 
267
280
  this[symbols.rendererEventListeners] = []
281
+
282
+ if (this[symbols.holder] !== undefined && Settings.get('inspector', false)) {
283
+ this[symbols.holder].setInspectorMetadata({ 'blits-hasFocus': false })
284
+ }
285
+
286
+ // setup inspector data for render state
287
+ if (name === 'App' && Settings.get('inspector', false)) {
288
+ this.$setTimeout(() => {
289
+ const wrapperEl = this[symbols.wrapper]
290
+ const idleCb = () => {
291
+ wrapperEl.setInspectorMetadata({ 'blits-renderState': 'idle' })
292
+ }
293
+ const activeCb = () => {
294
+ wrapperEl.setInspectorMetadata({ 'blits-renderState': 'active' })
295
+ }
296
+ renderer.on('idle', idleCb)
297
+ renderer.on('active', activeCb)
298
+ this[symbols.rendererEventListeners].push({ event: 'idle', cb: idleCb })
299
+ this[symbols.rendererEventListeners].push({ event: 'active', cb: activeCb })
300
+ })
301
+ }
302
+
268
303
  // register hooks if component has hooks specified
269
304
  if (config.hooks) {
270
- // push to next tick to ensure
271
- setTimeout(() => {
272
- // frame tick event
273
- if (config.hooks.frameTick) {
274
- const cb = (r, data) => emit('frameTick', this[symbols.identifier], this, [data])
275
- this[symbols.rendererEventListeners].push({ event: 'frameTick', cb })
276
- renderer.on('frameTick', cb)
277
- }
305
+ // frame tick event
306
+ if (config.hooks.frameTick) {
307
+ const cb = (_r, data) => emit('frameTick', this[symbols.identifier], this, [data])
308
+ renderer.on('frameTick', cb)
309
+ this[symbols.rendererEventListeners].push({ event: 'frameTick', cb })
310
+ }
278
311
 
279
- // idle event
280
- if (config.hooks.idle) {
281
- const cb = () => {
282
- emit('idle', this[symbols.identifier], this)
283
- }
284
- this[symbols.rendererEventListeners].push({ event: 'idle', cb })
285
- renderer.on('idle', cb)
312
+ // idle event
313
+ if (config.hooks.idle) {
314
+ const idleCb = () => {
315
+ emit('idle', this[symbols.identifier], this, [true])
316
+ }
317
+ const activeCb = () => {
318
+ emit('idle', this[symbols.identifier], this, [false])
286
319
  }
320
+ renderer.on('idle', idleCb)
321
+ renderer.on('active', activeCb)
322
+ this[symbols.rendererEventListeners].push({ event: 'idle', cb: idleCb })
323
+ this[symbols.rendererEventListeners].push({ event: 'active', cb: activeCb })
324
+ }
287
325
 
288
- // fpsUpdate event
289
- if (config.hooks.fpsUpdate) {
290
- const cb = (r, data) => {
291
- emit('fpsUpdate', this[symbols.identifier], this, [data.fps])
292
- }
293
- this[symbols.rendererEventListeners].push({ event: 'fpsUpdate', cb })
294
- renderer.on('fpsUpdate', cb)
326
+ // fpsUpdate event
327
+ if (config.hooks.fpsUpdate) {
328
+ const cb = (_r, data) => {
329
+ emit('fpsUpdate', this[symbols.identifier], this, [data.fps])
295
330
  }
296
- })
331
+ renderer.on('fpsUpdate', cb)
332
+ this[symbols.rendererEventListeners].push({ event: 'fpsUpdate', cb })
333
+ }
297
334
 
298
335
  // inBounds event emiting a lifecycle attach event
299
336
  if (config.hooks.attach) {
@@ -361,7 +398,7 @@ const Component = (name = required('name'), config = required('config')) => {
361
398
  }
362
399
  }
363
400
 
364
- let old = this[key]
401
+ let old = target[key]
365
402
 
366
403
  const eff = (force = false) => {
367
404
  const newValue = target[key]
@@ -456,6 +493,9 @@ const Component = (name = required('name'), config = required('config')) => {
456
493
  // during the code generation step
457
494
  factory[Symbol.for('config')] = config
458
495
 
496
+ // Display name for inspector
497
+ factory[Symbol.for('componentType')] = name
498
+
459
499
  // To determine whether dynamic component is actual Blits component or not
460
500
  factory[symbols.isComponent] = true
461
501