@lightningjs/blits 1.45.1 → 1.46.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 +18 -0
- 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 +2 -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 +52 -2
- package/src/lib/codegenerator/generator.js +22 -1
- package/src/lib/lifecycle.js +28 -2
- 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 +5 -4
- package/src/router/router.js +38 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.46.0
|
|
4
|
+
|
|
5
|
+
_22 jan 2026_
|
|
6
|
+
|
|
7
|
+
- Improved Announcer on Comcast devices
|
|
8
|
+
- Added new `afterEach` router hook
|
|
9
|
+
- Added router option to dynamically disable `back`-key handling by the router
|
|
10
|
+
- Added `this.$debounce`-method
|
|
11
|
+
- Added automatic inspector data items (`$componentType`, `$hasFocus` and `$isTransitioning`)
|
|
12
|
+
- Added functionality to auto size the holder node of a Component when the wrapper Element has dimensions
|
|
13
|
+
- Bumped Lightning renderer to v2.20.4 with correct handling of failed textures
|
|
14
|
+
|
|
15
|
+
## v1.45.2
|
|
16
|
+
|
|
17
|
+
_13 jan 2026_
|
|
18
|
+
|
|
19
|
+
- Updated renderer to v2.20.2
|
|
20
|
+
|
|
3
21
|
## v1.45.1
|
|
4
22
|
|
|
5
23
|
_29 dec 2025_
|
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.46.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
|
|
@@ -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] =
|
package/src/components/Circle.js
CHANGED
|
@@ -16,96 +16,106 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import Component from '../component.js'
|
|
19
|
-
import Router from '../router/router.js'
|
|
19
|
+
import Router, { state as routerState } from '../router/router.js'
|
|
20
20
|
import symbols from '../lib/symbols.js'
|
|
21
21
|
import Focus from '../focus.js'
|
|
22
22
|
|
|
23
23
|
let hashchangeHandler = null
|
|
24
24
|
|
|
25
|
+
/** @typedef {{ $input?: (event: any) => boolean, $focus?: (event: any) => void }} RouterViewParent */
|
|
26
|
+
|
|
25
27
|
export default () =>
|
|
26
|
-
Component(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
28
|
+
Component(
|
|
29
|
+
'RouterView',
|
|
30
|
+
/** @type {any} */ ({
|
|
31
|
+
/* eslint-disable no-unused-vars */
|
|
32
|
+
code: {
|
|
33
|
+
render: function anonymous(parent, component, context, components, effect, getRaw, Log) {
|
|
34
|
+
const elms = []
|
|
35
|
+
const elementConfigs = []
|
|
36
|
+
const forloops = []
|
|
37
|
+
const props = []
|
|
38
|
+
const created = []
|
|
39
|
+
const effects = {}
|
|
40
|
+
const skips = []
|
|
41
|
+
let componentType
|
|
42
|
+
let rootComponent = component
|
|
43
|
+
let propData
|
|
44
|
+
let slotComponent
|
|
45
|
+
let inSlot = false
|
|
46
|
+
let slotChildCounter = 0
|
|
47
|
+
let cmps = []
|
|
48
|
+
elementConfigs[0] = {}
|
|
49
|
+
elementConfigs[0]['___wrapper'] = true
|
|
45
50
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
elms[0] = this.element(
|
|
52
|
+
{ parent: parent || 'root' },
|
|
53
|
+
inSlot === true ? slotComponent : component
|
|
54
|
+
)
|
|
50
55
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
elementConfigs[0]['w'] = parent.node.width * (100 / 100)
|
|
57
|
+
elementConfigs[0]['height'] = parent.node.height * (100 / 100)
|
|
58
|
+
elms[0].populate(elementConfigs[0])
|
|
54
59
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
if (inSlot === true) {
|
|
61
|
+
slotChildCounter -= 1
|
|
62
|
+
}
|
|
58
63
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
64
|
+
return {
|
|
65
|
+
elms,
|
|
66
|
+
cleanup: () => {
|
|
67
|
+
rootComponent = null
|
|
68
|
+
propData = null
|
|
69
|
+
slotComponent = null
|
|
70
|
+
parent = null
|
|
71
|
+
component = null
|
|
72
|
+
cmps.length = 0
|
|
73
|
+
elms.length = 0
|
|
74
|
+
components.length = 0
|
|
75
|
+
elementConfigs.length = 0
|
|
76
|
+
forloops.length = 0
|
|
77
|
+
props.length = 0
|
|
78
|
+
skips.length = 0
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
effects: [],
|
|
83
|
+
context: {},
|
|
76
84
|
},
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
state() {
|
|
81
|
-
return {
|
|
82
|
-
activeView: null,
|
|
83
|
-
}
|
|
84
|
-
},
|
|
85
|
-
hooks: {
|
|
86
|
-
async ready() {
|
|
87
|
-
if (this.parent[symbols.routerHooks] && this.parent[symbols.routerHooks].init) {
|
|
88
|
-
await this.parent[symbols.routerHooks].init.apply(this.parent)
|
|
85
|
+
state() {
|
|
86
|
+
return {
|
|
87
|
+
activeView: null,
|
|
89
88
|
}
|
|
90
|
-
hashchangeHandler = () => Router.navigate.apply(this)
|
|
91
|
-
Router.navigate.apply(this)
|
|
92
|
-
window.addEventListener('hashchange', hashchangeHandler)
|
|
93
|
-
},
|
|
94
|
-
destroy() {
|
|
95
|
-
window.removeEventListener('hashchange', hashchangeHandler, false)
|
|
96
89
|
},
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
this.
|
|
100
|
-
|
|
90
|
+
hooks: {
|
|
91
|
+
async ready() {
|
|
92
|
+
if (this.parent[symbols.routerHooks] && this.parent[symbols.routerHooks].init) {
|
|
93
|
+
await this.parent[symbols.routerHooks].init.apply(this.parent)
|
|
94
|
+
}
|
|
95
|
+
hashchangeHandler = () => Router.navigate.apply(this)
|
|
96
|
+
Router.navigate.apply(this)
|
|
97
|
+
window.addEventListener('hashchange', hashchangeHandler)
|
|
98
|
+
},
|
|
99
|
+
destroy() {
|
|
100
|
+
window.removeEventListener('hashchange', hashchangeHandler, false)
|
|
101
|
+
},
|
|
102
|
+
focus() {
|
|
103
|
+
if (this.activeView && Focus.get() === this) {
|
|
104
|
+
this.activeView.$focus()
|
|
105
|
+
}
|
|
106
|
+
},
|
|
101
107
|
},
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
input: {
|
|
109
|
+
back(e) {
|
|
110
|
+
if (routerState.backNavigation === false) {
|
|
111
|
+
this.parent.$input(e)
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
const navigating = Router.back.call(this)
|
|
115
|
+
if (navigating === false) {
|
|
116
|
+
this.parent.$focus(e)
|
|
117
|
+
}
|
|
118
|
+
},
|
|
109
119
|
},
|
|
110
|
-
}
|
|
111
|
-
|
|
120
|
+
})
|
|
121
|
+
)
|
package/src/components/Sprite.js
CHANGED
|
@@ -243,15 +243,21 @@ const propsTransformer = {
|
|
|
243
243
|
},
|
|
244
244
|
set w(v) {
|
|
245
245
|
this.props['width'] = parsePercentage.call(this, v, 'width')
|
|
246
|
+
if (this.___wrapper === true && this.element.component[symbols.holder] !== undefined) {
|
|
247
|
+
this.element.component[symbols.holder].set('w', this.props['width'])
|
|
248
|
+
}
|
|
246
249
|
},
|
|
247
250
|
set width(v) {
|
|
248
|
-
this.
|
|
251
|
+
this.w = v
|
|
249
252
|
},
|
|
250
253
|
set h(v) {
|
|
251
254
|
this.props['height'] = parsePercentage.call(this, v, 'height')
|
|
255
|
+
if (this.___wrapper === true && this.element.component[symbols.holder] !== undefined) {
|
|
256
|
+
this.element.component[symbols.holder].set('h', this.props['height'])
|
|
257
|
+
}
|
|
252
258
|
},
|
|
253
259
|
set height(v) {
|
|
254
|
-
this.
|
|
260
|
+
this.h = v
|
|
255
261
|
},
|
|
256
262
|
set x(v) {
|
|
257
263
|
this.props['x'] = parsePercentage.call(this, v, 'width')
|
|
@@ -564,6 +570,39 @@ const Element = {
|
|
|
564
570
|
})
|
|
565
571
|
}
|
|
566
572
|
},
|
|
573
|
+
/**
|
|
574
|
+
* Sets framework-provided inspector metadata
|
|
575
|
+
* Only sets if inspector is enabled and in dev mode
|
|
576
|
+
* @param {Object} data - Framework inspector metadata to merge
|
|
577
|
+
*/
|
|
578
|
+
setInspectorMetadata(data) {
|
|
579
|
+
// Early return if inspector not enabled (performance optimization)
|
|
580
|
+
if (inspectorEnabled !== true) {
|
|
581
|
+
return
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Early return if element is destroyed (props.props is null)
|
|
585
|
+
if (this.props.props === undefined || this.props.props === null) {
|
|
586
|
+
return
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Initialize data object if it doesn't exist
|
|
590
|
+
if (this.props['data'] === undefined) {
|
|
591
|
+
this.props['data'] = {}
|
|
592
|
+
}
|
|
593
|
+
if (this.props.props['data'] === undefined) {
|
|
594
|
+
this.props.props['data'] = {}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Merge framework data (with $ prefix to prevent collisions)
|
|
598
|
+
Object.assign(this.props['data'], data)
|
|
599
|
+
Object.assign(this.props.props['data'], data)
|
|
600
|
+
|
|
601
|
+
// Sync to renderer node so inspector can see it
|
|
602
|
+
if (this.node !== undefined && this.node !== null) {
|
|
603
|
+
this.node.data = { ...this.props.props['data'] }
|
|
604
|
+
}
|
|
605
|
+
},
|
|
567
606
|
/**
|
|
568
607
|
* Set an individual property on the node
|
|
569
608
|
*
|
|
@@ -651,6 +690,11 @@ const Element = {
|
|
|
651
690
|
f,
|
|
652
691
|
}
|
|
653
692
|
|
|
693
|
+
// Update inspector metadata when transition starts
|
|
694
|
+
if (inspectorEnabled === true) {
|
|
695
|
+
this.setInspectorMetadata({ $isTransitioning: true })
|
|
696
|
+
}
|
|
697
|
+
|
|
654
698
|
if (transition.start !== undefined && typeof transition.start === 'function') {
|
|
655
699
|
// fire transition start callback when animation really starts (depending on specified delay)
|
|
656
700
|
f.once('animating', () => {
|
|
@@ -685,6 +729,12 @@ const Element = {
|
|
|
685
729
|
}
|
|
686
730
|
// remove the prop from scheduled transitions
|
|
687
731
|
delete this.scheduledTransitions[prop]
|
|
732
|
+
// Update inspector metadata when transition ends
|
|
733
|
+
if (inspectorEnabled === true) {
|
|
734
|
+
this.setInspectorMetadata({
|
|
735
|
+
$isTransitioning: Object.keys(this.scheduledTransitions).length > 0,
|
|
736
|
+
})
|
|
737
|
+
}
|
|
688
738
|
})
|
|
689
739
|
|
|
690
740
|
// start animation
|
|
@@ -151,6 +151,10 @@ const generateElementCode = function (
|
|
|
151
151
|
|
|
152
152
|
renderCode.push(`elementConfigs[${counter}] = {}`)
|
|
153
153
|
|
|
154
|
+
if (counter === 0) {
|
|
155
|
+
renderCode.push(`elementConfigs[${counter}]['___wrapper'] = true `)
|
|
156
|
+
}
|
|
157
|
+
|
|
154
158
|
if (options.forloop) {
|
|
155
159
|
renderCode.push(`if(${elm} === undefined) {`)
|
|
156
160
|
}
|
|
@@ -283,9 +287,11 @@ const generateComponentCode = function (
|
|
|
283
287
|
|
|
284
288
|
const children = templateObject.children
|
|
285
289
|
delete templateObject.children
|
|
290
|
+
// Capture holder counter before generating element code (which may process children and increment counter)
|
|
291
|
+
const holderCounter = counter
|
|
286
292
|
generateElementCode.call(this, templateObject, parent, { ...options, ...{ holder: true } })
|
|
287
293
|
|
|
288
|
-
parent = options.key ? `elms[${
|
|
294
|
+
parent = options.key ? `elms[${holderCounter}][${options.key}]` : `elms[${holderCounter}]`
|
|
289
295
|
|
|
290
296
|
counter++
|
|
291
297
|
|
|
@@ -354,6 +360,21 @@ const generateComponentCode = function (
|
|
|
354
360
|
}
|
|
355
361
|
`)
|
|
356
362
|
|
|
363
|
+
// For forloops, this code runs per instance, setting metadata for each component instance
|
|
364
|
+
if (isDev === true) {
|
|
365
|
+
const componentType = templateObject[Symbol.for('componentType')]
|
|
366
|
+
const holderElm = options.key
|
|
367
|
+
? `elms[${holderCounter}][${options.key}]`
|
|
368
|
+
: `elms[${holderCounter}]`
|
|
369
|
+
renderCode.push(`
|
|
370
|
+
if (${holderElm} !== undefined && typeof ${holderElm}.setInspectorMetadata === 'function') {
|
|
371
|
+
${holderElm}.setInspectorMetadata({
|
|
372
|
+
$componentType: '${componentType}'
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
`)
|
|
376
|
+
}
|
|
377
|
+
|
|
357
378
|
this.cleanupCode.push(`components[${counter}] = null`)
|
|
358
379
|
|
|
359
380
|
if (options.forloop) {
|
package/src/lib/lifecycle.js
CHANGED
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
import { Log } from './log.js'
|
|
19
19
|
import { emit, privateEmit } from './hooks.js'
|
|
20
20
|
import symbols from './symbols.js'
|
|
21
|
+
import Settings from '../settings.js'
|
|
22
|
+
|
|
23
|
+
let inspectorEnabled = null
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
* List of valid lifecycle states for a component.
|
|
@@ -82,8 +85,31 @@ export default {
|
|
|
82
85
|
// emit 'public' hook
|
|
83
86
|
emit(v, this.component[symbols.identifier], this.component)
|
|
84
87
|
// update the built-in hasFocus state variable
|
|
85
|
-
if (v === 'focus'
|
|
86
|
-
|
|
88
|
+
if (v === 'focus' || v === 'unfocus') {
|
|
89
|
+
if (inspectorEnabled === null) {
|
|
90
|
+
inspectorEnabled = Settings.get('inspector', false)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (v === 'focus') {
|
|
94
|
+
this.component[symbols.state].hasFocus = true
|
|
95
|
+
if (
|
|
96
|
+
inspectorEnabled === true &&
|
|
97
|
+
this.component[symbols.holder] &&
|
|
98
|
+
typeof this.component[symbols.holder].setInspectorMetadata === 'function'
|
|
99
|
+
) {
|
|
100
|
+
this.component[symbols.holder].setInspectorMetadata({ $hasFocus: true })
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (v === 'unfocus') {
|
|
104
|
+
this.component[symbols.state].hasFocus = false
|
|
105
|
+
if (
|
|
106
|
+
inspectorEnabled === true &&
|
|
107
|
+
this.component[symbols.holder] &&
|
|
108
|
+
typeof this.component[symbols.holder].setInspectorMetadata === 'function'
|
|
109
|
+
) {
|
|
110
|
+
this.component[symbols.holder].setInspectorMetadata({ $hasFocus: false })
|
|
111
|
+
}
|
|
112
|
+
}
|
|
87
113
|
}
|
|
88
114
|
},
|
|
89
115
|
}
|
package/src/lib/symbols.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* @property {symbol} inputEvents
|
|
18
18
|
* @property {symbol} internalEvent
|
|
19
19
|
* @property {symbol} intervals
|
|
20
|
+
* @property {symbol} debounces
|
|
20
21
|
* @property {symbol} isProxy
|
|
21
22
|
* @property {symbol} launched
|
|
22
23
|
* @property {symbol} level
|
|
@@ -70,6 +71,7 @@ export default {
|
|
|
70
71
|
inputEvents: Symbol('inputEvents'),
|
|
71
72
|
internalEvent: Symbol('internalEvent'),
|
|
72
73
|
intervals: Symbol('intervals'),
|
|
74
|
+
debounces: Symbol('debounces'),
|
|
73
75
|
isProxy: Symbol('isProxy'),
|
|
74
76
|
launched: Symbol('launched'),
|
|
75
77
|
level: Symbol('level'),
|
|
@@ -15,10 +15,7 @@
|
|
|
15
15
|
* SPDX-License-Identifier: Apache-2.0
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// Re-export AppStatePlugin for direct imports
|
|
21
|
-
export type { AppStatePlugin }
|
|
18
|
+
export type AppStatePlugin<TState extends Record<string, unknown> = Record<string, unknown>> = TState
|
|
22
19
|
|
|
23
20
|
declare const appState: {
|
|
24
21
|
readonly name: 'appState'
|
package/src/plugins/index.d.ts
CHANGED
|
@@ -21,9 +21,8 @@ export { default as appState } from './appstate.js'
|
|
|
21
21
|
export { default as storage } from './storage/storage.js'
|
|
22
22
|
|
|
23
23
|
// Re-export plugin interfaces for convenience
|
|
24
|
-
export type { LanguagePlugin,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
export type {
|
|
28
|
-
export type { ThemePluginConfig } from './theme.js'
|
|
24
|
+
export type { LanguagePlugin, LanguagePluginOptions } from './language.js'
|
|
25
|
+
export type { ThemePlugin, ThemePluginConfig } from './theme.js'
|
|
26
|
+
export type { StoragePlugin } from './storage/storage.js'
|
|
27
|
+
export type { AppStatePlugin } from './appstate.js'
|
|
29
28
|
|
|
@@ -15,16 +15,19 @@
|
|
|
15
15
|
* SPDX-License-Identifier: Apache-2.0
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
export interface LanguagePlugin {
|
|
19
|
+
translate(key: string, ...replacements: any[]): string
|
|
20
|
+
readonly language: string
|
|
21
|
+
set(language: string): void
|
|
22
|
+
translations(translationsObject: Record<string, unknown>): void
|
|
23
|
+
load(file: string): Promise<void>
|
|
24
|
+
}
|
|
19
25
|
|
|
20
26
|
export interface LanguagePluginOptions {
|
|
21
27
|
file?: string
|
|
22
28
|
language?: string
|
|
23
29
|
}
|
|
24
30
|
|
|
25
|
-
// Re-export LanguagePlugin for direct imports
|
|
26
|
-
export type { LanguagePlugin }
|
|
27
|
-
|
|
28
31
|
declare const language: {
|
|
29
32
|
readonly name: 'language'
|
|
30
33
|
plugin: (options?: LanguagePluginOptions) => LanguagePlugin
|
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
* SPDX-License-Identifier: Apache-2.0
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
export interface StoragePlugin {
|
|
19
|
+
get<T = unknown>(key: string): T | null
|
|
20
|
+
set(key: string, value: unknown): boolean
|
|
21
|
+
remove(key: string): void
|
|
22
|
+
clear(): void
|
|
23
|
+
}
|
|
22
24
|
|
|
23
25
|
declare const storage: {
|
|
24
26
|
readonly name: 'storage'
|
package/src/plugins/theme.d.ts
CHANGED
|
@@ -15,7 +15,11 @@
|
|
|
15
15
|
* SPDX-License-Identifier: Apache-2.0
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
export interface ThemePlugin {
|
|
19
|
+
get<T = unknown>(key: string): T | undefined
|
|
20
|
+
get<T>(key: string, fallback: T): T
|
|
21
|
+
set(theme: string): void
|
|
22
|
+
}
|
|
19
23
|
|
|
20
24
|
export interface ThemePluginConfig {
|
|
21
25
|
themes?: Record<string, Record<string, unknown>>
|
|
@@ -23,9 +27,6 @@ export interface ThemePluginConfig {
|
|
|
23
27
|
base?: string
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
// Re-export ThemePlugin for direct imports
|
|
27
|
-
export type { ThemePlugin }
|
|
28
|
-
|
|
29
30
|
declare const theme: {
|
|
30
31
|
readonly name: 'theme'
|
|
31
32
|
plugin: (config?: ThemePluginConfig | Record<string, unknown>) => ThemePlugin
|
package/src/router/router.js
CHANGED
|
@@ -30,6 +30,9 @@ import Settings from '../settings.js'
|
|
|
30
30
|
* @typedef {import('../component.js').BlitsComponent} BlitsComponent - The element of the route
|
|
31
31
|
* @typedef {import('../engines/L3/element.js').BlitsElement} BlitsElement - The element of the route
|
|
32
32
|
*
|
|
33
|
+
* @typedef {BlitsComponent|BlitsComponentFactory} RouteView
|
|
34
|
+
* @typedef {RouteView & { default?: BlitsComponentFactory }} RouteViewWithOptionalDefault
|
|
35
|
+
*
|
|
33
36
|
* @typedef {Object} Route
|
|
34
37
|
* @property {string} path - The path of the route
|
|
35
38
|
* @property {string} hash - The hash of the route
|
|
@@ -57,6 +60,7 @@ export const state = reactive(
|
|
|
57
60
|
data: null,
|
|
58
61
|
params: null,
|
|
59
62
|
hash: '',
|
|
63
|
+
backNavigation: true,
|
|
60
64
|
},
|
|
61
65
|
Settings.get('reactivityMode'),
|
|
62
66
|
true
|
|
@@ -335,6 +339,7 @@ export const navigate = async function () {
|
|
|
335
339
|
return
|
|
336
340
|
}
|
|
337
341
|
}
|
|
342
|
+
|
|
338
343
|
// add the previous route (technically still the current route at this point)
|
|
339
344
|
// into the history stack when inHistory is true and we're not navigating back
|
|
340
345
|
if (
|
|
@@ -354,6 +359,7 @@ export const navigate = async function () {
|
|
|
354
359
|
/** @type {import('../engines/L3/element.js').BlitsElement} */
|
|
355
360
|
let holder
|
|
356
361
|
|
|
362
|
+
/** @type {RouteViewWithOptionalDefault|undefined|null} */
|
|
357
363
|
let view
|
|
358
364
|
let focus
|
|
359
365
|
// when navigating back let's see if we're navigating back to a route that was kept alive
|
|
@@ -476,15 +482,17 @@ export const navigate = async function () {
|
|
|
476
482
|
}
|
|
477
483
|
|
|
478
484
|
let shouldAnimate = false
|
|
485
|
+
// Declare oldView in broader scope so it can be used in hooks below
|
|
486
|
+
let oldView = null
|
|
479
487
|
|
|
480
488
|
// apply out out transition on previous view if available, unless
|
|
481
489
|
// we're reusing the prvious page component
|
|
482
490
|
if (previousRoute !== undefined && reuse === false) {
|
|
483
491
|
// only animate when there is a previous route
|
|
484
492
|
shouldAnimate = true
|
|
485
|
-
|
|
493
|
+
oldView = this[symbols.children].splice(1, 1).pop()
|
|
486
494
|
if (oldView) {
|
|
487
|
-
removeView(previousRoute, oldView, route.transition.out, navigatingBack)
|
|
495
|
+
await removeView(previousRoute, oldView, route.transition.out, navigatingBack)
|
|
488
496
|
}
|
|
489
497
|
}
|
|
490
498
|
|
|
@@ -492,7 +500,7 @@ export const navigate = async function () {
|
|
|
492
500
|
if (route.transition.in) {
|
|
493
501
|
if (Array.isArray(route.transition.in)) {
|
|
494
502
|
for (let i = 0; i < route.transition.in.length; i++) {
|
|
495
|
-
i === route.transition.length - 1
|
|
503
|
+
i === route.transition.in.length - 1
|
|
496
504
|
? await setOrAnimate(holder, route.transition.in[i], shouldAnimate)
|
|
497
505
|
: setOrAnimate(holder, route.transition.in[i], shouldAnimate)
|
|
498
506
|
}
|
|
@@ -500,6 +508,33 @@ export const navigate = async function () {
|
|
|
500
508
|
await setOrAnimate(holder, route.transition.in, shouldAnimate)
|
|
501
509
|
}
|
|
502
510
|
}
|
|
511
|
+
|
|
512
|
+
if (this.parent[symbols.routerHooks]) {
|
|
513
|
+
const hooks = this.parent[symbols.routerHooks]
|
|
514
|
+
if (hooks.afterEach) {
|
|
515
|
+
try {
|
|
516
|
+
await hooks.afterEach.call(
|
|
517
|
+
this.parent,
|
|
518
|
+
route, // to
|
|
519
|
+
previousRoute // from
|
|
520
|
+
)
|
|
521
|
+
} catch (error) {
|
|
522
|
+
Log.error('Error in "AfterEach" Hook', error)
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (route.hooks.after) {
|
|
528
|
+
try {
|
|
529
|
+
await route.hooks.after.call(
|
|
530
|
+
this.parent,
|
|
531
|
+
route, // to
|
|
532
|
+
previousRoute // from
|
|
533
|
+
)
|
|
534
|
+
} catch (error) {
|
|
535
|
+
Log.error('Error or Rejected Promise in "After" Hook', error)
|
|
536
|
+
}
|
|
537
|
+
}
|
|
503
538
|
} else {
|
|
504
539
|
Log.error(`Route ${route.hash} not found`)
|
|
505
540
|
const routerHooks = this.parent[symbols.routerHooks]
|