@projektemacher/storymapjs 0.10.8 → 0.11.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.
Files changed (195) hide show
  1. package/CHANGELOG +81 -0
  2. package/README.md +129 -12
  3. package/dist/assets/{demo-DBpeF31K.js → demo-bWnOyfUw.js} +1 -1
  4. package/dist/assets/harness-C3JUv5Ii.js +1 -0
  5. package/dist/assets/{harness-multi-CYHgI0a9.js → harness-multi-XZNex3tb.js} +1 -1
  6. package/dist/assets/{harness-support-Do7sIp2j.js → harness-support-CjfZIVeb.js} +1 -1
  7. package/dist/assets/main-DQ32hWBM.js +17 -0
  8. package/dist/context.json +13 -2
  9. package/dist/css/storymap.css +1 -1
  10. package/dist/demo.html +3 -2
  11. package/dist/docs/api/assets/navigation.js +1 -1
  12. package/dist/docs/api/assets/search.js +1 -1
  13. package/dist/docs/api/classes/StoryMap.html +777 -269
  14. package/dist/docs/api/functions/MediaType.html +1 -1
  15. package/dist/docs/api/functions/isPresentation3Collection.html +1 -1
  16. package/dist/docs/api/functions/isPresentation3Manifest.html +1 -1
  17. package/dist/docs/api/functions/isSlideshowCollection.html +51 -0
  18. package/dist/docs/api/functions/loadCSS.html +1 -1
  19. package/dist/docs/api/functions/manifestToStorymapData.html +1 -1
  20. package/dist/docs/api/functions/setLanguage.html +1 -1
  21. package/dist/docs/api/functions/slideshowToStorymapData.html +51 -0
  22. package/dist/docs/api/functions/slideshowVersion.html +48 -0
  23. package/dist/docs/api/functions/storymapToManifest.html +1 -1
  24. package/dist/docs/api/functions/validateStorymap.html +1 -1
  25. package/dist/docs/api/functions/validateStorymapAndReport.html +1 -1
  26. package/dist/docs/api/index.html +115 -12
  27. package/dist/docs/api/interfaces/AnimationHandle.html +2 -2
  28. package/dist/docs/api/interfaces/ImagereadyPayload.html +4 -4
  29. package/dist/docs/api/interfaces/LanguageEntry.html +6 -6
  30. package/dist/docs/api/interfaces/LatLngLiteral.html +4 -4
  31. package/dist/docs/api/interfaces/LoadOptions.html +2 -2
  32. package/dist/docs/api/interfaces/MapEvents.html +116 -0
  33. package/dist/docs/api/interfaces/MapMarkerEvents.html +66 -0
  34. package/dist/docs/api/interfaces/MarkerEventPayload.html +57 -0
  35. package/dist/docs/api/interfaces/MediaEvents.html +78 -0
  36. package/dist/docs/api/interfaces/MenuBarEvents.html +114 -0
  37. package/dist/docs/api/interfaces/MessageEvents.html +72 -0
  38. package/dist/docs/api/interfaces/OlLayer.html +1 -1
  39. package/dist/docs/api/interfaces/OlMap.html +1 -1
  40. package/dist/docs/api/interfaces/OlSource.html +1 -1
  41. package/dist/docs/api/interfaces/OlTileLayer.html +1 -1
  42. package/dist/docs/api/interfaces/OlVectorLayer.html +1 -1
  43. package/dist/docs/api/interfaces/OlView.html +1 -1
  44. package/dist/docs/api/interfaces/SlideBackgroundState.html +66 -0
  45. package/dist/docs/api/interfaces/SlideEvents.html +78 -0
  46. package/dist/docs/api/interfaces/SlideNavEvents.html +72 -0
  47. package/dist/docs/api/interfaces/SlideshowPlayerSettings.html +182 -0
  48. package/dist/docs/api/interfaces/SlideshowToStorymapResult.html +60 -0
  49. package/dist/docs/api/interfaces/SlideshowTranslatorOptions.html +65 -0
  50. package/dist/docs/api/interfaces/SlideshowWarning.html +72 -0
  51. package/dist/docs/api/interfaces/StoryMapEvents.html +146 -0
  52. package/dist/docs/api/interfaces/StoryMapMarker.html +763 -0
  53. package/dist/docs/api/interfaces/StorySliderEvents.html +102 -0
  54. package/dist/docs/api/interfaces/StorymapBasemap.html +69 -0
  55. package/dist/docs/api/interfaces/StorymapData.html +12 -6
  56. package/dist/docs/api/interfaces/StorymapDataWrapper.html +2 -2
  57. package/dist/docs/api/interfaces/StorymapDocument.html +2 -2
  58. package/dist/docs/api/interfaces/StorymapError.html +3 -3
  59. package/dist/docs/api/interfaces/StorymapGeoreference.html +5 -5
  60. package/dist/docs/api/interfaces/StorymapGeoreferenceBody.html +4 -4
  61. package/dist/docs/api/interfaces/StorymapGroundControlPoint.html +4 -4
  62. package/dist/docs/api/interfaces/StorymapLanguageMap.html +2 -2
  63. package/dist/docs/api/interfaces/StorymapManifest.html +12 -12
  64. package/dist/docs/api/interfaces/StorymapManifestAnnotation.html +43 -11
  65. package/dist/docs/api/interfaces/StorymapManifestBackground.html +6 -6
  66. package/dist/docs/api/interfaces/StorymapManifestCanvas.html +65 -17
  67. package/dist/docs/api/interfaces/StorymapManifestContentResource.html +8 -8
  68. package/dist/docs/api/interfaces/StorymapManifestFeatureCollection.html +4 -4
  69. package/dist/docs/api/interfaces/StorymapManifestGeoreferencing.html +7 -7
  70. package/dist/docs/api/interfaces/StorymapManifestImageService.html +4 -4
  71. package/dist/docs/api/interfaces/StorymapManifestRange.html +5 -5
  72. package/dist/docs/api/interfaces/StorymapManifestService.html +4 -4
  73. package/dist/docs/api/interfaces/StorymapManifestSpecificResource.html +4 -4
  74. package/dist/docs/api/interfaces/StorymapManifestStatement.html +3 -3
  75. package/dist/docs/api/interfaces/StorymapMapOptions.html +5 -5
  76. package/dist/docs/api/interfaces/StorymapOptions.html +277 -75
  77. package/dist/docs/api/interfaces/StorymapOverlayLayer.html +42 -11
  78. package/dist/docs/api/interfaces/StorymapSlide.html +70 -16
  79. package/dist/docs/api/interfaces/StorymapSlideBackground.html +4 -4
  80. package/dist/docs/api/interfaces/StorymapSlideLocation.html +58 -14
  81. package/dist/docs/api/interfaces/StorymapSlideMarker.html +8 -9
  82. package/dist/docs/api/interfaces/StorymapSlideMedia.html +49 -10
  83. package/dist/docs/api/interfaces/StorymapSlideNarration.html +61 -9
  84. package/dist/docs/api/interfaces/StorymapSlideText.html +4 -4
  85. package/dist/docs/api/interfaces/StorymapTilejson.html +8 -8
  86. package/dist/docs/api/interfaces/SwipableEvents.html +102 -0
  87. package/dist/docs/api/interfaces/TextEvents.html +66 -0
  88. package/dist/docs/api/media/events.md +66 -0
  89. package/dist/docs/api/media/migration-from-knightlab.md +89 -49
  90. package/dist/docs/api/media/slideshow.md +109 -0
  91. package/dist/docs/api/media/storymap-as-iiif-manifest.md +1007 -0
  92. package/dist/docs/api/modules.html +6 -6
  93. package/dist/docs/api/types/ContentState.html +3 -3
  94. package/dist/docs/api/types/FiredEvent.html +43 -0
  95. package/dist/docs/api/types/ImagereadyKind.html +1 -1
  96. package/dist/docs/api/types/MediaTypeMatch.html +5 -5
  97. package/dist/docs/api/types/SwipeDirection.html +36 -0
  98. package/dist/docs/api/types/TileSourceFactory.html +1 -1
  99. package/dist/docs/authoring.html +20 -7
  100. package/dist/docs/events.html +263 -0
  101. package/dist/docs/iiif.html +136 -22
  102. package/dist/docs/migration.html +66 -19
  103. package/dist/docs/readme.html +113 -12
  104. package/dist/embed/index.html +7 -0
  105. package/dist/examples/font-css-false.json +33 -0
  106. package/dist/examples/issue-image-region.json +1 -1
  107. package/dist/examples/layers-control.json +60 -0
  108. package/dist/examples/map-template-provider.json +36 -0
  109. package/dist/examples/multi-media.json +71 -0
  110. package/dist/examples-iiif/annotated-image.json +16 -0
  111. package/dist/examples-iiif/font-css-false.json +114 -0
  112. package/dist/examples-iiif/issue-image-region.json +1 -1
  113. package/dist/examples-iiif/layers-control.json +226 -0
  114. package/dist/examples-iiif/map-template-provider.json +133 -0
  115. package/dist/examples-iiif/multi-media.json +237 -0
  116. package/dist/examples-slideshow/single.json +79 -0
  117. package/dist/examples-slideshow/static/pixel.png +0 -0
  118. package/dist/examples-slideshow/static.json +44 -0
  119. package/dist/harness-multi.html +3 -3
  120. package/dist/harness.html +4 -3
  121. package/dist/index.html +3 -2
  122. package/dist/js/storymap.d.ts +1326 -289
  123. package/dist/js/storymap.js +6690 -4784
  124. package/dist/js/storymap.js.map +1 -1
  125. package/dist/navplace-properties.json +14 -0
  126. package/dist/site.css +54 -36
  127. package/docs/migration-from-knightlab.md +89 -49
  128. package/docs/storymap-as-iiif-manifest.md +199 -126
  129. package/package.json +4 -2
  130. package/schema/storymap.schema.json +334 -34
  131. package/src/core/Util.ts +3 -1
  132. package/src/core/mixins.ts +42 -28
  133. package/src/language/Language.ts +7 -0
  134. package/src/language/locale/.expected-gaps.json +287 -27
  135. package/src/language/locale/de.json +11 -1
  136. package/src/language/locale/en.json +11 -1
  137. package/src/main.ts +51 -0
  138. package/src/map/Map.ts +44 -6
  139. package/src/map/MapMarker.ts +16 -3
  140. package/src/map/openlayers/Map.OpenLayers.ts +787 -62
  141. package/src/map/openlayers/MapMarker.OpenLayers.ts +113 -13
  142. package/src/map/slideView.ts +149 -0
  143. package/src/map/types.ts +2 -0
  144. package/src/media/EmbedUtil.ts +27 -2
  145. package/src/media/Media.ts +27 -4
  146. package/src/media/embedId.ts +6 -0
  147. package/src/media/types/Flickr.ts +14 -3
  148. package/src/media/types/HtmlMedia.ts +66 -0
  149. package/src/media/types/Image.ts +15 -3
  150. package/src/media/types/SoundCloud.ts +1 -1
  151. package/src/media/types/Text.ts +27 -8
  152. package/src/media/types/Twitter.ts +9 -7
  153. package/src/media/types/Vimeo.ts +1 -1
  154. package/src/media/types/YouTube.ts +2 -2
  155. package/src/scss/Typography.scss +15 -15
  156. package/src/scss/VCO.StoryMap.scss +173 -8
  157. package/src/scss/core/Reset.scss +5 -18
  158. package/src/scss/map/VCO.Map.scss +1 -1
  159. package/src/scss/map/VCO.MapMarker.scss +20 -9
  160. package/src/scss/map/openlayers/VCO.Map.OpenLayers.scss +7 -4
  161. package/src/scss/media/VCO.Media.scss +7 -7
  162. package/src/scss/media/types/VCO.Media.Blockquote.scss +1 -1
  163. package/src/scss/media/types/VCO.Media.Text.scss +5 -5
  164. package/src/scss/media/types/VCO.Media.Twitter.scss +4 -4
  165. package/src/scss/media/types/VCO.Media.Wikipedia.scss +2 -19
  166. package/src/scss/site/site.scss +55 -36
  167. package/src/scss/slider/VCO.Slide.scss +49 -11
  168. package/src/scss/slider/VCO.SlideNav.scss +23 -7
  169. package/src/scss/slider/VCO.StorySlider.scss +50 -0
  170. package/src/scss/snapmap/VCO.SnapMap.scss +3 -3
  171. package/src/scss/ui/VCO.Consent.scss +20 -14
  172. package/src/scss/ui/VCO.Layers.scss +119 -0
  173. package/src/scss/ui/VCO.MenuBar.Button.scss +140 -19
  174. package/src/scss/ui/VCO.MenuBar.scss +9 -2
  175. package/src/scss/ui/VCO.Message.scss +14 -0
  176. package/src/slider/Slide.ts +232 -74
  177. package/src/slider/SlideNav.ts +15 -4
  178. package/src/slider/StorySlider.ts +203 -22
  179. package/src/storymap/Consent.ts +22 -0
  180. package/src/storymap/StoryMap.ts +944 -131
  181. package/src/storymap/content-state.ts +3 -8
  182. package/src/storymap/font-css.ts +72 -0
  183. package/src/storymap/from-slideshow.ts +785 -0
  184. package/src/storymap/iiif-shared.ts +228 -0
  185. package/src/storymap/iiif.ts +289 -100
  186. package/src/storymap/to-iiif.ts +131 -44
  187. package/src/types.ts +382 -17
  188. package/src/ui/LayersControl.ts +302 -0
  189. package/src/ui/MenuBar.ts +232 -21
  190. package/src/ui/Message.ts +36 -3
  191. package/src/ui/Swipable.ts +25 -7
  192. package/dist/assets/harness-Cr0rYpvt.js +0 -1
  193. package/dist/assets/main-DXalctuR.js +0 -17
  194. package/src/scss/VCO.StoryMap.Dark.scss +0 -150
  195. package/src/scss/Variables.Dark.scss +0 -57
