@lightningjs/blits 2.0.0-beta.1 → 2.0.0-beta.10
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 +83 -0
- package/README.md +2 -0
- package/element-test-full.txt +0 -0
- package/index.d.ts +114 -14
- package/package.json +28 -8
- package/scripts/lint.js +17 -0
- package/scripts/prepublishOnly.js +15 -6
- package/scripts/prepush.js +25 -7
- package/src/announcer/announcer.js +54 -6
- package/src/announcer/speechSynthesis.js +156 -37
- package/src/application.js +98 -3
- package/src/component/base/methods.js +5 -2
- package/src/component/base/router.js +6 -0
- package/src/component/base/timeouts_intervals.js +56 -0
- package/src/component/setup/routes.js +5 -0
- package/src/component/setup/state.js +2 -0
- package/src/component.js +64 -24
- package/src/components/Circle.js +5 -3
- package/src/components/FPScounter.js +1591 -1577
- package/src/components/RouterView.js +141 -124
- package/src/components/Sprite.js +65 -8
- package/src/components/index.js +0 -2
- package/src/constants.js +17 -0
- package/src/engine.js +17 -0
- package/src/engines/L3/element.js +101 -7
- package/src/engines/L3/fontLoader.js +17 -0
- package/src/engines/L3/index.js +17 -0
- package/src/engines/L3/launch.js +4 -1
- package/src/engines/L3/shaderLoader.js +17 -0
- package/src/engines/index.js +17 -0
- package/src/{focus.js → focus/focus.js} +6 -18
- package/src/focus/helpers.js +31 -0
- package/src/focus/hover.js +94 -0
- package/src/helpers/fetchJson.js +4 -6
- package/src/launch.js +1 -0
- package/src/lib/codegenerator/generator.js +55 -9
- package/src/lib/lifecycle.js +34 -3
- package/src/lib/log.js +4 -4
- package/src/lib/precompiler/precompiler.js +23 -14
- package/src/lib/reactivity/effect.js +3 -2
- package/src/lib/reactivityguard/computedprops.js +113 -28
- package/src/lib/shaders/shaders.js +2 -1
- package/src/lib/symbols.js +19 -0
- package/src/lib/utils.js +17 -0
- package/src/plugin.js +17 -0
- package/src/plugins/appstate.d.ts +26 -0
- package/src/plugins/index.d.ts +28 -0
- package/src/plugins/language.d.ts +37 -0
- package/src/plugins/storage/localCookie.js +4 -6
- package/src/plugins/storage/storage.d.ts +31 -0
- package/src/plugins/theme.d.ts +43 -0
- package/src/plugins/theme.js +30 -3
- package/src/router/router.js +78 -14
- package/test-output.txt +0 -0
- package/vite/inject-dev-config.js +17 -0
- package/vite/preCompiler.js +15 -5
- 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
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
resolve()
|
|
167
|
+
utterance.onend = (result) => {
|
|
168
|
+
clear(id)
|
|
169
|
+
resolve(result)
|
|
90
170
|
}
|
|
91
171
|
|
|
92
172
|
utterance.onerror = (e) => {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
173
|
+
Log.warn('SpeechSynthesisUtterance error:', e)
|
|
174
|
+
clear(id)
|
|
175
|
+
resolve()
|
|
96
176
|
}
|
|
97
177
|
|
|
98
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
package/src/application.js
CHANGED
|
@@ -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
|
|
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,27 @@ 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 () {
|
|
48
|
+
// Cancel pending key-hold timeout and reset hold state so focus is not left in hold mode after teardown
|
|
49
|
+
clearTimeout(holdTimeout)
|
|
50
|
+
Focus.hold = false
|
|
42
51
|
document.removeEventListener('keydown', keyDownHandler)
|
|
43
52
|
document.removeEventListener('keyup', keyUpHandler)
|
|
53
|
+
if (mouseListenersAdded) {
|
|
54
|
+
document.removeEventListener('mousemove', mouseMoveHandler)
|
|
55
|
+
document.removeEventListener('click', mouseClickHandler)
|
|
56
|
+
window.removeEventListener('resize', updateCanvasRect)
|
|
57
|
+
window.removeEventListener('scroll', updateCanvasRect)
|
|
58
|
+
Hover.clear()
|
|
59
|
+
}
|
|
44
60
|
}
|
|
45
61
|
|
|
46
62
|
config.hooks[symbols.init] = function () {
|
|
@@ -57,6 +73,8 @@ const Application = (config) => {
|
|
|
57
73
|
/** @type {number} Input throttle time in milliseconds (0 = disabled) */
|
|
58
74
|
const throttleMs = Settings.get('inputThrottle', 0)
|
|
59
75
|
|
|
76
|
+
const mouseEnabled = Settings.get('enableMouse', false)
|
|
77
|
+
|
|
60
78
|
keyDownHandler = async (e) => {
|
|
61
79
|
const currentTime = performance.now()
|
|
62
80
|
|
|
@@ -89,6 +107,7 @@ const Application = (config) => {
|
|
|
89
107
|
}
|
|
90
108
|
|
|
91
109
|
Focus.input(key, e)
|
|
110
|
+
if (mouseEnabled === true) Hover.clear()
|
|
92
111
|
clearTimeout(holdTimeout)
|
|
93
112
|
holdTimeout = setTimeout(
|
|
94
113
|
() => {
|
|
@@ -108,8 +127,84 @@ const Application = (config) => {
|
|
|
108
127
|
Focus.hold = false
|
|
109
128
|
}
|
|
110
129
|
|
|
130
|
+
let lastMoved = 0
|
|
131
|
+
let currentNode = undefined
|
|
132
|
+
let currentComponent = undefined
|
|
133
|
+
let canvasRect = null
|
|
134
|
+
|
|
135
|
+
updateCanvasRect = () => {
|
|
136
|
+
if (renderer.canvas !== undefined) {
|
|
137
|
+
canvasRect = renderer.canvas.getBoundingClientRect()
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// limit the amount of move events per time frame
|
|
142
|
+
mouseMoveHandler = (e) => {
|
|
143
|
+
if (e.timeStamp - lastMoved < 100) return
|
|
144
|
+
lastMoved = e.timeStamp
|
|
145
|
+
|
|
146
|
+
this.$emit('mouse::move', e)
|
|
147
|
+
|
|
148
|
+
if (canvasRect === null && renderer.canvas != null) {
|
|
149
|
+
updateCanvasRect()
|
|
150
|
+
}
|
|
151
|
+
if (canvasRect === null) {
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const stage = renderer.stage
|
|
156
|
+
if (stage == null) {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Convert viewport → canvas display coordinates
|
|
161
|
+
const x = e.clientX - canvasRect.left
|
|
162
|
+
const y = e.clientY - canvasRect.top
|
|
163
|
+
|
|
164
|
+
const node = stage.getNodeFromPosition({ x, y })
|
|
165
|
+
|
|
166
|
+
if (node === null) {
|
|
167
|
+
currentComponent = undefined
|
|
168
|
+
Hover.clear()
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
if (node === currentNode) return
|
|
172
|
+
|
|
173
|
+
currentNode = node
|
|
174
|
+
currentComponent = componentMap.get(currentNode)
|
|
175
|
+
|
|
176
|
+
if (currentComponent === undefined) {
|
|
177
|
+
Hover.clear()
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
Hover.set(currentComponent)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
mouseClickHandler = () => {
|
|
184
|
+
if (currentComponent === undefined || currentComponent.eol === true) return
|
|
185
|
+
|
|
186
|
+
const e = new KeyboardEvent('keydown', {
|
|
187
|
+
key: 'Enter',
|
|
188
|
+
code: 'Enter',
|
|
189
|
+
keyCode: 13,
|
|
190
|
+
bubbles: true,
|
|
191
|
+
cancelable: true,
|
|
192
|
+
composed: true,
|
|
193
|
+
})
|
|
194
|
+
currentComponent.$focus()
|
|
195
|
+
currentComponent.$input(e)
|
|
196
|
+
}
|
|
197
|
+
|
|
111
198
|
document.addEventListener('keydown', keyDownHandler)
|
|
112
199
|
document.addEventListener('keyup', keyUpHandler)
|
|
200
|
+
if (mouseEnabled === true) {
|
|
201
|
+
updateCanvasRect()
|
|
202
|
+
document.addEventListener('mousemove', mouseMoveHandler)
|
|
203
|
+
document.addEventListener('click', mouseClickHandler)
|
|
204
|
+
window.addEventListener('resize', updateCanvasRect)
|
|
205
|
+
window.addEventListener('scroll', updateCanvasRect)
|
|
206
|
+
mouseListenersAdded = true
|
|
207
|
+
}
|
|
113
208
|
|
|
114
209
|
// next tick
|
|
115
210
|
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
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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 =
|
|
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
|
|