@hkdigital/lib-sveltekit 0.1.23 → 0.1.24

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.
@@ -17,7 +17,8 @@
17
17
  * imageMeta?: import('../../config/typedef.js').ImageMeta | import('../../config/typedef.js').ImageMeta[],
18
18
  * imageLoader?: import('../../classes/svelte/image/index.js').ImageLoader,
19
19
  * alt?: string,
20
- * onProgress?: (progress: import('../../classes/svelte/network-loader/typedef.js').LoadingProgress) => void,
20
+ * id?: string|Symbol
21
+ * onProgress?: (progress: import('../../classes/svelte/network-loader/typedef.js').LoadingProgress, id: string|Symbol) => void,
21
22
  * [attr: string]: any
22
23
  * }}
23
24
  */
@@ -42,6 +43,9 @@
42
43
  // Accessibility
43
44
  alt = '',
44
45
 
46
+ // Identification
47
+ id = Symbol('ImageBox'),
48
+
45
49
  // Events
46
50
  onProgress,
47
51
 
@@ -87,11 +91,11 @@
87
91
 
88
92
  // Report progress from variants loader
89
93
  if (variantsLoader) {
90
- onProgress(variantsLoader.progress);
94
+ onProgress(variantsLoader.progress, id);
91
95
  }
92
96
  // Report progress from single image loader
93
97
  else if (imageLoader_) {
94
- onProgress(imageLoader_.progress);
98
+ onProgress(imageLoader_.progress, id);
95
99
  }
96
100
  });
97
101
 
@@ -13,5 +13,6 @@ declare const ImageBox: import("svelte").Component<{
13
13
  imageMeta?: import("../../config/typedef.js").ImageMeta | import("../../config/typedef.js").ImageMeta[];
14
14
  imageLoader?: import("../../classes/svelte/image/index.js").ImageLoader;
15
15
  alt?: string;
16
- onProgress?: (progress: import("../../classes/svelte/network-loader/typedef.js").LoadingProgress) => void;
16
+ id?: string | Symbol;
17
+ onProgress?: (progress: import("../../classes/svelte/network-loader/typedef.js").LoadingProgress, id: string | Symbol) => void;
17
18
  }, {}, "">;
