@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.
- package/CHANGELOG +81 -0
- package/README.md +129 -12
- package/dist/assets/{demo-DBpeF31K.js → demo-bWnOyfUw.js} +1 -1
- package/dist/assets/harness-C3JUv5Ii.js +1 -0
- package/dist/assets/{harness-multi-CYHgI0a9.js → harness-multi-XZNex3tb.js} +1 -1
- package/dist/assets/{harness-support-Do7sIp2j.js → harness-support-CjfZIVeb.js} +1 -1
- package/dist/assets/main-DQ32hWBM.js +17 -0
- package/dist/context.json +13 -2
- package/dist/css/storymap.css +1 -1
- package/dist/demo.html +3 -2
- package/dist/docs/api/assets/navigation.js +1 -1
- package/dist/docs/api/assets/search.js +1 -1
- package/dist/docs/api/classes/StoryMap.html +777 -269
- package/dist/docs/api/functions/MediaType.html +1 -1
- package/dist/docs/api/functions/isPresentation3Collection.html +1 -1
- package/dist/docs/api/functions/isPresentation3Manifest.html +1 -1
- package/dist/docs/api/functions/isSlideshowCollection.html +51 -0
- package/dist/docs/api/functions/loadCSS.html +1 -1
- package/dist/docs/api/functions/manifestToStorymapData.html +1 -1
- package/dist/docs/api/functions/setLanguage.html +1 -1
- package/dist/docs/api/functions/slideshowToStorymapData.html +51 -0
- package/dist/docs/api/functions/slideshowVersion.html +48 -0
- package/dist/docs/api/functions/storymapToManifest.html +1 -1
- package/dist/docs/api/functions/validateStorymap.html +1 -1
- package/dist/docs/api/functions/validateStorymapAndReport.html +1 -1
- package/dist/docs/api/index.html +115 -12
- package/dist/docs/api/interfaces/AnimationHandle.html +2 -2
- package/dist/docs/api/interfaces/ImagereadyPayload.html +4 -4
- package/dist/docs/api/interfaces/LanguageEntry.html +6 -6
- package/dist/docs/api/interfaces/LatLngLiteral.html +4 -4
- package/dist/docs/api/interfaces/LoadOptions.html +2 -2
- package/dist/docs/api/interfaces/MapEvents.html +116 -0
- package/dist/docs/api/interfaces/MapMarkerEvents.html +66 -0
- package/dist/docs/api/interfaces/MarkerEventPayload.html +57 -0
- package/dist/docs/api/interfaces/MediaEvents.html +78 -0
- package/dist/docs/api/interfaces/MenuBarEvents.html +114 -0
- package/dist/docs/api/interfaces/MessageEvents.html +72 -0
- package/dist/docs/api/interfaces/OlLayer.html +1 -1
- package/dist/docs/api/interfaces/OlMap.html +1 -1
- package/dist/docs/api/interfaces/OlSource.html +1 -1
- package/dist/docs/api/interfaces/OlTileLayer.html +1 -1
- package/dist/docs/api/interfaces/OlVectorLayer.html +1 -1
- package/dist/docs/api/interfaces/OlView.html +1 -1
- package/dist/docs/api/interfaces/SlideBackgroundState.html +66 -0
- package/dist/docs/api/interfaces/SlideEvents.html +78 -0
- package/dist/docs/api/interfaces/SlideNavEvents.html +72 -0
- package/dist/docs/api/interfaces/SlideshowPlayerSettings.html +182 -0
- package/dist/docs/api/interfaces/SlideshowToStorymapResult.html +60 -0
- package/dist/docs/api/interfaces/SlideshowTranslatorOptions.html +65 -0
- package/dist/docs/api/interfaces/SlideshowWarning.html +72 -0
- package/dist/docs/api/interfaces/StoryMapEvents.html +146 -0
- package/dist/docs/api/interfaces/StoryMapMarker.html +763 -0
- package/dist/docs/api/interfaces/StorySliderEvents.html +102 -0
- package/dist/docs/api/interfaces/StorymapBasemap.html +69 -0
- package/dist/docs/api/interfaces/StorymapData.html +12 -6
- package/dist/docs/api/interfaces/StorymapDataWrapper.html +2 -2
- package/dist/docs/api/interfaces/StorymapDocument.html +2 -2
- package/dist/docs/api/interfaces/StorymapError.html +3 -3
- package/dist/docs/api/interfaces/StorymapGeoreference.html +5 -5
- package/dist/docs/api/interfaces/StorymapGeoreferenceBody.html +4 -4
- package/dist/docs/api/interfaces/StorymapGroundControlPoint.html +4 -4
- package/dist/docs/api/interfaces/StorymapLanguageMap.html +2 -2
- package/dist/docs/api/interfaces/StorymapManifest.html +12 -12
- package/dist/docs/api/interfaces/StorymapManifestAnnotation.html +43 -11
- package/dist/docs/api/interfaces/StorymapManifestBackground.html +6 -6
- package/dist/docs/api/interfaces/StorymapManifestCanvas.html +65 -17
- package/dist/docs/api/interfaces/StorymapManifestContentResource.html +8 -8
- package/dist/docs/api/interfaces/StorymapManifestFeatureCollection.html +4 -4
- package/dist/docs/api/interfaces/StorymapManifestGeoreferencing.html +7 -7
- package/dist/docs/api/interfaces/StorymapManifestImageService.html +4 -4
- package/dist/docs/api/interfaces/StorymapManifestRange.html +5 -5
- package/dist/docs/api/interfaces/StorymapManifestService.html +4 -4
- package/dist/docs/api/interfaces/StorymapManifestSpecificResource.html +4 -4
- package/dist/docs/api/interfaces/StorymapManifestStatement.html +3 -3
- package/dist/docs/api/interfaces/StorymapMapOptions.html +5 -5
- package/dist/docs/api/interfaces/StorymapOptions.html +277 -75
- package/dist/docs/api/interfaces/StorymapOverlayLayer.html +42 -11
- package/dist/docs/api/interfaces/StorymapSlide.html +70 -16
- package/dist/docs/api/interfaces/StorymapSlideBackground.html +4 -4
- package/dist/docs/api/interfaces/StorymapSlideLocation.html +58 -14
- package/dist/docs/api/interfaces/StorymapSlideMarker.html +8 -9
- package/dist/docs/api/interfaces/StorymapSlideMedia.html +49 -10
- package/dist/docs/api/interfaces/StorymapSlideNarration.html +61 -9
- package/dist/docs/api/interfaces/StorymapSlideText.html +4 -4
- package/dist/docs/api/interfaces/StorymapTilejson.html +8 -8
- package/dist/docs/api/interfaces/SwipableEvents.html +102 -0
- package/dist/docs/api/interfaces/TextEvents.html +66 -0
- package/dist/docs/api/media/events.md +66 -0
- package/dist/docs/api/media/migration-from-knightlab.md +89 -49
- package/dist/docs/api/media/slideshow.md +109 -0
- package/dist/docs/api/media/storymap-as-iiif-manifest.md +1007 -0
- package/dist/docs/api/modules.html +6 -6
- package/dist/docs/api/types/ContentState.html +3 -3
- package/dist/docs/api/types/FiredEvent.html +43 -0
- package/dist/docs/api/types/ImagereadyKind.html +1 -1
- package/dist/docs/api/types/MediaTypeMatch.html +5 -5
- package/dist/docs/api/types/SwipeDirection.html +36 -0
- package/dist/docs/api/types/TileSourceFactory.html +1 -1
- package/dist/docs/authoring.html +20 -7
- package/dist/docs/events.html +263 -0
- package/dist/docs/iiif.html +136 -22
- package/dist/docs/migration.html +66 -19
- package/dist/docs/readme.html +113 -12
- package/dist/embed/index.html +7 -0
- package/dist/examples/font-css-false.json +33 -0
- package/dist/examples/issue-image-region.json +1 -1
- package/dist/examples/layers-control.json +60 -0
- package/dist/examples/map-template-provider.json +36 -0
- package/dist/examples/multi-media.json +71 -0
- package/dist/examples-iiif/annotated-image.json +16 -0
- package/dist/examples-iiif/font-css-false.json +114 -0
- package/dist/examples-iiif/issue-image-region.json +1 -1
- package/dist/examples-iiif/layers-control.json +226 -0
- package/dist/examples-iiif/map-template-provider.json +133 -0
- package/dist/examples-iiif/multi-media.json +237 -0
- package/dist/examples-slideshow/single.json +79 -0
- package/dist/examples-slideshow/static/pixel.png +0 -0
- package/dist/examples-slideshow/static.json +44 -0
- package/dist/harness-multi.html +3 -3
- package/dist/harness.html +4 -3
- package/dist/index.html +3 -2
- package/dist/js/storymap.d.ts +1326 -289
- package/dist/js/storymap.js +6690 -4784
- package/dist/js/storymap.js.map +1 -1
- package/dist/navplace-properties.json +14 -0
- package/dist/site.css +54 -36
- package/docs/migration-from-knightlab.md +89 -49
- package/docs/storymap-as-iiif-manifest.md +199 -126
- package/package.json +4 -2
- package/schema/storymap.schema.json +334 -34
- package/src/core/Util.ts +3 -1
- package/src/core/mixins.ts +42 -28
- package/src/language/Language.ts +7 -0
- package/src/language/locale/.expected-gaps.json +287 -27
- package/src/language/locale/de.json +11 -1
- package/src/language/locale/en.json +11 -1
- package/src/main.ts +51 -0
- package/src/map/Map.ts +44 -6
- package/src/map/MapMarker.ts +16 -3
- package/src/map/openlayers/Map.OpenLayers.ts +787 -62
- package/src/map/openlayers/MapMarker.OpenLayers.ts +113 -13
- package/src/map/slideView.ts +149 -0
- package/src/map/types.ts +2 -0
- package/src/media/EmbedUtil.ts +27 -2
- package/src/media/Media.ts +27 -4
- package/src/media/embedId.ts +6 -0
- package/src/media/types/Flickr.ts +14 -3
- package/src/media/types/HtmlMedia.ts +66 -0
- package/src/media/types/Image.ts +15 -3
- package/src/media/types/SoundCloud.ts +1 -1
- package/src/media/types/Text.ts +27 -8
- package/src/media/types/Twitter.ts +9 -7
- package/src/media/types/Vimeo.ts +1 -1
- package/src/media/types/YouTube.ts +2 -2
- package/src/scss/Typography.scss +15 -15
- package/src/scss/VCO.StoryMap.scss +173 -8
- package/src/scss/core/Reset.scss +5 -18
- package/src/scss/map/VCO.Map.scss +1 -1
- package/src/scss/map/VCO.MapMarker.scss +20 -9
- package/src/scss/map/openlayers/VCO.Map.OpenLayers.scss +7 -4
- package/src/scss/media/VCO.Media.scss +7 -7
- package/src/scss/media/types/VCO.Media.Blockquote.scss +1 -1
- package/src/scss/media/types/VCO.Media.Text.scss +5 -5
- package/src/scss/media/types/VCO.Media.Twitter.scss +4 -4
- package/src/scss/media/types/VCO.Media.Wikipedia.scss +2 -19
- package/src/scss/site/site.scss +55 -36
- package/src/scss/slider/VCO.Slide.scss +49 -11
- package/src/scss/slider/VCO.SlideNav.scss +23 -7
- package/src/scss/slider/VCO.StorySlider.scss +50 -0
- package/src/scss/snapmap/VCO.SnapMap.scss +3 -3
- package/src/scss/ui/VCO.Consent.scss +20 -14
- package/src/scss/ui/VCO.Layers.scss +119 -0
- package/src/scss/ui/VCO.MenuBar.Button.scss +140 -19
- package/src/scss/ui/VCO.MenuBar.scss +9 -2
- package/src/scss/ui/VCO.Message.scss +14 -0
- package/src/slider/Slide.ts +232 -74
- package/src/slider/SlideNav.ts +15 -4
- package/src/slider/StorySlider.ts +203 -22
- package/src/storymap/Consent.ts +22 -0
- package/src/storymap/StoryMap.ts +944 -131
- package/src/storymap/content-state.ts +3 -8
- package/src/storymap/font-css.ts +72 -0
- package/src/storymap/from-slideshow.ts +785 -0
- package/src/storymap/iiif-shared.ts +228 -0
- package/src/storymap/iiif.ts +289 -100
- package/src/storymap/to-iiif.ts +131 -44
- package/src/types.ts +382 -17
- package/src/ui/LayersControl.ts +302 -0
- package/src/ui/MenuBar.ts +232 -21
- package/src/ui/Message.ts +36 -3
- package/src/ui/Swipable.ts +25 -7
- package/dist/assets/harness-Cr0rYpvt.js +0 -1
- package/dist/assets/main-DXalctuR.js +0 -17
- package/src/scss/VCO.StoryMap.Dark.scss +0 -150
- 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.
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
975
|
+
_onNavigation(e: { direction: string | null }) {
|
|
976
|
+
const direction = e.direction;
|
|
977
|
+
if (direction === "next" || direction === "left") {
|
|
801
978
|
this.next();
|
|
802
|
-
} else if (
|
|
979
|
+
} else if (direction === "previous" || direction === "right") {
|
|
803
980
|
this.previous();
|
|
981
|
+
} else {
|
|
982
|
+
return;
|
|
804
983
|
}
|
|
805
|
-
this.fire(
|
|
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(
|
|
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
|
}
|
package/src/storymap/Consent.ts
CHANGED
|
@@ -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
|
|