@lightningjs/blits 2.0.0-beta.1 → 2.0.0-beta.11
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 +169 -41
- 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
|
@@ -26,7 +26,9 @@ let currentId = null
|
|
|
26
26
|
let debounce = null
|
|
27
27
|
|
|
28
28
|
// Global default utterance options
|
|
29
|
-
let globalDefaultOptions = {
|
|
29
|
+
let globalDefaultOptions = {
|
|
30
|
+
enableUtteranceKeepAlive: !/android/i.test((window.navigator || {}).userAgent || ''),
|
|
31
|
+
}
|
|
30
32
|
|
|
31
33
|
const noopAnnouncement = {
|
|
32
34
|
then() {},
|
|
@@ -50,7 +52,6 @@ const toggle = (v) => {
|
|
|
50
52
|
|
|
51
53
|
const speak = (message, politeness = 'off', options = {}) => {
|
|
52
54
|
if (active === false) return noopAnnouncement
|
|
53
|
-
|
|
54
55
|
return addToQueue(message, politeness, false, options)
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -106,6 +107,8 @@ const addToQueue = (message, politeness, delay = false, options = {}) => {
|
|
|
106
107
|
return done
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
let currentResolveFn = null
|
|
111
|
+
|
|
109
112
|
const processQueue = async () => {
|
|
110
113
|
if (isProcessing === true || queue.length === 0) return
|
|
111
114
|
isProcessing = true
|
|
@@ -113,11 +116,13 @@ const processQueue = async () => {
|
|
|
113
116
|
const { message, resolveFn, delay, id, options = {} } = queue.shift()
|
|
114
117
|
|
|
115
118
|
currentId = id
|
|
119
|
+
currentResolveFn = resolveFn
|
|
116
120
|
|
|
117
121
|
if (delay) {
|
|
118
122
|
setTimeout(() => {
|
|
119
123
|
isProcessing = false
|
|
120
124
|
currentId = null
|
|
125
|
+
currentResolveFn = null
|
|
121
126
|
resolveFn('finished')
|
|
122
127
|
processQueue()
|
|
123
128
|
}, delay)
|
|
@@ -138,19 +143,23 @@ const processQueue = async () => {
|
|
|
138
143
|
Log.debug(`Announcer - finished speaking: "${message}" (id: ${id})`)
|
|
139
144
|
|
|
140
145
|
currentId = null
|
|
146
|
+
currentResolveFn = null
|
|
141
147
|
isProcessing = false
|
|
142
148
|
resolveFn('finished')
|
|
143
149
|
processQueue()
|
|
144
150
|
})
|
|
145
151
|
.catch((e) => {
|
|
146
|
-
currentId
|
|
147
|
-
|
|
152
|
+
if (id === currentId) {
|
|
153
|
+
currentId = null
|
|
154
|
+
currentResolveFn = null
|
|
155
|
+
isProcessing = false
|
|
156
|
+
}
|
|
148
157
|
Log.debug(`Announcer - error ("${e.error}") while speaking: "${message}" (id: ${id})`)
|
|
149
158
|
resolveFn(e.error)
|
|
150
159
|
processQueue()
|
|
151
160
|
})
|
|
152
161
|
debounce = null
|
|
153
|
-
},
|
|
162
|
+
}, 300)
|
|
154
163
|
}
|
|
155
164
|
}
|
|
156
165
|
|
|
@@ -158,13 +167,52 @@ const polite = (message, options = {}) => speak(message, 'polite', options)
|
|
|
158
167
|
|
|
159
168
|
const assertive = (message, options = {}) => speak(message, 'assertive', options)
|
|
160
169
|
|
|
170
|
+
// Clear debounce timer
|
|
171
|
+
const clearDebounceTimer = () => {
|
|
172
|
+
if (debounce !== null) {
|
|
173
|
+
clearTimeout(debounce)
|
|
174
|
+
debounce = null
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
161
178
|
const stop = () => {
|
|
179
|
+
// Clear debounce timer if speech hasn't started yet
|
|
180
|
+
clearDebounceTimer()
|
|
181
|
+
|
|
182
|
+
// Always cancel speech synthesis to ensure clean state
|
|
162
183
|
speechSynthesis.cancel()
|
|
184
|
+
|
|
185
|
+
// Store resolve function before resetting state
|
|
186
|
+
const resolveFn = currentResolveFn
|
|
187
|
+
|
|
188
|
+
// Reset state
|
|
189
|
+
currentId = null
|
|
190
|
+
currentResolveFn = null
|
|
191
|
+
isProcessing = false
|
|
192
|
+
|
|
193
|
+
// Resolve promise if there was an active utterance
|
|
194
|
+
if (resolveFn !== null) {
|
|
195
|
+
resolveFn('interrupted')
|
|
196
|
+
}
|
|
163
197
|
}
|
|
164
198
|
|
|
165
199
|
const clear = () => {
|
|
200
|
+
// Clear debounce timer
|
|
201
|
+
clearDebounceTimer()
|
|
202
|
+
|
|
203
|
+
// Resolve all pending items in queue
|
|
204
|
+
while (queue.length > 0) {
|
|
205
|
+
const item = queue.shift()
|
|
206
|
+
if (item.resolveFn) {
|
|
207
|
+
Log.debug(`Announcer - clearing queued item: "${item.message}" (id: ${item.id})`)
|
|
208
|
+
item.resolveFn('cleared')
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Reset state
|
|
213
|
+
currentId = null
|
|
214
|
+
currentResolveFn = null
|
|
166
215
|
isProcessing = false
|
|
167
|
-
queue.length = 0
|
|
168
216
|
}
|
|
169
217
|
|
|
170
218
|
const configure = (options = {}) => {
|
|
@@ -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++) {
|