@@ -0,0 +1,613 @@
1
+ import { defineStateContext } from '$lib/util/svelte/state-context/index.js';
2
+
3
+ import { findFirst } from '$lib/util/array/index.js';
4
+
5
+ import { untrack } from 'svelte';
6
+
7
+ import { HkPromise } from '$lib/classes/promise/index.js';
8
+
9
+ /* ----------------------------------------------------------------- typedefs */
10
+
11
+ /**
12
+ * @typedef {import("./typedef").Slide} Slide
13
+ */
14
+
15
+ /**
16
+ * @typedef {import("./typedef").Transition} Transition
17
+ */
18
+
19
+ /**
20
+ * @typedef {import("./typedef").Layer} Layer
21
+ */
22
+
23
+ /**
24
+ * @typedef {Object} LoadController
25
+ * @property {() => void} loaded - Function to call when loading is complete
26
+ * @property {() => void} cancel - Function to return to the previous slide
27
+ */
28
+
29
+ /**
30
+ * @typedef {Object} PresenterRef
31
+ * @property {(name: string) => void} gotoSlide - Navigate to a slide by name
32
+ * @property {() => string} getCurrentSlideName - Get the current slide name
33
+ */
34
+
35
+ /* -------------------------------------------------------------- Constants */
36
+
37
+ const Z_BACK = 0;
38
+ const Z_FRONT = 10;
39
+
40
+ const LABEL_A = 'A';
41
+ const LABEL_B = 'B';
42
+
43
+ /* ------------------------------------------------------- Define state class */
44
+
45
+ export class PresenterState {
46
+ /** @type {Slide[]} */
47
+ slides = $state.raw([]);
48
+
49
+ /** @type {Layer} */
50
+ layerA = $state.raw({ z: Z_BACK, visible: false, stageIdle: true });
51
+
52
+ /** @type {Layer} */
53
+ layerB = $state.raw({ z: Z_FRONT, visible: false, stageIdle: true });
54
+
55
+ /** @type {Slide|null} */
56
+ slideA = $state.raw(null);
57
+
58
+ /** @type {Slide|null} */
59
+ slideB = $state.raw(null);
60
+
61
+ /** @type {string} */
62
+ currentLayerLabel = $state(LABEL_B);
63
+
64
+ /** @type {string} */
65
+ nextLayerLabel = $state(LABEL_A);
66
+
67
+ /** @type {HkPromise[]} */
68
+ transitionPromises = $state.raw([]);
69
+
70
+ /** @type {boolean} */
71
+ isSlideLoading = $state(false);
72
+
73
+ /** @type {boolean} */
74
+ controllerRequested = $state(false);
75
+
76
+ /** @type {number} Loading timeout in milliseconds (0 = disabled) */
77
+ loadingTimeout = $state(1000);
78
+
79
+ /** @type {boolean} */
80
+ busy = $derived.by(() => {
81
+ const { layerA, layerB } = this;
82
+
83
+ const layerAStable =
84
+ layerA.stageShow || layerA.stageAfter || layerA.stageIdle;
85
+
86
+ const layerBStable =
87
+ layerB.stageShow || layerB.stageAfter || layerB.stageIdle;
88
+
89
+ return !(layerAStable && layerBStable);
90
+ });
91
+
92
+ /** @type {string} */
93
+ currentSlideName = $derived.by(() => {
94
+ const currentSlide = this.#getSlide(this.currentLayerLabel);
95
+ return currentSlide?.name || '';
96
+ });
97
+
98
+ /**
99
+ * Initialize the presenter state and set up reactivity
100
+ */
101
+ constructor() {
102
+ this.#setupStageTransitions();
103
+ this.#setupTransitionTracking();
104
+ this.#setupLoadingTransitions();
105
+ }
106
+
107
+ /**
108
+ * Returns a simplified presenter reference with essential methods
109
+ * for slide components to use
110
+ *
111
+ * @returns {PresenterRef} A reference object with presenter methods
112
+ */
113
+ getPresenterRef() {
114
+ return {
115
+ gotoSlide: (name) => this.gotoSlide(name),
116
+ getCurrentSlideName: () => this.currentSlideName
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Set up reactivity for stage transitions between the before/after states
122
+ * This handles the animation timing for both layers
123
+ */
124
+ #setupStageTransitions() {
125
+ // Handle layer A stage transitions
126
+ $effect(() => {
127
+ if (this.layerA.stageBeforeIn || this.layerA.stageBeforeOut) {
128
+ this.layerA = this.#processStageTransition(this.layerA);
129
+ }
130
+ });
131
+
132
+ // Handle layer B stage transitions
133
+ $effect(() => {
134
+ if (this.layerB.stageBeforeIn || this.layerB.stageBeforeOut) {
135
+ this.layerB = this.#processStageTransition(this.layerB);
136
+ }
137
+ });
138
+ }
139
+
140
+ /**
141
+ * Process a single stage transition for a layer
142
+ *
143
+ * @param {Layer} layer - The layer to process
144
+ * @returns {Layer} - The updated layer with new stage
145
+ */
146
+ #processStageTransition(layer) {
147
+ const updatedLayer = { ...layer };
148
+
149
+ if (updatedLayer.stageBeforeIn) {
150
+ delete updatedLayer.stageBeforeIn;
151
+ updatedLayer.stageIn = true;
152
+ } else if (updatedLayer.stageBeforeOut) {
153
+ delete updatedLayer.stageBeforeOut;
154
+ updatedLayer.stageOut = true;
155
+ }
156
+
157
+ return updatedLayer;
158
+ }
159
+
160
+ /**
161
+ * Set up reactivity for tracking transition promises
162
+ * This handles the completion of animations and layer swapping
163
+ */
164
+ #setupTransitionTracking() {
165
+ $effect(() => {
166
+ const promises = this.transitionPromises;
167
+
168
+ if (promises.length > 0) {
169
+ const nextSlide = this.#getSlide(this.nextLayerLabel);
170
+
171
+ if (!nextSlide) {
172
+ return;
173
+ }
174
+
175
+ untrack(() => {
176
+ this.#executeTransition(promises);
177
+ });
178
+ }
179
+ });
180
+ }
181
+
182
+ /**
183
+ * Set up reactivity to start transitions after component loading is complete
184
+ */
185
+ #setupLoadingTransitions() {
186
+ $effect(() => {
187
+ // Only start transitions when loading is complete and we have a next slide
188
+ if (!this.isSlideLoading && this.#getSlide(this.nextLayerLabel)) {
189
+ const currentSlide = this.#getSlide(this.currentLayerLabel);
190
+ const nextSlide = this.#getSlide(this.nextLayerLabel);
191
+
192
+ // Prepare the next layer for its entrance transition
193
+ this.#updateLayer(this.nextLayerLabel, {
194
+ z: Z_FRONT,
195
+ visible: true,
196
+ stageBeforeIn: true,
197
+ transitions: nextSlide?.intro ?? []
198
+ });
199
+
200
+ // Prepare the current layer for its exit transition
201
+ this.#updateLayer(this.currentLayerLabel, {
202
+ z: Z_BACK,
203
+ visible: true,
204
+ stageBeforeOut: true,
205
+ transitions: currentSlide?.outro ?? []
206
+ });
207
+
208
+ // Start transitions
209
+ this.#applyTransitions();
210
+ }
211
+ });
212
+ }
213
+
214
+ /**
215
+ * Execute the transition by waiting for all promises and then
216
+ * completing the transition
217
+ *
218
+ * @param {HkPromise[]} promises - Array of transition promises to wait for
219
+ */
220
+ async #executeTransition(promises) {
221
+ try {
222
+ await Promise.allSettled(promises);
223
+
224
+ untrack(() => {
225
+ this.#completeTransition();
226
+ });
227
+ } catch (error) {
228
+ console.log('transition promises cancelled', error);
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Complete the transition by updating layers and swapping them
234
+ */
235
+ #completeTransition() {
236
+ // Hide current layer and set stage to AFTER
237
+ this.#updateLayer(this.currentLayerLabel, {
238
+ z: Z_BACK,
239
+ visible: false,
240
+ stageAfter: true
241
+ });
242
+
243
+ // Set next layer stage to SHOW
244
+ this.#updateLayer(this.nextLayerLabel, {
245
+ z: Z_FRONT,
246
+ visible: true,
247
+ stageShow: true
248
+ });
249
+
250
+ // Remove slide from current layer
251
+ this.#updateSlide(this.currentLayerLabel, null);
252
+
253
+ // Swap current and next layer labels
254
+ this.#swapLayers();
255
+ }
256
+
257
+ // /**
258
+ // * Complete the transition by updating layers and swapping them
259
+ // * Ensures proper cleanup of all stage states
260
+ // */
261
+ // #completeTransition() {
262
+ // // Update current layer: hide it and set to AFTER state
263
+ // this.#updateLayer(this.currentLayerLabel, {
264
+ // z: Z_BACK,
265
+ // visible: false,
266
+ // stageIdle: false,
267
+ // stageBeforeIn: false,
268
+ // stageIn: false,
269
+ // stageBeforeOut: false,
270
+ // stageOut: false,
271
+ // stageShow: false,
272
+ // stageAfter: true
273
+ // });
274
+
275
+ // // Update next layer: show it and set to SHOW state
276
+ // this.#updateLayer(this.nextLayerLabel, {
277
+ // z: Z_FRONT,
278
+ // visible: true,
279
+ // stageIdle: false,
280
+ // stageBeforeIn: false,
281
+ // stageIn: false,
282
+ // stageBeforeOut: false,
283
+ // stageOut: false,
284
+ // stageAfter: false,
285
+ // stageShow: true
286
+ // });
287
+
288
+ // // Remove slide from current layer
289
+ // this.#updateSlide(this.currentLayerLabel, null);
290
+
291
+ // // Swap current and next layer labels
292
+ // this.#swapLayers();
293
+
294
+ // // Reset layer states after swap - crucial for next transition cycle
295
+ // // Reset former next layer (now current) to idle after showing
296
+ // setTimeout(() => {
297
+ // this.#updateLayer(this.currentLayerLabel, {
298
+ // stageShow: false,
299
+ // stageIdle: true,
300
+ // transitions: [] // Clear any lingering transitions
301
+ // });
302
+
303
+ // // Reset former current layer (now next) to idle after being hidden
304
+ // this.#updateLayer(this.nextLayerLabel, {
305
+ // stageAfter: false,
306
+ // stageIdle: true,
307
+ // transitions: [] // Clear any lingering transitions
308
+ // });
309
+ // }, 50); // Small delay to ensure DOM updates have completed
310
+ // }
311
+
312
+ /**
313
+ * Swap the current and next layer labels
314
+ */
315
+ #swapLayers() {
316
+ if (this.currentLayerLabel === LABEL_A) {
317
+ this.currentLayerLabel = LABEL_B;
318
+ this.nextLayerLabel = LABEL_A;
319
+ } else {
320
+ this.currentLayerLabel = LABEL_A;
321
+ this.nextLayerLabel = LABEL_B;
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Mark the slide as loaded, which triggers transitions to begin
327
+ */
328
+ finishSlideLoading() {
329
+ this.isSlideLoading = false;
330
+ }
331
+
332
+ /**
333
+ * Returns a controller object for managing manual loading
334
+ * Components can use this to signal when they're done loading
335
+ * or to cancel and go back to the previous slide
336
+ *
337
+ * @returns {LoadController} Object with loaded() and cancel() methods
338
+ */
339
+ getLoadingController() {
340
+ // Mark that the controller was requested
341
+ this.controllerRequested = true;
342
+
343
+ console.debug('controllerRequested');
344
+
345
+ return {
346
+ /**
347
+ * Call when component has finished loading
348
+ */
349
+ loaded: () => {
350
+ this.finishSlideLoading();
351
+ console.debug('finishSlideLoading');
352
+ },
353
+
354
+ /**
355
+ * Call to cancel loading and return to previous slide
356
+ */
357
+ cancel: () => {
358
+ // Return to previous slide if available
359
+ const currentSlideName = this.currentSlideName;
360
+ if (currentSlideName) {
361
+ this.gotoSlide(currentSlideName);
362
+ } else if (this.slides.length > 0) {
363
+ // Fallback to first slide if no current slide
364
+ this.gotoSlide(this.slides[0].name);
365
+ }
366
+ }
367
+ };
368
+ }
369
+
370
+ /**
371
+ * Configure the presentation
372
+ *
373
+ * @param {object} _
374
+ * @param {boolean} [_.autostart=false] - Whether to start automatically
375
+ * @param {string} [_.startSlide] - Name of the slide to start with
376
+ * @param {Slide[]} [_.slides] - Array of slides for the presentation
377
+ */
378
+ configure({ slides, autostart = true, startSlide }) {
379
+ untrack(() => {
380
+ if (slides) {
381
+ // Only update slides if provided
382
+ this.slides = slides;
383
+ }
384
+
385
+ if ((autostart || startSlide) && this.slides?.length) {
386
+ if (startSlide) {
387
+ this.gotoSlide(startSlide);
388
+ } else {
389
+ this.#gotoSlide(this.slides[0]);
390
+ }
391
+ }
392
+ });
393
+ }
394
+
395
+ /**
396
+ * Configure the presentation slides
397
+ *
398
+ * @param {Slide[]} slides - Array of slides for the presentation
399
+ */
400
+ configureSlides(slides) {
401
+ this.slides = slides ?? [];
402
+ }
403
+
404
+ /**
405
+ * Transition to another slide by name
406
+ *
407
+ * @param {string} name - Name of the slide to transition to
408
+ */
409
+ async gotoSlide(name) {
410
+ untrack(() => {
411
+ const slide = findFirst(this.slides, { name });
412
+
413
+ if (!slide) {
414
+ console.log('available slides', this.slides);
415
+ throw new Error(`Slide [${name}] has not been defined`);
416
+ }
417
+
418
+ this.#gotoSlide(slide);
419
+ });
420
+ }
421
+
422
+ /**
423
+ * Internal method to transition to another slide
424
+ *
425
+ * @param {Slide} slide - The slide to transition to
426
+ */
427
+ async #gotoSlide(slide) {
428
+ if (this.busy) {
429
+ throw new Error('Transition in progress');
430
+ }
431
+
432
+ // Reset controller requested flag
433
+ this.controllerRequested = false;
434
+
435
+ // Set loading state to true before starting transition
436
+ this.isSlideLoading = true;
437
+
438
+ // Add controller function to slide props if it has a component
439
+ if (slide.data?.component) {
440
+ // Get a presenter reference to pass to the slide
441
+ const presenterRef = this.getPresenterRef();
442
+
443
+ // Create a copy of the slide to avoid mutating the original
444
+ const slideWithExtras = {
445
+ ...slide,
446
+ data: {
447
+ ...slide.data,
448
+ props: {
449
+ ...(slide.data.props || {}),
450
+ getLoadingController: () => this.getLoadingController(),
451
+ presenter: presenterRef // Add presenter reference to props
452
+ }
453
+ }
454
+ };
455
+
456
+ // Add next slide to next layer with controller and presenter included
457
+ this.#updateSlide(this.nextLayerLabel, slideWithExtras);
458
+
459
+ // If a timeout is configured, automatically finish loading after delay
460
+ if (this.loadingTimeout > 0) {
461
+ setTimeout(() => {
462
+ // Only auto-finish if the controller wasn't requested
463
+ if (!this.controllerRequested && this.isSlideLoading) {
464
+ // console.debug(
465
+ // `Slide '${slide.name}' didn't request loading controller, auto-finishing.`
466
+ // );
467
+ this.finishSlideLoading();
468
+ }
469
+ }, this.loadingTimeout);
470
+ }
471
+ } else {
472
+ // No component, so just use the slide as is
473
+ this.#updateSlide(this.nextLayerLabel, slide);
474
+ // No component to load, so finish loading immediately
475
+ this.finishSlideLoading();
476
+ }
477
+
478
+ // Make next layer visible, move to front
479
+ this.#updateLayer(this.nextLayerLabel, {
480
+ z: Z_FRONT,
481
+ visible: true
482
+ });
483
+ }
484
+
485
+ /**
486
+ * Apply transitions between current and next slide
487
+ */
488
+ #applyTransitions() {
489
+ // Cancel existing transitions
490
+ let transitionPromises = this.transitionPromises;
491
+
492
+ for (const current of transitionPromises) {
493
+ current.tryCancel();
494
+ }
495
+
496
+ // Start new transitions
497
+ transitionPromises = [];
498
+
499
+ const currentSlide = this.#getSlide(this.currentLayerLabel);
500
+ const nextSlide = this.#getSlide(this.nextLayerLabel);
501
+
502
+ // Apply transitions `out` from currentslide
503
+ const transitionsOut = currentSlide?.outro;
504
+
505
+ if (transitionsOut) {
506
+ for (const transition of transitionsOut) {
507
+ const promise = this.#applyTransition(transition);
508
+ transitionPromises.push(promise);
509
+ }
510
+ }
511
+
512
+ // Apply transitions `in` from next slide
513
+ const transitionsIn = nextSlide?.intro;
514
+
515
+ if (transitionsIn) {
516
+ for (const transition of transitionsIn) {
517
+ const promise = this.#applyTransition(transition);
518
+ transitionPromises.push(promise);
519
+ }
520
+ }
521
+
522
+ this.transitionPromises = transitionPromises;
523
+ }
524
+
525
+ /**
526
+ * Apply a transition and return a transition promise
527
+ *
528
+ * @param {Transition} transition - The transition to apply
529
+ * @returns {HkPromise} Promise that resolves when transition completes
530
+ */
531
+ #applyTransition(transition) {
532
+ const delay = (transition.delay ?? 0) + (transition.duration ?? 0);
533
+
534
+ if (0 === delay) {
535
+ const promise = new HkPromise(() => {});
536
+ promise.resolve(true);
537
+ return promise;
538
+ }
539
+
540
+ let promise = new HkPromise((/** @type {function} */ resolve) => {
541
+ if (delay) {
542
+ setTimeout(() => {
543
+ resolve(true);
544
+ }, delay);
545
+ }
546
+ });
547
+
548
+ return promise;
549
+ }
550
+
551
+ /**
552
+ * Get slide by layer label
553
+ *
554
+ * @param {string} label - Layer label (A or B)
555
+ * @returns {Slide|null} The slide for the specified layer or null
556
+ */
557
+ #getSlide(label) {
558
+ if (label === LABEL_A) {
559
+ return this.slideA;
560
+ }
561
+
562
+ if (label === LABEL_B) {
563
+ return this.slideB;
564
+ }
565
+
566
+ return null;
567
+ }
568
+
569
+ /**
570
+ * Update layer by label
571
+ *
572
+ * @param {string} label - Layer label (A or B)
573
+ * @param {Layer} data - Layer data to update
574
+ */
575
+ #updateLayer(label, data) {
576
+ if (label === LABEL_A) {
577
+ this.layerA = data;
578
+ return;
579
+ }
580
+
581
+ if (label === LABEL_B) {
582
+ this.layerB = data;
583
+ return;
584
+ }
585
+
586
+ throw new Error(`Missing layer [${label}]`);
587
+ }
588
+
589
+ /**
590
+ * Update slide by label
591
+ *
592
+ * @param {string} label - Layer label (A or B)
593
+ * @param {Slide|null} data - Slide data to update or null to clear
594
+ */
595
+ #updateSlide(label, data) {
596
+ if (label === LABEL_A) {
597
+ this.slideA = data;
598
+ return;
599
+ }
600
+
601
+ if (label === LABEL_B) {
602
+ this.slideB = data;
603
+ return;
604
+ }
605
+
606
+ throw new Error(`Missing slide [${label}]`);
607
+ }
608
+ }
609
+
610
+ /* -------------------------------------- Export create & get state functions */
611
+
612
+ export const [createOrGetState, createState, getState] =
613
+ defineStateContext(PresenterState);
@@ -15,35 +15,16 @@ export class PresenterState {
15
15
  nextLayerLabel: string;
16
16
  /** @type {HkPromise[]} */
17
17
  transitionPromises: HkPromise[];
18
+ /** @type {HkPromise} */
19
+ slideLoadingPromise: HkPromise;
18
20
  /** @type {boolean} */
19
21
  isSlideLoading: boolean;
20
22
  /** @type {boolean} */
21
- controllerRequested: boolean;
22
- /** @type {number} Loading timeout in milliseconds (0 = disabled) */
23
- loadingTimeout: number;
23
+ loadingSpinner: boolean;
24
24
  /** @type {boolean} */
25
25
  busy: boolean;
26
26
  /** @type {string} */
27
27
  currentSlideName: string;
28
- /**
29
- * Returns a simplified presenter reference with essential methods
30
- * for slide components to use
31
- *
32
- * @returns {PresenterRef} A reference object with presenter methods
33
- */
34
- getPresenterRef(): PresenterRef;
35
- /**
36
- * Mark the slide as loaded, which triggers transitions to begin
37
- */
38
- finishSlideLoading(): void;
39
- /**
40
- * Returns a controller object for managing manual loading
41
- * Components can use this to signal when they're done loading
42
- * or to cancel and go back to the previous slide
43
- *
44
- * @returns {LoadController} Object with loaded() and cancel() methods
45
- */
46
- getLoadingController(): LoadController;
47
28
  /**
48
29
  * Configure the presentation
49
30
  *
@@ -1,3 +1,5 @@
1
+ import { tick } from 'svelte';
2
+
1
3
  import { defineStateContext } from '../../util/svelte/state-context/index.js';
2
4
 
3
5
  import { findFirst } from '../../util/array/index.js';
@@ -67,24 +69,25 @@ export class PresenterState {
67
69
  /** @type {HkPromise[]} */
68
70
  transitionPromises = $state.raw([]);
69
71
 
72
+ /** @type {HkPromise} */
73
+ slideLoadingPromise = null;
74
+
70
75
  /** @type {boolean} */
71
76
  isSlideLoading = $state(false);
72
77
 
73
78
  /** @type {boolean} */
74
- controllerRequested = $state(false);
75
-
76
- /** @type {number} Loading timeout in milliseconds (0 = disabled) */
77
- loadingTimeout = $state(1000);
79
+ loadingSpinner = $state(false);
78
80
 
79
81
  /** @type {boolean} */
80
82
  busy = $derived.by(() => {
81
- const { layerA, layerB } = this;
83
+ const { layerA, layerB, isSlideLoading } = this;
84
+
82
85
  const layerAStable =
83
86
  layerA.stageShow || layerA.stageAfter || layerA.stageIdle;
84
87
  const layerBStable =
85
88
  layerB.stageShow || layerB.stageAfter || layerB.stageIdle;
86
89
 
87
- return !(layerAStable && layerBStable);
90
+ return !(layerAStable && layerBStable) || isSlideLoading;
88
91
  });
89
92
 
90
93
  /** @type {string} */
@@ -98,21 +101,26 @@ export class PresenterState {
98
101
  */
99
102
  constructor() {
100
103
  this.#setupStageTransitions();
101
- this.#setupTransitionTracking();
102
- this.#setupLoadingTransitions();
103
- }
104
104
 
105
- /**
106
- * Returns a simplified presenter reference with essential methods
107
- * for slide components to use
108
- *
109
- * @returns {PresenterRef} A reference object with presenter methods
110
- */
111
- getPresenterRef() {
112
- return {
113
- gotoSlide: (name) => this.gotoSlide(name),
114
- getCurrentSlideName: () => this.currentSlideName
115
- };
105
+ let timeout;
106
+
107
+ $effect((slideLoadingPromise) => {
108
+ if (this.isSlideLoading) {
109
+ // Enable spinner after a short delay
110
+ clearTimeout(timeout);
111
+ setTimeout(() => {
112
+ untrack(() => {
113
+ if (this.isSlideLoading) {
114
+ this.loadingSpinner = true;
115
+ } else {
116
+ this.loadingSpinner = false;
117
+ }
118
+ });
119
+ }, 500);
120
+ } else {
121
+ this.loadingSpinner = false;
122
+ }
123
+ });
116
124
  }
117
125
 
118
126
  /**
@@ -155,59 +163,27 @@ export class PresenterState {
155
163
  return updatedLayer;
156
164
  }
157
165
 
158
- /**
159
- * Set up reactivity for tracking transition promises
160
- * This handles the completion of animations and layer swapping
161
- */
162
- #setupTransitionTracking() {
163
- $effect(() => {
164
- const promises = this.transitionPromises;
166
+ // /**
167
+ // * Set up reactivity for tracking transition promises
168
+ // * This handles the completion of animations and layer swapping
169
+ // */
170
+ // #setupTransitionTracking() {
171
+ // $effect(() => {
172
+ // const promises = this.transitionPromises;
165
173
 
166
- if (promises.length > 0) {
167
- const nextSlide = this.#getSlide(this.nextLayerLabel);
174
+ // if (promises.length > 0) {
175
+ // const nextSlide = this.#getSlide(this.nextLayerLabel);
168
176
 
169
- if (!nextSlide) {
170
- return;
171
- }
177
+ // if (!nextSlide) {
178
+ // return;
179
+ // }
172
180
 
173
- untrack(() => {
174
- this.#executeTransition(promises);
175
- });
176
- }
177
- });
178
- }
179
-
180
- /**
181
- * Set up reactivity to start transitions after component loading is complete
182
- */
183
- #setupLoadingTransitions() {
184
- $effect(() => {
185
- // Only start transitions when loading is complete and we have a next slide
186
- if (!this.isSlideLoading && this.#getSlide(this.nextLayerLabel)) {
187
- const currentSlide = this.#getSlide(this.currentLayerLabel);
188
- const nextSlide = this.#getSlide(this.nextLayerLabel);
189
-
190
- // Prepare the next layer for its entrance transition
191
- this.#updateLayer(this.nextLayerLabel, {
192
- z: Z_FRONT,
193
- visible: true,
194
- stageBeforeIn: true,
195
- transitions: nextSlide?.intro ?? []
196
- });
197
-
198
- // Prepare the current layer for its exit transition
199
- this.#updateLayer(this.currentLayerLabel, {
200
- z: Z_BACK,
201
- visible: true,
202
- stageBeforeOut: true,
203
- transitions: currentSlide?.outro ?? []
204
- });
205
-
206
- // Start transitions
207
- this.#applyTransitions();
208
- }
209
- });
210
- }
181
+ // untrack(() => {
182
+ // this.#executeTransition(promises);
183
+ // });
184
+ // }
185
+ // });
186
+ // }
211
187
 
