@hkdigital/lib-core 0.5.57 → 0.5.58
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/dist/logging/internal/logger/Logger.js +1 -1
- package/dist/state/machines/page-machine/PageMachine.svelte.d.ts +76 -122
- package/dist/state/machines/page-machine/PageMachine.svelte.js +97 -403
- package/dist/state/machines/page-machine/PageMachine.svelte.js__ +708 -0
- package/dist/state/machines/page-machine/README.md +102 -66
- package/package.json +1 -1
|
@@ -1,99 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Route-aware data manager for page groups
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Does NOT enforce
|
|
6
|
-
* (because users can navigate to any URL via browser).
|
|
4
|
+
* Tracks navigation within a route group and manages business/domain data.
|
|
5
|
+
* Does NOT enforce transitions - allows free navigation via browser.
|
|
7
6
|
*
|
|
8
7
|
* Features:
|
|
9
|
-
* -
|
|
8
|
+
* - Current route tracking and synchronization
|
|
10
9
|
* - Start path management
|
|
11
10
|
* - Data properties for business/domain state
|
|
12
|
-
* - Visited
|
|
13
|
-
* - onEnter hooks with abort/complete handlers for animations
|
|
11
|
+
* - Visited routes tracking
|
|
14
12
|
*
|
|
15
13
|
* Basic usage:
|
|
16
14
|
* ```javascript
|
|
17
15
|
* const machine = new PageMachine({
|
|
18
16
|
* startPath: '/intro/start',
|
|
19
|
-
*
|
|
20
|
-
* [STATE_START]: '/intro/start',
|
|
21
|
-
* [STATE_PROFILE]: '/intro/profile'
|
|
22
|
-
* }
|
|
17
|
+
* routes: ['/intro/start', '/intro/profile', '/intro/complete']
|
|
23
18
|
* });
|
|
24
19
|
*
|
|
25
|
-
* // Sync machine
|
|
20
|
+
* // Sync machine with URL changes
|
|
26
21
|
* $effect(() => {
|
|
27
22
|
* machine.syncFromPath($page.url.pathname);
|
|
28
23
|
* });
|
|
24
|
+
*
|
|
25
|
+
* // Check current route
|
|
26
|
+
* if (machine.current === '/intro/profile') {
|
|
27
|
+
* // Do something
|
|
28
|
+
* }
|
|
29
29
|
* ```
|
|
30
30
|
*
|
|
31
|
-
*
|
|
31
|
+
* Animations and page-specific logic should use $effect in pages:
|
|
32
32
|
* ```javascript
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* routeMap: {
|
|
36
|
-
* [STATE_ANIMATE]: '/game/animate',
|
|
37
|
-
* [STATE_PLAY]: '/game/play'
|
|
38
|
-
* },
|
|
39
|
-
* onEnterHooks: {
|
|
40
|
-
* [STATE_ANIMATE]: (done) => {
|
|
41
|
-
* const animation = playAnimation(1000);
|
|
42
|
-
* animation.finished.then(() => done(STATE_PLAY));
|
|
33
|
+
* // In +page.svelte
|
|
34
|
+
* const animations = new PageAnimations();
|
|
43
35
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* };
|
|
48
|
-
* }
|
|
36
|
+
* $effect(() => {
|
|
37
|
+
* if (someCondition) {
|
|
38
|
+
* animations.start();
|
|
49
39
|
* }
|
|
50
40
|
* });
|
|
51
|
-
*
|
|
52
|
-
* // Fast-forward animation
|
|
53
|
-
* machine.completeTransitions();
|
|
54
|
-
*
|
|
55
|
-
* // Cancel animation
|
|
56
|
-
* machine.abortTransitions();
|
|
57
41
|
* ```
|
|
58
42
|
*/
|
|
59
|
-
import { switchToPage } from '../../../util/sveltekit.js';
|
|
60
|
-
|
|
61
43
|
export default class PageMachine {
|
|
62
44
|
/**
|
|
63
|
-
* Logger instance
|
|
45
|
+
* Logger instance
|
|
64
46
|
* @type {import('../../../logging/client.js').Logger}
|
|
65
47
|
*/
|
|
66
48
|
logger;
|
|
49
|
+
|
|
67
50
|
/**
|
|
68
|
-
* Current
|
|
51
|
+
* Current route path
|
|
69
52
|
* @type {string}
|
|
70
53
|
*/
|
|
71
54
|
// @ts-ignore
|
|
72
55
|
#current = $state();
|
|
73
56
|
|
|
74
57
|
/**
|
|
75
|
-
* Start path for this
|
|
58
|
+
* Start path for this route group
|
|
76
59
|
* @type {string}
|
|
77
60
|
*/
|
|
78
61
|
#startPath = '';
|
|
79
62
|
|
|
80
63
|
/**
|
|
81
|
-
*
|
|
82
|
-
* @type {string}
|
|
83
|
-
*/
|
|
84
|
-
#startState = '';
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Map of states to route paths
|
|
88
|
-
* @type {Record<string, string>}
|
|
64
|
+
* Optional list of valid routes for this group
|
|
65
|
+
* @type {string[]}
|
|
89
66
|
*/
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Reverse map of route paths to states
|
|
94
|
-
* @type {Record<string, string>}
|
|
95
|
-
*/
|
|
96
|
-
#pathToStateMap = {};
|
|
67
|
+
#routes = [];
|
|
97
68
|
|
|
98
69
|
/**
|
|
99
70
|
* Data properties for business/domain state
|
|
@@ -103,12 +74,12 @@ export default class PageMachine {
|
|
|
103
74
|
#data = $state({});
|
|
104
75
|
|
|
105
76
|
/**
|
|
106
|
-
* Track which
|
|
77
|
+
* Track which routes have been visited during this session
|
|
107
78
|
* Useful for showing first-time hints/tips
|
|
108
79
|
* @type {Set<string>}
|
|
109
80
|
*/
|
|
110
81
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
111
|
-
#
|
|
82
|
+
#visitedRoutes = new Set();
|
|
112
83
|
|
|
113
84
|
/**
|
|
114
85
|
* Revision counter for triggering reactivity
|
|
@@ -116,159 +87,68 @@ export default class PageMachine {
|
|
|
116
87
|
*/
|
|
117
88
|
#revision = $state(0);
|
|
118
89
|
|
|
119
|
-
/**
|
|
120
|
-
* Map of state names to onEnter hook configurations
|
|
121
|
-
* @type {Record<string, {onEnter: Function}>}
|
|
122
|
-
*/
|
|
123
|
-
#onEnterHooks = {};
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Current state's onEnter handler (abort/complete functions)
|
|
127
|
-
* @type {{abort?: Function, complete?: Function} | null}
|
|
128
|
-
*/
|
|
129
|
-
#currentOnEnterHandler = null;
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Current state's done callback
|
|
133
|
-
* @type {Function | null}
|
|
134
|
-
*/
|
|
135
|
-
#currentOnEnterDone = null;
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Flag to prevent concurrent state transitions
|
|
139
|
-
* @type {boolean}
|
|
140
|
-
*/
|
|
141
|
-
#isTransitioning = false;
|
|
142
|
-
|
|
143
90
|
/**
|
|
144
91
|
* Constructor
|
|
145
92
|
*
|
|
146
93
|
* @param {Object} config - Configuration object
|
|
147
94
|
* @param {string} config.startPath
|
|
148
95
|
* Start path for this route group (e.g., '/game/play')
|
|
149
|
-
* @param {
|
|
150
|
-
*
|
|
96
|
+
* @param {string[]} [config.routes=[]]
|
|
97
|
+
* Optional list of valid routes (for validation/dev tools)
|
|
151
98
|
* @param {Record<string, any>} [config.initialData={}]
|
|
152
99
|
* Initial data properties (from server)
|
|
153
|
-
* @param {Record<string, Function>} [config.onEnterHooks={}]
|
|
154
|
-
* Map of states to onEnter hook functions
|
|
155
100
|
* @param {import('../../../logging/client.js').Logger} [config.logger]
|
|
156
|
-
* Logger instance (optional
|
|
101
|
+
* Logger instance (optional)
|
|
157
102
|
*
|
|
158
103
|
* @example
|
|
159
104
|
* ```javascript
|
|
160
105
|
* const machine = new PageMachine({
|
|
161
106
|
* startPath: '/intro/start',
|
|
162
|
-
*
|
|
163
|
-
* [STATE_START]: '/intro/start',
|
|
164
|
-
* [STATE_ANIMATE]: '/intro/animate'
|
|
165
|
-
* },
|
|
107
|
+
* routes: ['/intro/start', '/intro/profile'],
|
|
166
108
|
* initialData: {
|
|
167
109
|
* INTRO_COMPLETED: false
|
|
168
|
-
* },
|
|
169
|
-
* onEnterHooks: {
|
|
170
|
-
* [STATE_ANIMATE]: (done) => {
|
|
171
|
-
* setTimeout(() => done(STATE_START), 1000);
|
|
172
|
-
* return {
|
|
173
|
-
* abort: () => clearTimeout(...),
|
|
174
|
-
* complete: () => done(STATE_START)
|
|
175
|
-
* };
|
|
176
|
-
* }
|
|
177
110
|
* }
|
|
178
111
|
* });
|
|
179
112
|
* ```
|
|
180
113
|
*/
|
|
181
|
-
constructor({
|
|
182
|
-
startPath,
|
|
183
|
-
routeMap = {},
|
|
184
|
-
initialData = {},
|
|
185
|
-
onEnterHooks = {},
|
|
186
|
-
logger = null
|
|
187
|
-
}) {
|
|
114
|
+
constructor({ startPath, routes = [], initialData = {}, logger = null }) {
|
|
188
115
|
if (!startPath) {
|
|
189
116
|
throw new Error('PageMachine requires startPath parameter');
|
|
190
117
|
}
|
|
191
118
|
|
|
192
119
|
this.logger = logger;
|
|
193
120
|
this.#startPath = startPath;
|
|
194
|
-
this.#
|
|
121
|
+
this.#routes = routes;
|
|
195
122
|
this.#data = initialData;
|
|
196
|
-
this.#
|
|
197
|
-
|
|
198
|
-
// Build reverse map (path -> state) and validate no duplicates
|
|
199
|
-
for (const [state, path] of Object.entries(routeMap)) {
|
|
200
|
-
// Check if this path is already mapped to a different state
|
|
201
|
-
const existingState = this.#pathToStateMap[path];
|
|
202
|
-
if (existingState && existingState !== state) {
|
|
203
|
-
throw new Error(
|
|
204
|
-
`PageMachine: Duplicate route mapping detected. ` +
|
|
205
|
-
`Path "${path}" is mapped to both "${existingState}" and "${state}". ` +
|
|
206
|
-
`Each route path must map to exactly one state.`
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
this.#pathToStateMap[path] = state;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Derive initial state from startPath
|
|
213
|
-
const initialState = this.#pathToStateMap[startPath];
|
|
214
|
-
if (!initialState) {
|
|
215
|
-
throw new Error(
|
|
216
|
-
`PageMachine: startPath "${startPath}" not found in routeMap`
|
|
217
|
-
);
|
|
218
|
-
}
|
|
123
|
+
this.#current = startPath;
|
|
219
124
|
|
|
220
|
-
|
|
221
|
-
this.#
|
|
222
|
-
|
|
223
|
-
// Mark initial state as visited
|
|
224
|
-
this.#visitedStates.add(initialState);
|
|
125
|
+
// Mark start path as visited
|
|
126
|
+
this.#visitedRoutes.add(startPath);
|
|
225
127
|
}
|
|
226
128
|
|
|
227
129
|
/**
|
|
228
|
-
*
|
|
229
|
-
* Converts function to {onEnter: function} object
|
|
130
|
+
* Synchronize machine with URL path
|
|
230
131
|
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*/
|
|
234
|
-
#normalizeOnEnterHooks(hooks) {
|
|
235
|
-
/** @type {Record<string, {onEnter: Function} */
|
|
236
|
-
const normalized = {};
|
|
237
|
-
|
|
238
|
-
for (const [state, hook] of Object.entries(hooks)) {
|
|
239
|
-
if (typeof hook === 'function') {
|
|
240
|
-
// Simple function -> wrap in object
|
|
241
|
-
normalized[state] = { onEnter: hook };
|
|
242
|
-
} else if (hook?.onEnter) {
|
|
243
|
-
// Already an object with onEnter
|
|
244
|
-
normalized[state] = hook;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return normalized;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Synchronize machine state with URL path
|
|
253
|
-
*
|
|
254
|
-
* Call this in a $effect that watches $page.url.pathname
|
|
255
|
-
* Automatically tracks visited states and triggers onEnter hooks
|
|
132
|
+
* Call this in a $effect that watches $page.url.pathname.
|
|
133
|
+
* Automatically tracks visited routes.
|
|
256
134
|
*
|
|
257
135
|
* @param {string} currentPath - Current URL pathname
|
|
258
136
|
*
|
|
259
|
-
* @returns {boolean} True if
|
|
137
|
+
* @returns {boolean} True if route was changed
|
|
260
138
|
*/
|
|
261
139
|
syncFromPath(currentPath) {
|
|
262
|
-
|
|
140
|
+
// Find matching route
|
|
141
|
+
const matchedRoute = this.#findMatchingRoute(currentPath);
|
|
142
|
+
|
|
143
|
+
if (matchedRoute && matchedRoute !== this.#current) {
|
|
144
|
+
this.logger?.debug(`syncFromPath: ${currentPath} → ${matchedRoute}`);
|
|
263
145
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
this.
|
|
267
|
-
|
|
268
|
-
);
|
|
146
|
+
const oldRoute = this.#current;
|
|
147
|
+
this.#current = matchedRoute;
|
|
148
|
+
this.#visitedRoutes.add(matchedRoute);
|
|
149
|
+
this.#revision++;
|
|
269
150
|
|
|
270
|
-
|
|
271
|
-
this.#setState(targetState);
|
|
151
|
+
this.logger?.debug(`Route changed: ${oldRoute} → ${matchedRoute}`);
|
|
272
152
|
|
|
273
153
|
return true;
|
|
274
154
|
}
|
|
@@ -277,154 +157,50 @@ export default class PageMachine {
|
|
|
277
157
|
}
|
|
278
158
|
|
|
279
159
|
/**
|
|
280
|
-
*
|
|
281
|
-
* Handles onEnter hooks and auto-transitions
|
|
282
|
-
*
|
|
283
|
-
* Note: This is private to enforce URL-first navigation.
|
|
284
|
-
* To change state, navigate to the URL using switchToPage()
|
|
285
|
-
* and let syncFromPath() update the state.
|
|
286
|
-
*
|
|
287
|
-
* @param {string} newState - Target state
|
|
288
|
-
*/
|
|
289
|
-
async #setState(newState) {
|
|
290
|
-
if (newState === this.#current || this.#isTransitioning) {
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Abort previous state's onEnter handler
|
|
295
|
-
if (this.#currentOnEnterHandler?.abort) {
|
|
296
|
-
this.#currentOnEnterHandler.abort();
|
|
297
|
-
}
|
|
298
|
-
this.#currentOnEnterHandler = null;
|
|
299
|
-
this.#currentOnEnterDone = null;
|
|
300
|
-
|
|
301
|
-
const oldState = this.#current;
|
|
302
|
-
this.#isTransitioning = true;
|
|
303
|
-
this.#current = newState;
|
|
304
|
-
this.#visitedStates.add(newState);
|
|
305
|
-
|
|
306
|
-
// Log state transition
|
|
307
|
-
this.logger?.debug(`setState: ${oldState} → ${newState}`);
|
|
308
|
-
|
|
309
|
-
// Check if this state has an onEnter hook
|
|
310
|
-
const hookConfig = this.#onEnterHooks[newState];
|
|
311
|
-
if (hookConfig?.onEnter) {
|
|
312
|
-
// Create done callback for auto-transition
|
|
313
|
-
let doneCalled = false;
|
|
314
|
-
|
|
315
|
-
const done = (/** @type {string} */ nextState) => {
|
|
316
|
-
if (!doneCalled && nextState && nextState !== newState) {
|
|
317
|
-
doneCalled = true;
|
|
318
|
-
this.#isTransitioning = false;
|
|
319
|
-
this.#setState(nextState);
|
|
320
|
-
}
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
this.#currentOnEnterDone = done;
|
|
324
|
-
|
|
325
|
-
// Call the onEnter hook
|
|
326
|
-
try {
|
|
327
|
-
const handler = hookConfig.onEnter(done);
|
|
328
|
-
|
|
329
|
-
// Store abort/complete handlers if provided
|
|
330
|
-
if (handler && typeof handler === 'object') {
|
|
331
|
-
if (handler.abort || handler.complete) {
|
|
332
|
-
this.#currentOnEnterHandler = {
|
|
333
|
-
abort: handler.abort,
|
|
334
|
-
complete: handler.complete
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// If hook returned a promise, await it
|
|
340
|
-
if (handler?.then) {
|
|
341
|
-
await handler;
|
|
342
|
-
}
|
|
343
|
-
} catch (error) {
|
|
344
|
-
const logger = this.logger ?? console;
|
|
345
|
-
logger.error(`Error in onEnter hook for state ${newState}:`, error);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
this.#isTransitioning = false;
|
|
350
|
-
this.#revision++;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Get state name from URL path
|
|
160
|
+
* Find matching route from path
|
|
355
161
|
*
|
|
356
162
|
* @param {string} path - URL pathname
|
|
357
163
|
*
|
|
358
|
-
* @returns {string|null}
|
|
164
|
+
* @returns {string|null} Matched route or null
|
|
359
165
|
*/
|
|
360
|
-
#
|
|
361
|
-
//
|
|
362
|
-
if (this.#
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
// Try partial match (path includes route)
|
|
367
|
-
for (const [routePath, state] of Object.entries(this.#pathToStateMap)) {
|
|
368
|
-
if (path.includes(routePath)) {
|
|
369
|
-
return state;
|
|
166
|
+
#findMatchingRoute(path) {
|
|
167
|
+
// If routes list provided, try to match against it
|
|
168
|
+
if (this.#routes.length > 0) {
|
|
169
|
+
// Try exact match first
|
|
170
|
+
if (this.#routes.includes(path)) {
|
|
171
|
+
return path;
|
|
370
172
|
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
return null;
|
|
374
|
-
}
|
|
375
173
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
*/
|
|
383
|
-
getPathForState(state) {
|
|
384
|
-
const path = this.#routeMap[state];
|
|
174
|
+
// Try partial match (path starts with route)
|
|
175
|
+
for (const route of this.#routes) {
|
|
176
|
+
if (path.startsWith(route)) {
|
|
177
|
+
return route;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
385
180
|
|
|
386
|
-
|
|
387
|
-
throw new Error(`No path found for state [${state}]`);
|
|
181
|
+
return null;
|
|
388
182
|
}
|
|
389
183
|
|
|
184
|
+
// No routes list - accept any path
|
|
390
185
|
return path;
|
|
391
186
|
}
|
|
392
187
|
|
|
393
188
|
/**
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
* @param {string} state - State name
|
|
397
|
-
*/
|
|
398
|
-
navigateToState(state) {
|
|
399
|
-
const path = this.getPathForState(state);
|
|
400
|
-
switchToPage(path);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Get route path for current state
|
|
189
|
+
* Get current route
|
|
405
190
|
*
|
|
406
|
-
* @returns {string
|
|
407
|
-
*/
|
|
408
|
-
getCurrentPath() {
|
|
409
|
-
return this.getPathForState(this.#current);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Get current state
|
|
414
|
-
*
|
|
415
|
-
* @returns {string} Current state name
|
|
191
|
+
* @returns {string} Current route path
|
|
416
192
|
*/
|
|
417
193
|
get current() {
|
|
418
194
|
return this.#current;
|
|
419
195
|
}
|
|
420
196
|
|
|
421
197
|
/**
|
|
422
|
-
* Get the
|
|
198
|
+
* Get the routes list
|
|
423
199
|
*
|
|
424
|
-
* @returns {
|
|
200
|
+
* @returns {string[]} Copy of routes list
|
|
425
201
|
*/
|
|
426
|
-
get
|
|
427
|
-
return
|
|
202
|
+
get routes() {
|
|
203
|
+
return [...this.#routes];
|
|
428
204
|
}
|
|
429
205
|
|
|
430
206
|
/* ===== Data Properties (Business/Domain State) ===== */
|
|
@@ -485,7 +261,8 @@ export default class PageMachine {
|
|
|
485
261
|
/**
|
|
486
262
|
* Update multiple data properties at once
|
|
487
263
|
*
|
|
488
|
-
* @param {Record<string, any>} dataUpdates
|
|
264
|
+
* @param {Record<string, any>} dataUpdates
|
|
265
|
+
* Object with key-value pairs
|
|
489
266
|
*
|
|
490
267
|
* @example
|
|
491
268
|
* ```javascript
|
|
@@ -503,32 +280,32 @@ export default class PageMachine {
|
|
|
503
280
|
this.#revision++;
|
|
504
281
|
}
|
|
505
282
|
|
|
506
|
-
/* ===== Visited
|
|
283
|
+
/* ===== Visited Routes Tracking ===== */
|
|
507
284
|
|
|
508
285
|
/**
|
|
509
|
-
* Check if a
|
|
286
|
+
* Check if a route has been visited
|
|
510
287
|
*
|
|
511
|
-
* @param {string}
|
|
288
|
+
* @param {string} route - Route path to check
|
|
512
289
|
*
|
|
513
|
-
* @returns {boolean} True if the
|
|
290
|
+
* @returns {boolean} True if the route has been visited
|
|
514
291
|
*
|
|
515
292
|
* @example
|
|
516
293
|
* ```javascript
|
|
517
|
-
* if (machine.hasVisited(
|
|
294
|
+
* if (machine.hasVisited('/intro/profile')) {
|
|
518
295
|
* // User has seen profile page, skip intro
|
|
519
296
|
* }
|
|
520
297
|
* ```
|
|
521
298
|
*/
|
|
522
|
-
hasVisited(
|
|
299
|
+
hasVisited(route) {
|
|
523
300
|
// Access revision to ensure reactivity
|
|
524
301
|
this.#revision;
|
|
525
|
-
return this.#
|
|
302
|
+
return this.#visitedRoutes.has(route);
|
|
526
303
|
}
|
|
527
304
|
|
|
528
305
|
/**
|
|
529
|
-
* Check if the start
|
|
306
|
+
* Check if the start route has been visited
|
|
530
307
|
*
|
|
531
|
-
* @returns {boolean} True if the start
|
|
308
|
+
* @returns {boolean} True if the start route has been visited
|
|
532
309
|
*
|
|
533
310
|
* @example
|
|
534
311
|
* ```javascript
|
|
@@ -538,27 +315,27 @@ export default class PageMachine {
|
|
|
538
315
|
* ```
|
|
539
316
|
*/
|
|
540
317
|
get hasVisitedStart() {
|
|
541
|
-
return this.hasVisited(this.#
|
|
318
|
+
return this.hasVisited(this.#startPath);
|
|
542
319
|
}
|
|
543
320
|
|
|
544
321
|
/**
|
|
545
|
-
* Get all visited
|
|
322
|
+
* Get all visited routes
|
|
546
323
|
*
|
|
547
|
-
* @returns {string[]} Array of visited
|
|
324
|
+
* @returns {string[]} Array of visited route paths
|
|
548
325
|
*/
|
|
549
|
-
|
|
326
|
+
getVisitedRoutes() {
|
|
550
327
|
// Access revision to ensure reactivity
|
|
551
328
|
this.#revision;
|
|
552
|
-
return Array.from(this.#
|
|
329
|
+
return Array.from(this.#visitedRoutes);
|
|
553
330
|
}
|
|
554
331
|
|
|
555
332
|
/**
|
|
556
|
-
* Reset visited
|
|
333
|
+
* Reset visited routes tracking
|
|
557
334
|
* Useful for testing or resetting experience
|
|
558
335
|
*/
|
|
559
|
-
|
|
560
|
-
this.#
|
|
561
|
-
this.#
|
|
336
|
+
resetVisitedRoutes() {
|
|
337
|
+
this.#visitedRoutes.clear();
|
|
338
|
+
this.#visitedRoutes.add(this.#current);
|
|
562
339
|
this.#revision++;
|
|
563
340
|
}
|
|
564
341
|
|
|
@@ -573,15 +350,6 @@ export default class PageMachine {
|
|
|
573
350
|
return this.#startPath;
|
|
574
351
|
}
|
|
575
352
|
|
|
576
|
-
/**
|
|
577
|
-
* Get the start state
|
|
578
|
-
*
|
|
579
|
-
* @returns {string} Start state name
|
|
580
|
-
*/
|
|
581
|
-
get startState() {
|
|
582
|
-
return this.#startState;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
353
|
/**
|
|
586
354
|
* Check if the supplied path matches the start path
|
|
587
355
|
*
|
|
@@ -601,19 +369,19 @@ export default class PageMachine {
|
|
|
601
369
|
}
|
|
602
370
|
|
|
603
371
|
/**
|
|
604
|
-
* Check if currently on the start
|
|
372
|
+
* Check if currently on the start path
|
|
605
373
|
*
|
|
606
|
-
* @returns {boolean} True if current
|
|
374
|
+
* @returns {boolean} True if current route is the start path
|
|
607
375
|
*
|
|
608
376
|
* @example
|
|
609
377
|
* ```javascript
|
|
610
|
-
* if (machine.
|
|
378
|
+
* if (machine.isOnStartPath) {
|
|
611
379
|
* // Show onboarding
|
|
612
380
|
* }
|
|
613
381
|
* ```
|
|
614
382
|
*/
|
|
615
|
-
get
|
|
616
|
-
return this.#current === this.#
|
|
383
|
+
get isOnStartPath() {
|
|
384
|
+
return this.#current === this.#startPath;
|
|
617
385
|
}
|
|
618
386
|
|
|
619
387
|
/**
|
|
@@ -631,78 +399,4 @@ export default class PageMachine {
|
|
|
631
399
|
switchToPage(this.#startPath);
|
|
632
400
|
});
|
|
633
401
|
}
|
|
634
|
-
|
|
635
|
-
/* ===== Transition Control Methods ===== */
|
|
636
|
-
|
|
637
|
-
/**
|
|
638
|
-
* Abort current state's transitions
|
|
639
|
-
* Cancels animations/operations immediately (incomplete state)
|
|
640
|
-
*
|
|
641
|
-
* @example
|
|
642
|
-
* ```javascript
|
|
643
|
-
* // User clicks "Cancel" button
|
|
644
|
-
* machine.abortTransitions();
|
|
645
|
-
* ```
|
|
646
|
-
*/
|
|
647
|
-
abortTransitions() {
|
|
648
|
-
if (this.#currentOnEnterHandler?.abort) {
|
|
649
|
-
this.#currentOnEnterHandler.abort();
|
|
650
|
-
this.#currentOnEnterHandler = null;
|
|
651
|
-
this.#currentOnEnterDone = null;
|
|
652
|
-
this.#revision++;
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
/**
|
|
657
|
-
* Complete current state's transitions immediately
|
|
658
|
-
* Fast-forwards animations/operations to completion (complete state)
|
|
659
|
-
*
|
|
660
|
-
* @example
|
|
661
|
-
* ```javascript
|
|
662
|
-
* // User clicks "Skip" or "Next" button
|
|
663
|
-
* machine.completeTransitions();
|
|
664
|
-
* ```
|
|
665
|
-
*/
|
|
666
|
-
completeTransitions() {
|
|
667
|
-
if (this.#currentOnEnterHandler?.complete) {
|
|
668
|
-
this.#currentOnEnterHandler.complete();
|
|
669
|
-
this.#currentOnEnterHandler = null;
|
|
670
|
-
this.#currentOnEnterDone = null;
|
|
671
|
-
this.#revision++;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
/**
|
|
676
|
-
* Check if current state has transitions that can be completed
|
|
677
|
-
*
|
|
678
|
-
* @returns {boolean} True if completeTransitions() can be called
|
|
679
|
-
*
|
|
680
|
-
* @example
|
|
681
|
-
* ```svelte
|
|
682
|
-
* {#if machine.canCompleteTransitions}
|
|
683
|
-
* <button onclick={() => machine.completeTransitions()}>Skip</button>
|
|
684
|
-
* {/if}
|
|
685
|
-
* ```
|
|
686
|
-
*/
|
|
687
|
-
get canCompleteTransitions() {
|
|
688
|
-
this.#revision; // Ensure reactivity
|
|
689
|
-
return !!this.#currentOnEnterHandler?.complete;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
/**
|
|
693
|
-
* Check if current state has transitions that can be aborted
|
|
694
|
-
*
|
|
695
|
-
* @returns {boolean} True if abortTransitions() can be called
|
|
696
|
-
*
|
|
697
|
-
* @example
|
|
698
|
-
* ```svelte
|
|
699
|
-
* {#if machine.canAbortTransitions}
|
|
700
|
-
* <button onclick={() => machine.abortTransitions()}>Cancel</button>
|
|
701
|
-
* {/if}
|
|
702
|
-
* ```
|
|
703
|
-
*/
|
|
704
|
-
get canAbortTransitions() {
|
|
705
|
-
this.#revision; // Ensure reactivity
|
|
706
|
-
return !!this.#currentOnEnterHandler?.abort;
|
|
707
|
-
}
|
|
708
402
|
}
|