@lightningjs/blits 1.45.2 → 1.47.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 +21 -1
- package/index.d.ts +55 -25
- package/package.json +2 -2
- package/scripts/prepublishOnly.js +7 -1
- package/src/announcer/announcer.js +50 -4
- package/src/announcer/speechSynthesis.js +156 -37
- package/src/component/base/methods.js +4 -0
- 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.js +3 -0
- package/src/components/Circle.js +1 -0
- package/src/components/FPScounter.js +1 -0
- package/src/components/RouterView.js +88 -78
- package/src/components/Sprite.js +1 -0
- package/src/engines/L3/element.js +60 -2
- package/src/lib/codegenerator/generator.js +22 -1
- package/src/lib/lifecycle.js +28 -2
- package/src/lib/reactivityguard/computedprops.js +79 -13
- package/src/lib/symbols.js +2 -0
- package/src/plugins/appstate.d.ts +1 -4
- package/src/plugins/index.d.ts +4 -5
- package/src/plugins/language.d.ts +7 -4
- package/src/plugins/storage/storage.d.ts +6 -4
- package/src/plugins/theme.d.ts +12 -4
- package/src/plugins/theme.js +30 -3
- package/src/router/router.js +35 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.47.0
|
|
4
|
+
|
|
5
|
+
_26 jan 2026_
|
|
6
|
+
|
|
7
|
+
- Fixed issue with reactivity guard vite plugin failing for commented-out code
|
|
8
|
+
- Fixed issue with autosizing of holder node by adding extra `eol` check
|
|
9
|
+
- Added support for _variants_ to the Theme plugin
|
|
10
|
+
|
|
11
|
+
## v1.46.0
|
|
12
|
+
|
|
13
|
+
_22 jan 2026_
|
|
14
|
+
|
|
15
|
+
- Improved Announcer on Comcast devices
|
|
16
|
+
- Added new `afterEach` router hook
|
|
17
|
+
- Added router option to dynamically disable `back`-key handling by the router
|
|
18
|
+
- Added `this.$debounce`-method
|
|
19
|
+
- Added automatic inspector data items (`$componentType`, `$hasFocus` and `$isTransitioning`)
|
|
20
|
+
- Added functionality to auto size the holder node of a Component when the wrapper Element has dimensions
|
|
21
|
+
- Bumped Lightning renderer to v2.20.4 with correct handling of failed textures
|
|
22
|
+
|
|
3
23
|
## v1.45.2
|
|
4
24
|
|
|
5
|
-
|
|
25
|
+
_13 jan 2026_
|
|
6
26
|
|
|
7
27
|
- Updated renderer to v2.20.2
|
|
8
28
|
|
package/index.d.ts
CHANGED
|
@@ -54,6 +54,12 @@ declare module '@lightningjs/blits' {
|
|
|
54
54
|
* @default 1
|
|
55
55
|
*/
|
|
56
56
|
volume?: number,
|
|
57
|
+
/**
|
|
58
|
+
* Whether to enable utterance keep-alive (prevents pausing on some platforms)
|
|
59
|
+
*
|
|
60
|
+
* @default undefined
|
|
61
|
+
*/
|
|
62
|
+
enableUtteranceKeepAlive?: boolean
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
export interface AnnouncerUtterance<T = any> extends Promise<T> {
|
|
@@ -321,6 +327,11 @@ declare module '@lightningjs/blits' {
|
|
|
321
327
|
*/
|
|
322
328
|
back(): boolean;
|
|
323
329
|
|
|
330
|
+
/**
|
|
331
|
+
* Enable or disable RouterView history navigation on Back input
|
|
332
|
+
*/
|
|
333
|
+
backNavigation: boolean;
|
|
334
|
+
|
|
324
335
|
/**
|
|
325
336
|
* Get the current route read-only
|
|
326
337
|
*/
|
|
@@ -328,17 +339,17 @@ declare module '@lightningjs/blits' {
|
|
|
328
339
|
|
|
329
340
|
/**
|
|
330
341
|
* Get the list of all routes
|
|
331
|
-
|
|
342
|
+
*/
|
|
332
343
|
readonly routes: Route[];
|
|
333
344
|
|
|
334
345
|
/**
|
|
335
346
|
* Get navigating state
|
|
336
|
-
|
|
347
|
+
*/
|
|
337
348
|
readonly navigating: boolean;
|
|
338
349
|
|
|
339
350
|
/**
|
|
340
351
|
* Reactive router state
|
|
341
|
-
|
|
352
|
+
*/
|
|
342
353
|
state: {
|
|
343
354
|
/**
|
|
344
355
|
* Path of the current route
|
|
@@ -371,28 +382,6 @@ declare module '@lightningjs/blits' {
|
|
|
371
382
|
// Empty by design: extend in your app via TypeScript module augmentation.
|
|
372
383
|
}
|
|
373
384
|
|
|
374
|
-
export interface LanguagePlugin {
|
|
375
|
-
translate(key: string, ...replacements: any[]): string
|
|
376
|
-
readonly language: string
|
|
377
|
-
set(language: string): void
|
|
378
|
-
translations(translationsObject: Record<string, unknown>): void
|
|
379
|
-
load(file: string): Promise<void>
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
export interface ThemePlugin {
|
|
383
|
-
get<T = unknown>(key: string): T | undefined
|
|
384
|
-
get<T>(key: string, fallback: T): T
|
|
385
|
-
set(theme: string): void
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
export interface StoragePlugin {
|
|
389
|
-
get<T = unknown>(key: string): T | null
|
|
390
|
-
set(key: string, value: unknown): boolean
|
|
391
|
-
remove(key: string): void
|
|
392
|
-
clear(): void
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
export type AppStatePlugin<TState extends Record<string, unknown> = Record<string, unknown>> = TState
|
|
396
385
|
|
|
397
386
|
export interface ComponentBase extends CustomComponentProperties {
|
|
398
387
|
/**
|
|
@@ -444,6 +433,26 @@ declare module '@lightningjs/blits' {
|
|
|
444
433
|
*/
|
|
445
434
|
$clearTimeout: (id: ReturnType<typeof setTimeout>) => void
|
|
446
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Debounce a function execution, preventing memory leaks and function re-allocation
|
|
438
|
+
* @param name - Unique identifier for this debounce instance (unique per component instance)
|
|
439
|
+
* @param callback - Function to debounce
|
|
440
|
+
* @param ms - Delay in milliseconds
|
|
441
|
+
* @param args - Arguments to pass to the callback
|
|
442
|
+
*/
|
|
443
|
+
$debounce: (name: string, callback: (...args: any[]) => void, ms?: number, ...args: any[]) => ReturnType<typeof setTimeout>
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Clear a specific debounce by name
|
|
447
|
+
* @param name - The name of the debounce to clear
|
|
448
|
+
*/
|
|
449
|
+
$clearDebounce: (name: string) => void
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Clear all debounces registered on the component (automatically called on component destroy)
|
|
453
|
+
*/
|
|
454
|
+
$clearDebounces: () => void
|
|
455
|
+
|
|
447
456
|
/**
|
|
448
457
|
* Set an interval that is automatically cleaned upon component destroy
|
|
449
458
|
*/
|
|
@@ -658,6 +667,7 @@ declare module '@lightningjs/blits' {
|
|
|
658
667
|
export interface RouterHooks {
|
|
659
668
|
init?: () => Promise<void> | void;
|
|
660
669
|
beforeEach?: (to: Route, from: Route) => string | Route | Promise<string | Route> | void;
|
|
670
|
+
afterEach?: (to: Route, from: Route) => string | Route | Promise<string | Route> | void;
|
|
661
671
|
error?: (err: string) => string | Route | Promise<string | Route> | void;
|
|
662
672
|
}
|
|
663
673
|
|
|
@@ -681,6 +691,25 @@ declare module '@lightningjs/blits' {
|
|
|
681
691
|
* ```
|
|
682
692
|
*/
|
|
683
693
|
routes?: Route[]
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Enable or disable RouterView history navigation on Back input
|
|
697
|
+
*
|
|
698
|
+
* @default true
|
|
699
|
+
*
|
|
700
|
+
* @remarks
|
|
701
|
+
* This is an app-wide setting that affects all RouterView instances in your application.
|
|
702
|
+
* The router state is global and shared across all router instances.
|
|
703
|
+
*
|
|
704
|
+
* @example
|
|
705
|
+
* ```js
|
|
706
|
+
* router: {
|
|
707
|
+
* backNavigation: false, // Disable automatic back navigation
|
|
708
|
+
* routes: [...]
|
|
709
|
+
* }
|
|
710
|
+
* ```
|
|
711
|
+
*/
|
|
712
|
+
backNavigation?: boolean
|
|
684
713
|
}
|
|
685
714
|
|
|
686
715
|
export type ApplicationConfig<P extends Props, S, M, C, W> = ComponentConfig<P, S, M, C, W> & (
|
|
@@ -781,6 +810,7 @@ declare module '@lightningjs/blits' {
|
|
|
781
810
|
|
|
782
811
|
export interface RouteHooks {
|
|
783
812
|
before?: (to: Route, from: Route) => string | Route | Promise<string | Route>;
|
|
813
|
+
after?: (to: Route, from: Route) => string | Route | Promise<string | Route>;
|
|
784
814
|
}
|
|
785
815
|
|
|
786
816
|
export type Route = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightningjs/blits",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.47.0",
|
|
4
4
|
"description": "Blits: The Lightning 3 App Development Framework",
|
|
5
5
|
"bin": "bin/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
76
|
"@lightningjs/msdf-generator": "^1.2.0",
|
|
77
|
-
"@lightningjs/renderer": "^2.20.
|
|
77
|
+
"@lightningjs/renderer": "^2.20.4",
|
|
78
78
|
"magic-string": "^0.30.21"
|
|
79
79
|
},
|
|
80
80
|
"repository": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import fs from 'fs'
|
|
19
19
|
import path from 'path'
|
|
20
|
+
import { fileURLToPath } from 'url'
|
|
20
21
|
import compiler from '../src/lib/precompiler/precompiler.js'
|
|
21
22
|
import { exec } from 'child_process'
|
|
22
23
|
|
|
@@ -84,4 +85,9 @@ function formatFileWithESLint(filePath) {
|
|
|
84
85
|
})
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
// Only run if this file is executed directly (not imported)
|
|
89
|
+
if (fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
90
|
+
precompileComponents()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export { precompileComponents, processDirectory, processFile, formatFileWithESLint }
|
|
@@ -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,21 @@ 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
152
|
currentId = null
|
|
153
|
+
currentResolveFn = null
|
|
147
154
|
isProcessing = false
|
|
148
155
|
Log.debug(`Announcer - error ("${e.error}") while speaking: "${message}" (id: ${id})`)
|
|
149
156
|
resolveFn(e.error)
|
|
150
157
|
processQueue()
|
|
151
158
|
})
|
|
152
159
|
debounce = null
|
|
153
|
-
},
|
|
160
|
+
}, 300)
|
|
154
161
|
}
|
|
155
162
|
}
|
|
156
163
|
|
|
@@ -158,13 +165,52 @@ const polite = (message, options = {}) => speak(message, 'polite', options)
|
|
|
158
165
|
|
|
159
166
|
const assertive = (message, options = {}) => speak(message, 'assertive', options)
|
|
160
167
|
|
|
168
|
+
// Clear debounce timer
|
|
169
|
+
const clearDebounceTimer = () => {
|
|
170
|
+
if (debounce !== null) {
|
|
171
|
+
clearTimeout(debounce)
|
|
172
|
+
debounce = null
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
161
176
|
const stop = () => {
|
|
177
|
+
// Clear debounce timer if speech hasn't started yet
|
|
178
|
+
clearDebounceTimer()
|
|
179
|
+
|
|
180
|
+
// Always cancel speech synthesis to ensure clean state
|
|
162
181
|
speechSynthesis.cancel()
|
|
182
|
+
|
|
183
|
+
// Store resolve function before resetting state
|
|
184
|
+
const resolveFn = currentResolveFn
|
|
185
|
+
|
|
186
|
+
// Reset state
|
|
187
|
+
currentId = null
|
|
188
|
+
currentResolveFn = null
|
|
189
|
+
isProcessing = false
|
|
190
|
+
|
|
191
|
+
// Resolve promise if there was an active utterance
|
|
192
|
+
if (resolveFn !== null) {
|
|
193
|
+
resolveFn('interrupted')
|
|
194
|
+
}
|
|
163
195
|
}
|
|
164
196
|
|
|
165
197
|
const clear = () => {
|
|
198
|
+
// Clear debounce timer
|
|
199
|
+
clearDebounceTimer()
|
|
200
|
+
|
|
201
|
+
// Resolve all pending items in queue
|
|
202
|
+
while (queue.length > 0) {
|
|
203
|
+
const item = queue.shift()
|
|
204
|
+
if (item.resolveFn) {
|
|
205
|
+
Log.debug(`Announcer - clearing queued item: "${item.message}" (id: ${item.id})`)
|
|
206
|
+
item.resolveFn('cleared')
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Reset state
|
|
211
|
+
currentId = null
|
|
212
|
+
currentResolveFn = null
|
|
166
213
|
isProcessing = false
|
|
167
|
-
queue.length = 0
|
|
168
214
|
}
|
|
169
215
|
|
|
170
216
|
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
|
|
@@ -112,6 +112,8 @@ export default {
|
|
|
112
112
|
// pass focus to the parent so we don't get lost in focus limbo
|
|
113
113
|
if (this.hasFocus === true) this.parent.$focus()
|
|
114
114
|
|
|
115
|
+
// @todo - is this really necessary?
|
|
116
|
+
// This cause an issue with auto sizing of parent (and required an extra eol check there)
|
|
115
117
|
for (let key in this[symbols.state]) {
|
|
116
118
|
if (Array.isArray(this[symbols.state][key])) {
|
|
117
119
|
this[symbols.state][key] = []
|
|
@@ -120,6 +122,8 @@ export default {
|
|
|
120
122
|
|
|
121
123
|
this.$clearTimeouts()
|
|
122
124
|
this.$clearIntervals()
|
|
125
|
+
this.$clearDebounces()
|
|
126
|
+
|
|
123
127
|
eventListeners.removeListeners(this)
|
|
124
128
|
|
|
125
129
|
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
|
|
package/src/component.js
CHANGED
|
@@ -228,6 +228,9 @@ const Component = (name = required('name'), config = required('config')) => {
|
|
|
228
228
|
// create an empty array for storing intervals created by this component (via this.$setInterval)
|
|
229
229
|
this[symbols.intervals] = []
|
|
230
230
|
|
|
231
|
+
// create a Map for storing debounced functions (via this.$debounce)
|
|
232
|
+
this[symbols.debounces] = new Map()
|
|
233
|
+
|
|
231
234
|
// apply the state function (passing in the this reference to utilize configured props)
|
|
232
235
|
// and store a reference to this original state
|
|
233
236
|
this[symbols.originalState] =
|