212
188
  /**
213
189
  * Execute the transition by waiting for all promises and then
@@ -217,6 +193,8 @@ export class PresenterState {
217
193
  */
218
194
  async #executeTransition(promises) {
219
195
  try {
196
+ // console.debug('executeTransition');
197
+
220
198
  await Promise.allSettled(promises);
221
199
 
222
200
  untrack(() => {
@@ -265,48 +243,6 @@ export class PresenterState {
265
243
  }
266
244
  }
267
245
 
268
- /**
269
- * Mark the slide as loaded, which triggers transitions to begin
270
- */
271
- finishSlideLoading() {
272
- this.isSlideLoading = false;
273
- }
274
-
275
- /**
276
- * Returns a controller object for managing manual loading
277
- * Components can use this to signal when they're done loading
278
- * or to cancel and go back to the previous slide
279
- *
280
- * @returns {LoadController} Object with loaded() and cancel() methods
281
- */
282
- getLoadingController() {
283
- // Mark that the controller was requested
284
- this.controllerRequested = true;
285
-
286
- return {
287
- /**
288
- * Call when component has finished loading
289
- */
290
- loaded: () => {
291
- this.finishSlideLoading();
292
- },
293
-
294
- /**
295
- * Call to cancel loading and return to previous slide
296
- */
297
- cancel: () => {
298
- // Return to previous slide if available
299
- const currentSlideName = this.currentSlideName;
300
- if (currentSlideName) {
301
- this.gotoSlide(currentSlideName);
302
- } else if (this.slides.length > 0) {
303
- // Fallback to first slide if no current slide
304
- this.gotoSlide(this.slides[0].name);
305
- }
306
- }
307
- };
308
- }
309
-
310
246
  /**
311
247
  * Configure the presentation
312
248
  *
@@ -369,57 +305,79 @@ export class PresenterState {
369
305
  throw new Error('Transition in progress');
370
306
  }
371
307
 
372
- // Reset controller requested flag
373
- this.controllerRequested = false;
374
-
375
- // Set loading state to true before starting transition
376
- this.isSlideLoading = true;
377
-
378
- // Add controller function to slide props if it has a component
379
- if (slide.data?.component) {
380
- // Get a presenter reference to pass to the slide
381
- const presenterRef = this.getPresenterRef();
382
-
383
- // Create a copy of the slide to avoid mutating the original
384
- const slideWithExtras = {
385
- ...slide,
386
- data: {
387
- ...slide.data,
388
- props: {
389
- ...(slide.data.props || {}),
390
- getLoadingController: () => this.getLoadingController(),
391
- presenter: presenterRef // Add presenter reference to props
392
- }
308
+ this.slideLoadingPromise = null;
309
+
310
+ // Get a presenter reference to pass to the slide
311
+ const presenterRef = this.#getPresenterRef();
312
+
313
+ // Create a copy of the slide to avoid mutating the original
314
+ const slideWithProps = {
315
+ ...slide,
316
+ data: {
317
+ ...slide.data,
318
+ props: {
319
+ ...(slide.data.props || {}),
320
+ getLoadingController: () => {
321
+ this.isSlideLoading = true;
322
+ this.slideLoadingPromise = new HkPromise(() => {});
323
+
324
+ return this.#getLoadingController();
325
+ // this.slideLoadingPromise should be a HkPromise now
326
+ // console.log('slideLoadingPromise', this.slideLoadingPromise);
327
+ },
328
+ presenter: presenterRef // Add presenter reference to props
393
329
  }
394
- };
330
+ }
331
+ };
395
332
 
396
- // Add next slide to next layer with controller and presenter included
397
- this.#updateSlide(this.nextLayerLabel, slideWithExtras);
333
+ // console.debug('Checkpoint 1');
398
334
 
399
- // If a timeout is configured, automatically finish loading after delay
400
- if (this.loadingTimeout > 0) {
401
- setTimeout(() => {
402
- // Only auto-finish if the controller wasn't requested
403
- if (!this.controllerRequested && this.isSlideLoading) {
404
- // console.debug(
405
- // `Slide '${slide.name}' didn't request loading controller, auto-finishing.`
406
- // );
407
- this.finishSlideLoading();
408
- }
409
- }, this.loadingTimeout);
410
- }
411
- } else {
412
- // No component, so just use the slide as is
413
- this.#updateSlide(this.nextLayerLabel, slide);
414
- // No component to load, so finish loading immediately
415
- this.finishSlideLoading();
335
+ // Add next slide to next layer
336
+ this.#updateSlide(this.nextLayerLabel, slideWithProps);
337
+
338
+ // console.debug('Checkpoint 2');
339
+
340
+ await tick();
341
+
342
+ // console.debug('Checkpoint 3');
343
+
344
+ if (this.slideLoadingPromise) {
345
+ // console.debug('Waiting for slide to load');
346
+ // @ts-ignore
347
+ await this.slideLoadingPromise;
348
+ this.isSlideLoading = false;
349
+ // console.debug('Done waiting for slide loading');
416
350
  }
417
351
 
418
- // Make next layer visible, move to front
352
+ const currentSlide = this.#getSlide(this.currentLayerLabel);
353
+ const nextSlide = this.#getSlide(this.nextLayerLabel);
354
+
355
+ // console.debug('Checkpoint 4');
356
+
357
+ // Make next layer visible, move to front, and prepare for
358
+ // transition in
419
359
  this.#updateLayer(this.nextLayerLabel, {
420
360
  z: Z_FRONT,
421
- visible: true
361
+ visible: true,
362
+ stageBeforeIn: true,
363
+ transitions: nextSlide?.intro ?? []
422
364
  });
365
+
366
+ // Move current layer to back, keep visible, and prepare for
367
+ // transition out
368
+ this.#updateLayer(this.currentLayerLabel, {
369
+ z: Z_BACK,
370
+ visible: true,
371
+ stageBeforeOut: true,
372
+ transitions: currentSlide?.outro ?? []
373
+ });
374
+
375
+ // console.debug('Checkpoint 5');
376
+
377
+ // Start transitions
378
+ this.#applyTransitions();
379
+
380
+ await this.#executeTransition(this.transitionPromises);
423
381
  }
424
382
 
425
383
  /**
@@ -545,6 +503,49 @@ export class PresenterState {
545
503
 
546
504
  throw new Error(`Missing slide [${label}]`);
547
505
  }
506
+
507
+ /**
508
+ * Returns a simplified presenter reference with essential methods
509
+ * for slide components to use
510
+ *
511
+ * @returns {PresenterRef} A reference object with presenter methods
512
+ */
513
+ #getPresenterRef() {
514
+ return {
515
+ gotoSlide: (name) => this.gotoSlide(name),
516
+ getCurrentSlideName: () => this.currentSlideName
517
+ };
518
+ }
519
+
520
+ /**
521
+ * Returns a controller object for managing manual loading
522
+ * Components can use this to signal when they're done loading
523
+ * or to cancel and go back to the previous slide
524
+ *
525
+ * @returns {LoadController}
526
+ * Object with loaded() and cancel() methods
527
+ */
528
+ #getLoadingController() {
529
+ // console.debug('getLoadingController was called');
530
+
531
+ return {
532
+ /**
533
+ * Call when component has finished loading
534
+ */
535
+ loaded: () => {
536
+ // console.debug('Slide said loading has completed');
537
+ this.slideLoadingPromise?.tryResolve();
538
+ },
539
+
540
+ /**
541
+ * Call to cancel loading and return to previous slide
542
+ */
543
+ cancel: () => {
544
+ // console.debug('Slide said loading has cancelled');
545
+ this.slideLoadingPromise?.tryReject();
546
+ }
547
+ };
548
+ }
548
549
  }