@@ -55,8 +55,7 @@ interface StorySliderOptions {
55
55
  }
56
56
 
57
57
  /**
58
- * The slider's element cache. `live_region` only exists when the slide
59
- * announcements are enabled (the `a11y` option), so it starts out null.
58
+ * The slider's element cache.
60
59
  */
61
60
  type StorySliderElements = {
62
61
  container: HTMLElement;
@@ -73,6 +72,18 @@ interface SlideBackgroundChange {
73
72
  image?: boolean;
74
73
  }
75
74
 
75
+ export interface StorySliderEvents {
76
+ colorchange: SlideBackgroundChange;
77
+ nav_next: Partial<StorymapData>;
78
+ nav_previous: Partial<StorymapData>;
79
+ nav_left: Partial<StorymapData>;
80
+ nav_right: Partial<StorymapData>;
81
+ slideAdded: Partial<StorymapData>;
82
+ change: { current_slide: number; uniqueid: string | null | undefined };
83
+ loaded: Partial<StorymapData>;
84
+ title: { title: string };
85
+ }
86
+
76
87
  class StorySliderBase {
77
88
  declare "_el": StorySliderElements;
78
89
  declare "_nav": { previous: SlideNav; next: SlideNav };
@@ -84,13 +95,19 @@ class StorySliderBase {
84
95
  declare "_message": Message;
85
96
  declare "current_slide": number;
86
97
  declare "current_bg_color": string | null;
98
+ /** Layout the background gradient was built for; a portrait↔landscape
99
+ switch rebuilds it even when the color is unchanged. */
100
+ declare "current_bg_layout": string | null;
87
101
  declare "data": Partial<StorymapData>;
88
102
  declare "options": StorySliderOptions;
89
103
  declare "animator": AnimationHandle | null;
90
104
  declare "animator_background": AnimationHandle | null;
91
- declare "fire": EventedInstance["fire"];
105
+ declare "fire": EventedInstance<StorySliderEvents>["fire"];
92
106
  declare "_loaded": boolean;
93
- declare "hasEventListeners": EventedInstance["hasEventListeners"];
107
+ /** Scroll-spy observer for `mode: "static"`; null in standard mode. */
108
+ declare "_static_observer": IntersectionObserver | null;
109
+ /** True while a scroll-spy navigation is settling (suppresses echo scrolls). */
110
+ declare "_static_spying": boolean;
94
111
 
95
112
  /* Private Methods
96
113
  ================================================== */
@@ -126,6 +143,7 @@ class StorySliderBase {
126
143
 
127
144
  // Current Background Color
128
145
  this.current_bg_color = null;
146
+ this.current_bg_layout = null;
129
147
 
130
148
  // Data Object
131
149
  this.data = {};
@@ -165,6 +183,8 @@ class StorySliderBase {
165
183
  // Animation Object
166
184
  this.animator = null;
167
185
  this.animator_background = null;
186
+ this._static_observer = null;
187
+ this._static_spying = false;
168
188
 
169
189
  // Preload scheduling handles, at most one of which is ever live
170
190
  this.preloadTimer = undefined;
@@ -250,6 +270,11 @@ class StorySliderBase {
250
270
  slide.addTo(this._el.slider_item_container);
251
271
  slide.on("added", this._onSlideAdded, this);
252
272
  slide.on("background_change", this._onBackgroundChange, this);
273
+ // a slide added after layout (editor preview, StoryMap.createSlide)
274
+ // joins the scroll-spy in static mode
275
+ if (this._isStatic()) {
276
+ this._initStaticSpy();
277
+ }
253
278
  }
254
279
 
255
280
  /* Message
@@ -299,6 +324,8 @@ class StorySliderBase {
299
324
  */
300
325
  dispose() {
301
326
  this._cancelPreload();
327
+ this._static_observer?.disconnect();
328
+ this._static_observer = null;
302
329
  if (this._swipable) {
303
330
  this._swipable.dispose();
304
331
  }
@@ -317,7 +344,7 @@ class StorySliderBase {
317
344
  this._slides = [];
318
345
  }
319
346
 
320
- goTo(n: number, fast?: boolean, displayupdate?: boolean) {
347
+ goTo(n: number, fast?: boolean, displayupdate?: boolean, fromScroll = false) {
321
348
  this.changeBackground({ color_value: "", image: false });
322
349
 
323
350
  // Clear Preloader Timer
@@ -344,13 +371,46 @@ class StorySliderBase {
344
371
  this._swipable.stopMomentum();
345
372
  }
346
373
 
347
- if (fast || prefersReducedMotion()) {
374
+ // Static reading mode: all slides are stacked in a scrollable
375
+ // list instead of a translating strip. Programmatic navigation
376
+ // scrolls the slide into view; scroll-spy navigation (fromScroll)
377
+ // only flips the active state, or scrolling would fight the
378
+ // visitor's own scroll position.
379
+ if (this._isStatic()) {
380
+ if (!fromScroll) {
381
+ this._static_spying = true;
382
+ this._scrollSlideIntoView(this.current_slide);
383
+ // longer than the smooth scroll it triggers: the
384
+ // observer must stay deaf until the list settles, or it
385
+ // echoes the programmatic scroll back as user input
386
+ window.setTimeout(() => {
387
+ this._static_spying = false;
388
+ }, 600);
389
+ }
390
+ // scroll-spy navigation must fire: the map follows the
391
+ // reading position through the change event (fromScroll only
392
+ // suppresses the echo scroll above, never the event)
393
+ this._onSlideChange(fromScroll ? false : displayupdate);
394
+ } else if (fast || prefersReducedMotion() || this.options.fxmode === "none") {
395
+ this._el.slider_container.style.opacity = "";
348
396
  this._el.slider_container.style.left = -(this.slide_spacing * n) + "px";
349
397
  this._onSlideChange(displayupdate);
398
+ } else if (this.options.fxmode === "fade") {
399
+ // fire the change event at animation start so the map and the
400
+ // slider animate simultaneously
401
+ this._onSlideChange(displayupdate);
402
+ this._el.slider_container.style.left = -(this.slide_spacing * n) + "px";
403
+ this._el.slider_container.style.opacity = "0";
404
+ this.animator = Animate(this._el.slider_container, {
405
+ opacity: 1,
406
+ duration: transition_duration,
407
+ easing: this.options.ease,
408
+ });
350
409
  } else {
351
410
  // fire the change event at animation start so the map and the
352
411
  // slider animate simultaneously
353
412
  this._onSlideChange(displayupdate);
413
+ this._el.slider_container.style.opacity = "";
354
414
  this.animator = Animate(this._el.slider_container, {
355
415
  left: -(this.slide_spacing * n) + "px",
356
416
  duration: transition_duration,
@@ -465,6 +525,11 @@ class StorySliderBase {
465
525
  }
466
526
 
467
527
  showNav(nav_obj: SlideNav, show: boolean) {
528
+ // `shownav: false` (slideshow shownav) hides the previous/next
529
+ // chrome; keyboard, swipe and dots navigation still work
530
+ if (this.options.shownav === false) {
531
+ show = false;
532
+ }
468
533
  if (this.options.width <= 500 && Browser.mobile) {
469
534
  // hidden on small mobile screens
470
535
  } else {
@@ -486,26 +551,50 @@ class StorySliderBase {
486
551
  const bg_alpha_end = "0.87";
487
552
  const _bg_old = this._el.background.getAttribute("style");
488
553
 
554
+ // Dark theme without an explicit slide background: fade from the
555
+ // themed panel color. White (the light default) would wash the
556
+ // dark slide; the existing darkness heuristics below (less fade,
557
+ // inverted nav icons) then behave as they do for dark author
558
+ // backgrounds.
559
+ const dark =
560
+ !bg.color_value &&
561
+ (this.options.theme === "dark" ||
562
+ (this.options.theme !== "light" &&
563
+ typeof window.matchMedia === "function" &&
564
+ window.matchMedia("(prefers-color-scheme: dark)").matches));
489
565
  if (bg.color_value) {
490
566
  // any CSS color; unparseable values fall back to the default
491
567
  bg_color = parseCssColor(bg.color_value) || this.options.default_bg_color;
568
+ } else if (dark) {
569
+ bg_color = { r: 34, g: 34, b: 34 };
492
570
  } else {
493
571
  bg_color = this.options.default_bg_color;
494
572
  }
495
573
 
496
- // Stop animation
497
- if (this.animator_background) {
498
- this.animator_background.stop();
499
- }
500
-
501
574
  const bg_color_rgb = bg_color.r + "," + bg_color.g + "," + bg_color.b;
502
575
 
503
- if (!this.current_bg_color || this.current_bg_color !== bg_color_rgb) {
576
+ if (
577
+ !this.current_bg_color ||
578
+ this.current_bg_color !== bg_color_rgb ||
579
+ this.current_bg_layout !== this.options.layout
580
+ ) {
504
581
  this.current_bg_color = bg_color_rgb;
582
+ this.current_bg_layout = this.options.layout;
505
583
  do_animation = true;
506
584
  }
507
585
 
508
586
  if (do_animation) {
587
+ // Stop the in-flight fade only when replacing it: an early
588
+ // no-op call (same color, e.g. the empty reset at the top of
589
+ // goTo before the active slide fires background_change) must not
590
+ // kill the fade another call just started — stop() commits the
591
+ // partial opacity inline and the finish handler never runs, so
592
+ // the gradient below would never be applied and the panel would
593
+ // keep the opaque stylesheet fallback.
594
+ if (this.animator_background) {
595
+ this.animator_background.stop();
596
+ }
597
+
509
598
  // Figure out CSS
510
599
  if (this.options.layout === "landscape") {
511
600
  this._nav.next.setColor(false);
@@ -536,6 +625,8 @@ class StorySliderBase {
536
625
  } else {
537
626
  if (bg.color_value) {
538
627
  bg_css += "background-color:" + bg.color_value + ";";
628
+ } else if (dark) {
629
+ bg_css += "background-color:#222;";
539
630
  } else {
540
631
  bg_css += "background-color:#FFF;";
541
632
  }
@@ -552,7 +643,7 @@ class StorySliderBase {
552
643
  // FADE OUT IN
553
644
  this.animator_background = Animate(this._el.background, {
554
645
  opacity: 0,
555
- duration: this.options.duration / 2,
646
+ duration: prefersReducedMotion() ? 0 : this.options.duration / 2,
556
647
  easing: this.options.ease,
557
648
  complete: () => {
558
649
  this.fadeInBackground(bg_css);
@@ -572,14 +663,85 @@ class StorySliderBase {
572
663
 
573
664
  this.animator_background = Animate(this._el.background, {
574
665
  opacity: 1,
575
- duration: this.options.duration / 2,
666
+ duration: prefersReducedMotion() ? 0 : this.options.duration / 2,
576
667
  easing: this.options.ease,
668
+ complete: () => {
669
+ // Fold the end state into the inline style so the panel does
670
+ // not depend on a retained fill-forwards animation to stay
671
+ // visible: once the fill is dropped (cancel, GC) the element
672
+ // would snap back to the inline opacity:0 above.
673
+ this._el.background.style.opacity = "1";
674
+ this.animator_background?.stop();
675
+ this.animator_background = null;
676
+ },
577
677
  });
578
678
  }
579
679
 
580
680
  /* Private Methods
581
681
  ================================================== */
582
682
 
683
+ /** True in the stacked reading mode (`mode: "static"`): no strip translation. */
684
+ _isStatic(): boolean {
685
+ return (this.options as { mode?: unknown }).mode === "static";
686
+ }
687
+
688
+ /**
689
+ * Scroll-spy for the static reading mode: the most visible stacked slide
690
+ * becomes current, so the map follows the reading position. Programmatic
691
+ * scrolls set `_static_spying` while settling and are ignored, or the
692
+ * observer would echo them back.
693
+ */
694
+ _initStaticSpy(): void {
695
+ if (typeof IntersectionObserver === "undefined") return;
696
+ this._static_observer?.disconnect();
697
+ // the scrolling element is the observer root: the mask scrolls WITH
698
+ // the content (it is an ancestor chain member, not the scrollport),
699
+ // so intersections against it would never change
700
+ const root = this._isStatic() ? this._el.container : this._el.slider_container_mask;
701
+ this._static_observer = new IntersectionObserver(
702
+ (entries) => {
703
+ if (this._static_spying) return;
704
+ let best = -1;
705
+ let bestRatio = 0;
706
+ for (const entry of entries) {
707
+ const index = Number((entry.target as HTMLElement).dataset?.slideIndex ?? -1);
708
+ if (entry.isIntersecting && entry.intersectionRatio > bestRatio) {
709
+ best = index;
710
+ bestRatio = entry.intersectionRatio;
711
+ }
712
+ }
713
+ if (best >= 0 && best !== this.current_slide) {
714
+ this.goTo(best, true, false, true);
715
+ }
716
+ },
717
+ { root, threshold: [0, 0.25, 0.5, 0.75, 1] },
718
+ );
719
+ for (const child of Array.from(this._el.slider_item_container.children)) {
720
+ const index = [...this._el.slider_item_container.children].indexOf(child);
721
+ (child as HTMLElement).dataset.slideIndex = String(index);
722
+ this._static_observer.observe(child);
723
+ }
724
+ }
725
+
726
+ /** Scroll a stacked slide into view without moving the host page. */
727
+ _scrollSlideIntoView(n: number): void {
728
+ const slide = this._el.slider_item_container.children[n] as HTMLElement | undefined;
729
+ // the scrollport in static mode is the storyslider element itself
730
+ const scroller = this._isStatic() ? this._el.container : this._el.slider_container_mask;
731
+ if (!slide || !scroller || typeof slide.offsetTop !== "number") return;
732
+ const container = this._el.slider_item_container;
733
+ const top =
734
+ (typeof container.offsetTop === "number" ? container.offsetTop : 0) + slide.offsetTop;
735
+ try {
736
+ scroller.scrollTo({
737
+ top,
738
+ behavior: prefersReducedMotion() ? "auto" : "smooth",
739
+ });
740
+ } catch {
741
+ scroller.scrollTop = top;
742
+ }
743
+ }
744
+
583
745
  // Update Display
584
746
  _updateDisplay(width?: number, height?: number, animate?: unknown, layout?: string) {
585
747
  let _layout;
@@ -617,9 +779,19 @@ class StorySliderBase {
617
779
  this._nav.previous.setPosition({ top: nav_pos });
618
780
 
619
781
  // Position slides
620
- for (let i = 0; i < this._slides.length; i++) {
621
- this._slides[i].updateDisplay(this.options.width, this.options.height, _layout);
622
- this._slides[i].setPosition({ left: this.slide_spacing * i, top: 0 });
782
+ if (this._isStatic()) {
783
+ // stacked reading mode: slides flow vertically (see .vco-static),
784
+ // so no strip positions — but keep the scroll-spy observing the
785
+ // current set of slides
786
+ for (let i = 0; i < this._slides.length; i++) {
787
+ this._slides[i].updateDisplay(this.options.width, this.options.height, _layout);
788
+ }
789
+ this._initStaticSpy();
790
+ } else {
791
+ for (let i = 0; i < this._slides.length; i++) {
792
+ this._slides[i].updateDisplay(this.options.width, this.options.height, _layout);
793
+ this._slides[i].setPosition({ left: this.slide_spacing * i, top: 0 });
794
+ }
623
795
  }
624
796
 
625
797
  // Go to the current slide
@@ -663,6 +835,10 @@ class StorySliderBase {
663
835
  ================================================== */
664
836
  _initLayout() {
665
837
  this._el.container.className += " vco-storyslider";
838
+ // Static reading mode: stacked slides with scroll-spy navigation
839
+ if (this._isStatic()) {
840
+ this._el.container.classList.add("vco-static");
841
+ }
666
842
 
667
843
  // Create Layout
668
844
  this._el.slider_container_mask = Dom.create(
@@ -796,13 +972,16 @@ class StorySliderBase {
796
972
  this.goTo(this.current_slide);
797
973
  }
798
974
 
799
- _onNavigation(e: { direction: string }) {
800
- if (e.direction === "next" || e.direction === "left") {
975
+ _onNavigation(e: { direction: string | null }) {
976
+ const direction = e.direction;
977
+ if (direction === "next" || direction === "left") {
801
978
  this.next();
802
- } else if (e.direction === "previous" || e.direction === "right") {
979
+ } else if (direction === "previous" || direction === "right") {
803
980
  this.previous();
981
+ } else {
982
+ return;
804
983
  }
805
- this.fire("nav_" + e.direction, this.data);
984
+ this.fire(`nav_${direction}`, this.data);
806
985
  }
807
986
 
808
987
  _onSlideAdded(e?: unknown) {
@@ -826,7 +1005,9 @@ class StorySliderBase {
826
1005
  }
827
1006
  }
828
1007
 
829
- export default class StorySlider extends Evented(StorySliderBase) {
1008
+ export default class StorySlider extends Evented<StorySliderEvents, typeof StorySliderBase>(
1009
+ StorySliderBase,
1010
+ ) {
830
1011
  constructor(...args: ConstructorParameters<typeof StorySliderBase>) {
831
1012
  super(...args);
832
1013
  }
@@ -142,10 +142,15 @@ function migrateLegacyKey(key: string): string {
142
142
  function consentButton(kind: "allow" | "deny", text: string): HTMLButtonElement {
143
143
  const button = Dom.create("button", `vco-consent-${kind}`) as HTMLButtonElement;
144
144
  button.setAttribute("type", "button");
145
+ // Text content provides the accessible name; native <button> is kept
146
+ // deliberately (no icon-only buttons, so no extra aria-label needed).
145
147
  button.textContent = text;
146
148
  return button;
147
149
  }
148
150
 
151
+ /** Counter backing the unique title ids for aria-labelledby (multi-viewer pages). */
152
+ let consentDialogCounter = 0;
153
+
149
154
  /** The title/message pair shared by both dialog shapes. */
150
155
  function consentHeader(
151
156
  titleText: string,
@@ -153,6 +158,11 @@ function consentHeader(
153
158
  ): { title: HTMLElement; message: HTMLElement } {
154
159
  const title = Dom.create("p", "vco-consent-title");
155
160
  title.textContent = titleText;
161
+ // Focus target on open: programmatically focusable, unique per dialog
162
+ // instance so aria-labelledby never collides across viewers.
163
+ title.setAttribute("tabindex", "-1");
164
+ consentDialogCounter += 1;
165
+ title.id = `vco-consent-title-${consentDialogCounter}`;
156
166
  const message = Dom.create("p", "vco-consent-message");
157
167
  message.textContent = messageText;
158
168
  return { title: title, message: message };
@@ -322,6 +332,11 @@ export class ConsentManager {
322
332
  messages.consent_start_title ?? "External content",
323
333
  messages.consent_start_message ?? "",
324
334
  );
335
+ // Non-modal library widget: exposed as a dialog without trapping
336
+ // focus or closing on Escape (no auto-decision for the visitor).
337
+ el.setAttribute("role", "dialog");
338
+ el.setAttribute("aria-modal", "false");
339
+ el.setAttribute("aria-labelledby", title.id);
325
340
 
326
341
  const list = Dom.create("div", "vco-consent-services");
327
342
  const toggles: Array<{ key: string; allow: HTMLButtonElement; deny: HTMLButtonElement }> =
@@ -377,6 +392,7 @@ export class ConsentManager {
377
392
  el.append(title, message, list, actions);
378
393
  container.append(el);
379
394
  this.startDialogs.add(el);
395
+ title.focus();
380
396
  }
381
397
 
382
398
  /**
@@ -400,6 +416,8 @@ export class ConsentManager {
400
416
  return new Promise((resolve) => {
401
417
  const messages = Language.messages;
402
418
  const el = Dom.create("div", "vco-consent");
419
+ // Non-modal library widget: exposed as a dialog without trapping
420
+ // focus or closing on Escape (no auto-decision for the visitor).
403
421
 
404
422
  const template = messages.consent_message ?? "Load content from {service}?";
405
423
  const { title, message } = consentHeader(
@@ -409,6 +427,9 @@ export class ConsentManager {
409
427
  messages.consent_start_title ?? "External content",
410
428
  template.replace("{service}", service.label) + (host ? ` (${host})` : ""),
411
429
  );
430
+ el.setAttribute("role", "dialog");
431
+ el.setAttribute("aria-modal", "false");
432
+ el.setAttribute("aria-labelledby", title.id);
412
433
 
413
434
  const buttons = Dom.create("div", "vco-consent-buttons");
414
435
  const allow = consentButton("allow", messages.consent_allow ?? "Allow");
@@ -425,6 +446,7 @@ export class ConsentManager {
425
446
  buttons.append(allow, deny);
426
447
  el.append(title, message, buttons);
427
448
  container.append(el);
449
+ title.focus();
428
450
  });
429
451
  }
430
452