549
550
 
550
551
  /* -------------------------------------- Export create & get state functions */
@@ -3,7 +3,8 @@
3
3
 
4
4
  import { GridLayers } from '../../components/layout/index.js';
5
5
 
6
- import { createOrGetPresenterState } from './index.js';
6
+ import { PresenterState } from './Presenter.state.svelte.js';
7
+ import { getPresenterState } from './index.js';
7
8
  import { cssBefore, cssDuring } from './util.js';
8
9
 
9
10
  /* ------------------------------------------------------------------ Props */
@@ -23,7 +24,9 @@
23
24
  * autostart?: boolean,
24
25
  * startSlide?: string,
25
26
  * instanceKey?: Symbol | string,
26
- * layoutSnippet: import('svelte').Snippet<[Slide|null, Layer]>
27
+ * presenter?: import('./Presenter.state.svelte.js').PresenterState,
28
+ * layoutSnippet: import('svelte').Snippet<[Slide|null, Layer]>,
29
+ * loadingSnippet?: import('svelte').Snippet,
27
30
  * }}
28
31
  */
29
32
  let {
@@ -38,13 +41,20 @@
38
41
  // State
39
42
  instanceKey,
40
43
 
44
+ presenter = $bindable(new PresenterState()),
45
+
41
46
  // Snippets
42
- layoutSnippet
47
+ layoutSnippet,
48
+ loadingSnippet
43
49
  } = $props();
44
50
 
45
- /* ------------------------------------------------------------------ State */
51
+ // > Create presenter state object and register using setContext
52
+
53
+ // FIXME: Using getPresenterState to force creation of presenter outside
54
+ // the component. Otherwise transitions doe not work somehow..
55
+ presenter = getPresenterState(instanceKey);
46
56
 
47
- const presenter = createOrGetPresenterState(instanceKey);
57
+ // > State
48
58
 
49
59
  $effect.pre(() => {
50
60
  // Configure presenter with slides if provided
@@ -107,7 +117,7 @@
107
117
  inert={presenter.busy}
108
118
  class="justify-self-stretch self-stretch overflow-hidden"
109
119
  >
110
- <div class={classesA} style={stylesA}>
120
+ <div class="{classesA} h-full w-full" style={stylesA}>
111
121
  {@render layoutSnippet(presenter.slideA, presenter.layerA)}
112
122
  </div>
113
123
  </div>
@@ -118,8 +128,14 @@
118
128
  inert={presenter.busy}
119
129
  class="justify-self-stretch self-stretch overflow-hidden"
120
130
  >
121
- <div class={classesB} style={stylesB}>
131
+ <div class="{classesB} h-full w-full" style={stylesB}>
122
132
  {@render layoutSnippet(presenter.slideB, presenter.layerB)}
123
133
  </div>
124
134
  </div>
135
+
136
+ {#if loadingSnippet && presenter.loadingSpinner}
137
+ <div class="h-full w-full" style="z-index:20;">
138
+ {@render loadingSnippet()}
139
+ </div>
140
+ {/if}
125
141
  </GridLayers>
@@ -5,5 +5,7 @@ declare const Presenter: import("svelte").Component<{
5
5
  autostart?: boolean;
6
6
  startSlide?: string;
7
7
  instanceKey?: Symbol | string;
8
+ presenter?: import("./Presenter.state.svelte.js").PresenterState;
8
9
  layoutSnippet: import("svelte").Snippet<[import("./typedef.js").Slide | null, import("./typedef.js").Layer]>;
9
- }, {}, "">;
10
+ loadingSnippet?: import("svelte").Snippet;
11
+ }, {}, "presenter">;
@@ -0,0 +1,125 @@
1
+ <script>
2
+ /* ---------------------------------------------------------------- Imports */
3
+
4
+ import { GridLayers } from '$lib/components/layout/index.js';
5
+
6
+ import { createOrGetPresenterState } from './index.js';
7
+ import { cssBefore, cssDuring } from './util.js';
8
+
9
+ /* ------------------------------------------------------------------ Props */
10
+
11
+ /**
12
+ * @typedef {import("./typedef.js").Slide} Slide
13
+ */
14
+
15
+ /**
16
+ * @typedef {import("./typedef.js").Layer} Layer
17
+ */
18
+
19
+ /**
20
+ * @type {{
21
+ * classes?: string,
22
+ * slides?: import("./typedef.js").Slide[],
23
+ * autostart?: boolean,
24
+ * startSlide?: string,
25
+ * instanceKey?: Symbol | string,
26
+ * layoutSnippet: import('svelte').Snippet<[Slide|null, Layer]>
27
+ * }}
28
+ */
29
+ let {
30
+ // > Style
31
+ classes,
32
+
33
+ // > Functional
34
+ slides,
35
+ autostart = false,
36
+ startSlide,
37
+
38
+ // State
39
+ instanceKey,
40
+
41
+ // Snippets
42
+ layoutSnippet
43
+ } = $props();
44
+
45
+ /* ------------------------------------------------------------------ State */
46
+
47
+ const presenter = createOrGetPresenterState(instanceKey);
48
+
49
+ $effect.pre(() => {
50
+ // Configure presenter with slides if provided
51
+ presenter.configure({ slides, autostart, startSlide });
52
+ });
53
+
54
+ let classesA = $state('');
55
+ let classesB = $state('');
56
+
57
+ let stylesA = $state('');
58
+ let stylesB = $state('');
59
+
60
+ //> Apply stage classes and styles
61
+
62
+ $effect(() => {
63
+ // > layerA
64
+
65
+ const { stageBeforeIn, stageIn, stageBeforeOut, stageOut, transitions } =
66
+ presenter.layerA;
67
+
68
+ if (transitions && transitions.length) {
69
+ if (stageBeforeIn || stageBeforeOut) {
70
+ ({ style: stylesA, classes: classesA } = cssBefore(transitions));
71
+ } else if (stageIn || stageOut) {
72
+ setTimeout(() => {
73
+ ({ style: stylesA, classes: classesA } = cssDuring(transitions));
74
+ });
75
+ }
76
+ } else {
77
+ stylesA = '';
78
+ classesA = '';
79
+ }
80
+ });
81
+
82
+ $effect(() => {
83
+ // > layerB
84
+
85
+ const { stageBeforeIn, stageIn, stageBeforeOut, stageOut, transitions } =
86
+ presenter.layerB;
87
+
88
+ if (transitions) {
89
+ if (stageBeforeIn || stageBeforeOut) {
90
+ ({ style: stylesB, classes: classesB } = cssBefore(transitions));
91
+ } else if (stageIn || stageOut) {
92
+ setTimeout(() => {
93
+ ({ style: stylesB, classes: classesB } = cssDuring(transitions));
94
+ });
95
+ }
96
+ } else {
97
+ stylesB = '';
98
+ classesB = '';
99
+ }
100
+ });
101
+ </script>
102
+
103
+ <GridLayers data-component="presenter" {classes}>
104
+ <div
105
+ style:z-index={presenter.layerA.z}
106
+ style:visibility={presenter.layerA.visible ? 'visible' : 'hidden'}
107
+ inert={presenter.busy}
108
+ class="justify-self-stretch self-stretch overflow-hidden"
109
+ >
110
+ <div class={classesA} style={stylesA}>
111
+ {@render layoutSnippet(presenter.slideA, presenter.layerA)}
112
+ </div>
113
+ </div>
114
+
115
+ <div
116
+ style:z-index={presenter.layerB.z}
117
+ style:visibility={presenter.layerB.visible ? 'visible' : 'hidden'}
118
+ inert={presenter.busy}
119
+ class="justify-self-stretch self-stretch overflow-hidden"
120
+ >
121
+ <div class={classesB} style={stylesB}>
122
+ {@render layoutSnippet(presenter.slideB, presenter.layerB)}
123
+ </div>
124
+ </div>
125
+ </GridLayers>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hkdigital/lib-sveltekit",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
4
4
  "author": {
5
5
  "name": "HKdigital",
6
6
  "url": "https://hkdigital.nl"