@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
package/src/slider/Slide.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { mergeData } from "../core/Util";
|
|
1
|
+
import { mergeData, prefersReducedMotion } from "../core/Util";
|
|
2
2
|
import { DomMixed, Evented, type EventedInstance } from "../core/mixins";
|
|
3
3
|
import { DomEvent } from "../dom/DomEvent";
|
|
4
4
|
import Dom from "../dom/Dom";
|
|
5
5
|
import { easeInSpline } from "../animation/easings";
|
|
6
6
|
import MediaType from "../media/MediaType";
|
|
7
|
+
import type { MediaEvents } from "../media/Media";
|
|
7
8
|
import { sanitizeSlideText } from "../media/EmbedUtil";
|
|
8
9
|
import Text from "../media/types/Text";
|
|
9
10
|
import { Browser } from "../core/Browser";
|
|
@@ -50,9 +51,26 @@ interface MediaInstance {
|
|
|
50
51
|
stopMedia: () => void;
|
|
51
52
|
updateDisplay: (w?: number, h?: number, l?: string) => void;
|
|
52
53
|
dispose?: () => void;
|
|
53
|
-
on?: EventedInstance["on"];
|
|
54
|
-
off?: EventedInstance["off"];
|
|
54
|
+
on?: EventedInstance<MediaEvents>["on"];
|
|
55
|
+
off?: EventedInstance<MediaEvents>["off"];
|
|
55
56
|
_state?: { loaded?: boolean; eager?: boolean };
|
|
57
|
+
/** Per-item consent identity: each item is built with its own
|
|
58
|
+
`media_name`/`media_type`, which `hasPlayableMedia` reads back (#358). */
|
|
59
|
+
options?: { media_type?: string };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SlideBackgroundState {
|
|
63
|
+
image: boolean;
|
|
64
|
+
color: boolean;
|
|
65
|
+
color_value: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface SlideEvents {
|
|
69
|
+
background_change: SlideBackgroundState;
|
|
70
|
+
call_to_action: Event;
|
|
71
|
+
loaded: StorymapSlide;
|
|
72
|
+
added: StorymapSlide;
|
|
73
|
+
removed: StorymapSlide;
|
|
56
74
|
}
|
|
57
75
|
|
|
58
76
|
interface SlideHas {
|
|
@@ -60,32 +78,33 @@ interface SlideHas {
|
|
|
60
78
|
text: boolean;
|
|
61
79
|
media: boolean;
|
|
62
80
|
title: boolean;
|
|
63
|
-
background:
|
|
64
|
-
image: boolean;
|
|
65
|
-
color: boolean;
|
|
66
|
-
color_value: string;
|
|
67
|
-
};
|
|
81
|
+
background: SlideBackgroundState;
|
|
68
82
|
}
|
|
69
83
|
|
|
70
84
|
class SlideBase {
|
|
71
85
|
/** `call_to_action` is only created when the slide shows one. */
|
|
72
86
|
declare "_el": SlideElements;
|
|
87
|
+
/** The primary media item; always `_medias[0]` when present (#358). */
|
|
73
88
|
declare "_media": MediaInstance | null;
|
|
74
|
-
|
|
89
|
+
/** Every media item: `[media, ...media_extra]` with a non-empty url. */
|
|
90
|
+
declare "_medias": MediaInstance[];
|
|
75
91
|
declare "_text": Text;
|
|
76
92
|
declare "_state": { loaded: boolean };
|
|
77
93
|
declare "_scroll_hint": HTMLElement | null;
|
|
78
94
|
declare "_scroll_hint_dismissed": boolean;
|
|
79
95
|
declare "_onSlideScrollBound": EventListener;
|
|
80
|
-
/** Every `media_ended` handler registered via `onMediaEnded`,
|
|
81
|
-
|
|
96
|
+
/** Every `media_ended` handler registered via `onMediaEnded`, with the
|
|
97
|
+
media it was attached to so `dispose()` can remove them. */
|
|
98
|
+
declare "_media_ended_fns": { media: MediaInstance; fn: () => void }[];
|
|
99
|
+
/** The `media_loaded` handlers below, kept for the same reason. */
|
|
100
|
+
declare "_media_loaded_fns": { media: MediaInstance; fn: () => void }[];
|
|
82
101
|
declare "has": SlideHas;
|
|
83
102
|
declare "title": string;
|
|
84
103
|
declare "data": StorymapSlide;
|
|
85
104
|
declare "options": SlideOptions;
|
|
86
105
|
declare "active": boolean;
|
|
87
106
|
declare "animator": unknown;
|
|
88
|
-
declare "fire": EventedInstance["fire"];
|
|
107
|
+
declare "fire": EventedInstance<SlideEvents>["fire"];
|
|
89
108
|
declare "onLoaded": () => void;
|
|
90
109
|
|
|
91
110
|
//_el: {},
|
|
@@ -105,7 +124,7 @@ class SlideBase {
|
|
|
105
124
|
|
|
106
125
|
// Components
|
|
107
126
|
this._media = null;
|
|
108
|
-
this.
|
|
127
|
+
this._medias = [];
|
|
109
128
|
this._text = {} as Text;
|
|
110
129
|
|
|
111
130
|
// State
|
|
@@ -161,6 +180,7 @@ class SlideBase {
|
|
|
161
180
|
|
|
162
181
|
this._onSlideScrollBound = null as unknown as EventListener;
|
|
163
182
|
this._media_ended_fns = [];
|
|
183
|
+
this._media_loaded_fns = [];
|
|
164
184
|
mergeData(this.options, options);
|
|
165
185
|
mergeData(this.data, data);
|
|
166
186
|
|
|
@@ -195,13 +215,17 @@ class SlideBase {
|
|
|
195
215
|
}
|
|
196
216
|
this._el.container?.getAnimations?.().forEach((a) => a.cancel());
|
|
197
217
|
// the media_ended handlers registered via onMediaEnded() live on the
|
|
198
|
-
// media
|
|
218
|
+
// media objects, which outlive this slide once nulled — remove them
|
|
199
219
|
// here or a disposed slide's advance closure fires on a later ended
|
|
200
|
-
|
|
201
|
-
|
|
220
|
+
this.clearMediaEnded();
|
|
221
|
+
for (const { media, fn } of this._media_loaded_fns) {
|
|
222
|
+
media.off?.("media_loaded", fn);
|
|
202
223
|
}
|
|
203
|
-
this.
|
|
204
|
-
this.
|
|
224
|
+
this._media_loaded_fns = [];
|
|
225
|
+
for (const media of this._medias) {
|
|
226
|
+
media.dispose?.();
|
|
227
|
+
}
|
|
228
|
+
this._medias = [];
|
|
205
229
|
this._media = null;
|
|
206
230
|
this._el.container?.remove();
|
|
207
231
|
}
|
|
@@ -209,6 +233,19 @@ class SlideBase {
|
|
|
209
233
|
setActive(is_active: boolean) {
|
|
210
234
|
this.active = is_active;
|
|
211
235
|
|
|
236
|
+
// Off-screen slides are translated away but stay in the DOM, so a
|
|
237
|
+
// screen reader would read every slide's headline, body and caption.
|
|
238
|
+
// Hide inactive slides from assistive tech next to the flip that
|
|
239
|
+
// already marks them inactive; `inert` additionally drops their
|
|
240
|
+
// controls from keyboard reach. Neither affects loading: preloaded
|
|
241
|
+
// media (including iframes) still builds and fetches underneath.
|
|
242
|
+
this._el.container.setAttribute("aria-hidden", String(!is_active));
|
|
243
|
+
if (is_active) {
|
|
244
|
+
this._el.container.removeAttribute("inert");
|
|
245
|
+
} else {
|
|
246
|
+
this._el.container.setAttribute("inert", "");
|
|
247
|
+
}
|
|
248
|
+
|
|
212
249
|
if (this.active) {
|
|
213
250
|
if (this.data.background) {
|
|
214
251
|
this.fire("background_change", this.has.background);
|
|
@@ -219,8 +256,10 @@ class SlideBase {
|
|
|
219
256
|
// the active slide's images load eagerly: media built after this
|
|
220
257
|
// point (the 1200ms load timer) honors the flag, already-built
|
|
221
258
|
// images are upgraded below
|
|
222
|
-
|
|
223
|
-
|
|
259
|
+
for (const media of this._medias) {
|
|
260
|
+
if (media._state) {
|
|
261
|
+
media._state.eager = true;
|
|
262
|
+
}
|
|
224
263
|
}
|
|
225
264
|
this.loadMedia();
|
|
226
265
|
this._eagerLoadImages();
|
|
@@ -236,8 +275,10 @@ class SlideBase {
|
|
|
236
275
|
}
|
|
237
276
|
|
|
238
277
|
loadMedia() {
|
|
239
|
-
if (this.
|
|
240
|
-
this.
|
|
278
|
+
if (this._medias.length > 0 && !this._state.loaded) {
|
|
279
|
+
for (const media of this._medias) {
|
|
280
|
+
media.loadMedia();
|
|
281
|
+
}
|
|
241
282
|
this._state.loaded = true;
|
|
242
283
|
}
|
|
243
284
|
}
|
|
@@ -254,29 +295,62 @@ class SlideBase {
|
|
|
254
295
|
}
|
|
255
296
|
|
|
256
297
|
stopMedia() {
|
|
257
|
-
if (this.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
(
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
298
|
+
if (this._medias.length > 0 && this._state.loaded) {
|
|
299
|
+
for (const media of this._medias) {
|
|
300
|
+
// An audio bed that outlives its slide
|
|
301
|
+
// (`media.stopOnExit: false`, slideshow ambient audio) keeps
|
|
302
|
+
// playing; StoryMap stops it when another slide claims audio.
|
|
303
|
+
// Slideshow-only: internal documents always stop.
|
|
304
|
+
const mediaData = (media as unknown as { data?: { stopOnExit?: boolean } }).data;
|
|
305
|
+
const sourced =
|
|
306
|
+
(this.options as { slideshow_source?: unknown }).slideshow_source === true;
|
|
307
|
+
if (sourced && mediaData?.stopOnExit === false) continue;
|
|
308
|
+
try {
|
|
309
|
+
media.stopMedia();
|
|
310
|
+
} catch (e: unknown) {
|
|
311
|
+
// Some sort of race condition or other ordering condition can cause
|
|
312
|
+
// an error when the preview tab is selected in the editor due to
|
|
313
|
+
// the stopped media not being properly formed.
|
|
314
|
+
if (
|
|
315
|
+
(e as Error).message ===
|
|
316
|
+
"this._el.content_item.querySelector is not a function"
|
|
317
|
+
) {
|
|
318
|
+
console.warn("Ignoring error in editor context: " + (e as Error).message);
|
|
319
|
+
} else {
|
|
320
|
+
throw e;
|
|
321
|
+
}
|
|
270
322
|
}
|
|
271
323
|
}
|
|
272
|
-
// If
|
|
273
|
-
//
|
|
274
|
-
if (!this.
|
|
324
|
+
// If no media load started (pending timers were cancelled on a
|
|
325
|
+
// quick pass-through), allow a revisit to retry
|
|
326
|
+
if (!this._medias.some((media) => media._state?.loaded)) {
|
|
275
327
|
this._state.loaded = false;
|
|
276
328
|
}
|
|
277
329
|
}
|
|
278
330
|
}
|
|
279
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Stop this slide's ambient audio beds (`media.stopOnExit: false`) —
|
|
334
|
+
* called by the viewer when another slide claims the audio, not on every
|
|
335
|
+
* deactivation (see `stopMedia`, which skips them).
|
|
336
|
+
*/
|
|
337
|
+
stopAmbientMedia() {
|
|
338
|
+
// Slideshow-only by construction (see stopMedia): without the source
|
|
339
|
+
// flag no media can be ambient, so there is nothing to stop.
|
|
340
|
+
if ((this.options as { slideshow_source?: unknown }).slideshow_source !== true) return;
|
|
341
|
+
if (this._medias.length > 0 && this._state.loaded) {
|
|
342
|
+
for (const media of this._medias) {
|
|
343
|
+
const mediaData = (media as unknown as { data?: { stopOnExit?: boolean } }).data;
|
|
344
|
+
if (mediaData?.stopOnExit !== false) continue;
|
|
345
|
+
try {
|
|
346
|
+
media.stopMedia();
|
|
347
|
+
} catch {
|
|
348
|
+
// same editor-context race stopMedia() already tolerates
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
280
354
|
getBackground() {
|
|
281
355
|
return this.has.background;
|
|
282
356
|
}
|
|
@@ -287,20 +361,49 @@ class SlideBase {
|
|
|
287
361
|
* its timer; everything else keeps the timer.
|
|
288
362
|
*/
|
|
289
363
|
hasPlayableMedia(): boolean {
|
|
290
|
-
|
|
291
|
-
|
|
364
|
+
if (this._medias.length === 0) {
|
|
365
|
+
const kind = this.options.media_type as string | undefined;
|
|
366
|
+
return kind === "audio" || kind === "video";
|
|
367
|
+
}
|
|
368
|
+
return this._medias.some((media) => {
|
|
369
|
+
const kind = media.options?.media_type;
|
|
370
|
+
return kind === "audio" || kind === "video";
|
|
371
|
+
});
|
|
292
372
|
}
|
|
293
373
|
|
|
294
374
|
/**
|
|
295
375
|
* Call `fn` when this slide's media finishes playing. A slide with no
|
|
296
376
|
* media — or media that cannot end — never calls it, which is why
|
|
297
377
|
* `autoplay_media` keeps its timer as a fallback rather than waiting on
|
|
298
|
-
* an event that may not arrive.
|
|
378
|
+
* an event that may not arrive. Multi-media slides never call it either:
|
|
379
|
+
* waiting for the last of several items to end would stall autoplay, so
|
|
380
|
+
* the timer stays the advance for those (#358).
|
|
299
381
|
*/
|
|
300
382
|
onMediaEnded(fn: () => void): void {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
383
|
+
const playable = this._medias.filter((media) => {
|
|
384
|
+
const kind = media.options?.media_type;
|
|
385
|
+
return (kind === "audio" || kind === "video") && media.on;
|
|
386
|
+
});
|
|
387
|
+
if (playable.length !== 1) return;
|
|
388
|
+
playable[0].on?.("media_ended", fn);
|
|
389
|
+
this._media_ended_fns.push({ media: playable[0], fn });
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Drop `media_ended` handlers armed by `onMediaEnded()` without waiting
|
|
394
|
+
* for dispose: re-arming (autoplay revisit) or navigating away must not
|
|
395
|
+
* accumulate stale advance closures — an ambient bed outliving its slide
|
|
396
|
+
* would otherwise fire a previous slide's advance on ending.
|
|
397
|
+
*/
|
|
398
|
+
clearMediaEnded(): void {
|
|
399
|
+
for (const { media, fn } of this._media_ended_fns) {
|
|
400
|
+
try {
|
|
401
|
+
media.off?.("media_ended", fn);
|
|
402
|
+
} catch {
|
|
403
|
+
// same editor-context race stopMedia() already tolerates
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
this._media_ended_fns = [];
|
|
304
407
|
}
|
|
305
408
|
|
|
306
409
|
scrollToTop() {
|
|
@@ -327,7 +430,10 @@ class SlideBase {
|
|
|
327
430
|
return;
|
|
328
431
|
}
|
|
329
432
|
if (!this._scroll_hint) {
|
|
330
|
-
|
|
433
|
+
// a real button for keyboard/AT operability (issue #385 wave)
|
|
434
|
+
this._scroll_hint = Dom.create("button", "vco-slide-scroll-hint", el);
|
|
435
|
+
this._scroll_hint.setAttribute("type", "button");
|
|
436
|
+
this._scroll_hint.setAttribute("aria-label", "Scroll down");
|
|
331
437
|
this._scroll_hint.innerHTML = "<span class='vco-icon-arrow-down'></span>";
|
|
332
438
|
DomEvent.addListener(this._scroll_hint, "click", this._onScrollHintClick, this);
|
|
333
439
|
}
|
|
@@ -343,7 +449,10 @@ class SlideBase {
|
|
|
343
449
|
_onScrollHintClick() {
|
|
344
450
|
// scroll down one step, then hide (the scroll event dismisses too)
|
|
345
451
|
const el = this._el.container;
|
|
346
|
-
el.scrollBy({
|
|
452
|
+
el.scrollBy({
|
|
453
|
+
top: el.clientHeight * 0.8,
|
|
454
|
+
behavior: prefersReducedMotion() ? "auto" : "smooth",
|
|
455
|
+
});
|
|
347
456
|
this._scroll_hint_dismissed = true;
|
|
348
457
|
this._hideScrollHint();
|
|
349
458
|
}
|
|
@@ -359,11 +468,14 @@ class SlideBase {
|
|
|
359
468
|
}
|
|
360
469
|
|
|
361
470
|
addCallToAction(str: string) {
|
|
471
|
+
// a real button for keyboard/AT operability (issue #385 wave), like
|
|
472
|
+
// the menubar buttons; the classes carry the look
|
|
362
473
|
this._el.call_to_action = Dom.create(
|
|
363
|
-
"
|
|
474
|
+
"button",
|
|
364
475
|
"vco-slide-calltoaction",
|
|
365
476
|
this._el.content_container,
|
|
366
477
|
);
|
|
478
|
+
this._el.call_to_action.setAttribute("type", "button");
|
|
367
479
|
// The text comes from `call_to_action_text` in the storymap JSON, so
|
|
368
480
|
// it goes through the sanitizer like every other author string
|
|
369
481
|
// instead of being concatenated into markup.
|
|
@@ -429,8 +541,13 @@ class SlideBase {
|
|
|
429
541
|
}
|
|
430
542
|
}
|
|
431
543
|
|
|
432
|
-
// Determine Assets for layout and loading
|
|
433
|
-
|
|
544
|
+
// Determine Assets for layout and loading. A slide has media when
|
|
545
|
+
// any item — the primary or an extra — carries a non-empty url, so
|
|
546
|
+
// an empty `media.url` still yields false (#358).
|
|
547
|
+
const slide_media_items = [this.data.media, ...(this.data.media_extra ?? [])].filter(
|
|
548
|
+
(item): item is NonNullable<StorymapSlide["media"]> => !!item?.url,
|
|
549
|
+
);
|
|
550
|
+
if (slide_media_items.length > 0) {
|
|
434
551
|
this.has.media = true;
|
|
435
552
|
}
|
|
436
553
|
if (this.data.text && this.data.text.text) {
|
|
@@ -441,19 +558,35 @@ class SlideBase {
|
|
|
441
558
|
this.title = this.data.text.headline;
|
|
442
559
|
}
|
|
443
560
|
|
|
444
|
-
// Create Media
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
561
|
+
// Create Media (#358): the primary item plus every extra with a
|
|
562
|
+
// non-empty url, in order. Each item gets its own MediaType match and
|
|
563
|
+
// its own consent identity (media_name/media_type), so per-slide
|
|
564
|
+
// consent asks and the audio badge tell items apart.
|
|
565
|
+
const media_items = slide_media_items;
|
|
566
|
+
if (media_items.length > 0) {
|
|
567
|
+
for (const item of media_items) {
|
|
568
|
+
// Determine the media type
|
|
569
|
+
item.mediatype = MediaType(item) as MediaTypeMatch;
|
|
570
|
+
// Item 0 keeps the historical write-back: the marker icon
|
|
571
|
+
// class reads it off the slide data.
|
|
572
|
+
if (item === media_items[0]) {
|
|
573
|
+
this.options.media_name = item.mediatype.name;
|
|
574
|
+
this.options.media_type = item.mediatype.type;
|
|
575
|
+
}
|
|
576
|
+
// Create a media object using the matched class name
|
|
577
|
+
const media = new item.mediatype.cls(item, {
|
|
578
|
+
...this.options,
|
|
579
|
+
media_name: item.mediatype.name,
|
|
580
|
+
media_type: item.mediatype.type,
|
|
581
|
+
}) as MediaInstance;
|
|
582
|
+
// loaded media changes the content height — the scroll hint
|
|
583
|
+
// may appear or disappear
|
|
584
|
+
const on_loaded = () => this._updateScrollHint();
|
|
585
|
+
media.on?.("media_loaded", on_loaded);
|
|
586
|
+
this._media_loaded_fns.push({ media, fn: on_loaded });
|
|
587
|
+
this._medias.push(media);
|
|
588
|
+
}
|
|
589
|
+
this._media = this._medias[0] ?? null;
|
|
457
590
|
}
|
|
458
591
|
|
|
459
592
|
// Create Text
|
|
@@ -461,20 +594,37 @@ class SlideBase {
|
|
|
461
594
|
this._text = new Text(this.data.text, {
|
|
462
595
|
title: this.has.title,
|
|
463
596
|
text_align: this.options.text_align as string | undefined,
|
|
597
|
+
language: this.data.language ?? undefined,
|
|
464
598
|
});
|
|
465
599
|
}
|
|
466
600
|
|
|
467
601
|
// Add to DOM. Each branch has just established the members it uses
|
|
468
|
-
// (has.media ->
|
|
602
|
+
// (has.media -> _medias, has.text/headline -> _text). The media loop
|
|
603
|
+
// preserves the historical ordering: media-then-text, or
|
|
604
|
+
// text-then-media when there is a headline but no body text.
|
|
605
|
+
const medias = this._medias;
|
|
606
|
+
if (medias.length > 1) {
|
|
607
|
+
// `row` seats two items side by side; anything else stacks (D4)
|
|
608
|
+
const layout =
|
|
609
|
+
this.data.media_layout === "row" && medias.length === 2 ? "row" : "stack";
|
|
610
|
+
this._el.container.classList.add("vco-slide-media-multiple");
|
|
611
|
+
this._el.container.setAttribute("data-layout", layout);
|
|
612
|
+
}
|
|
469
613
|
if (!this.has.text && !this.has.headline && this.has.media) {
|
|
470
614
|
this._el.container.className += " vco-slide-media-only";
|
|
471
|
-
|
|
615
|
+
for (const media of medias) {
|
|
616
|
+
media.addTo(this._el.content);
|
|
617
|
+
}
|
|
472
618
|
} else if (this.has.headline && this.has.media && !this.has.text) {
|
|
473
619
|
this._el.container.className += " vco-slide-media-only";
|
|
474
620
|
this._text.addTo(this._el.content);
|
|
475
|
-
|
|
621
|
+
for (const media of medias) {
|
|
622
|
+
media.addTo(this._el.content);
|
|
623
|
+
}
|
|
476
624
|
} else if (this.has.text && this.has.media) {
|
|
477
|
-
|
|
625
|
+
for (const media of medias) {
|
|
626
|
+
media.addTo(this._el.content);
|
|
627
|
+
}
|
|
478
628
|
this._text.addTo(this._el.content);
|
|
479
629
|
} else if (this.has.text || this.has.headline) {
|
|
480
630
|
this._el.container.className += " vco-slide-text-only";
|
|
@@ -530,15 +680,19 @@ class SlideBase {
|
|
|
530
680
|
this.options.height = this._el.container.offsetHeight;
|
|
531
681
|
}
|
|
532
682
|
|
|
533
|
-
if (this.
|
|
683
|
+
if (this._medias.length > 0) {
|
|
534
684
|
if (!this.has.text && this.has.headline) {
|
|
535
|
-
this.
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
685
|
+
for (const media of this._medias) {
|
|
686
|
+
media.updateDisplay(
|
|
687
|
+
this.options.width,
|
|
688
|
+
this.options.height - this._text.headlineHeight(),
|
|
689
|
+
layout,
|
|
690
|
+
);
|
|
691
|
+
}
|
|
540
692
|
} else {
|
|
541
|
-
|
|
693
|
+
for (const media of this._medias) {
|
|
694
|
+
media.updateDisplay(this.options.width, this.options.height, layout);
|
|
695
|
+
}
|
|
542
696
|
}
|
|
543
697
|
}
|
|
544
698
|
|
|
@@ -547,7 +701,11 @@ class SlideBase {
|
|
|
547
701
|
}
|
|
548
702
|
}
|
|
549
703
|
|
|
550
|
-
|
|
704
|
+
const EventedSlideBase = Evented<SlideEvents, typeof SlideBase>(SlideBase);
|
|
705
|
+
|
|
706
|
+
export default class Slide extends DomMixed<SlideEvents, typeof EventedSlideBase>(
|
|
707
|
+
EventedSlideBase,
|
|
708
|
+
) {
|
|
551
709
|
constructor(...args: ConstructorParameters<typeof SlideBase>) {
|
|
552
710
|
super(...args);
|
|
553
711
|
}
|
package/src/slider/SlideNav.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mergeData } from "../core/Util";
|
|
1
|
+
import { mergeData, prefersReducedMotion } from "../core/Util";
|
|
2
2
|
import { DomMixed, Evented, type EventedInstance } from "../core/mixins";
|
|
3
3
|
import Dom from "../dom/Dom";
|
|
4
4
|
import Animate from "../animation/tween";
|
|
@@ -22,6 +22,13 @@ interface SlideNavOptions {
|
|
|
22
22
|
[key: string]: unknown;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export interface SlideNavEvents {
|
|
26
|
+
clicked: SlideNavOptions;
|
|
27
|
+
loaded: SlideNavData;
|
|
28
|
+
added: SlideNavData;
|
|
29
|
+
removed: SlideNavData;
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
class SlideNavBase {
|
|
26
33
|
declare "_el": Record<string, HTMLElement>;
|
|
27
34
|
declare "mediatype": unknown;
|
|
@@ -29,7 +36,7 @@ class SlideNavBase {
|
|
|
29
36
|
declare "options": SlideNavOptions;
|
|
30
37
|
declare "animator": AnimationHandle | null;
|
|
31
38
|
declare "animator_position": AnimationHandle | null;
|
|
32
|
-
declare "fire": EventedInstance["fire"];
|
|
39
|
+
declare "fire": EventedInstance<SlideNavEvents>["fire"];
|
|
33
40
|
|
|
34
41
|
//_el: {},
|
|
35
42
|
|
|
@@ -112,7 +119,7 @@ class SlideNavBase {
|
|
|
112
119
|
return_to_default: boolean,
|
|
113
120
|
) {
|
|
114
121
|
const ani: AnimateOptions = {
|
|
115
|
-
duration: duration,
|
|
122
|
+
duration: prefersReducedMotion() ? 0 : duration,
|
|
116
123
|
easing: ease,
|
|
117
124
|
complete: () => {
|
|
118
125
|
this._onUpdatePositionComplete(return_to_default);
|
|
@@ -239,7 +246,11 @@ class SlideNavBase {
|
|
|
239
246
|
}
|
|
240
247
|
}
|
|
241
248
|
|
|
242
|
-
|
|
249
|
+
const EventedSlideNavBase = Evented<SlideNavEvents, typeof SlideNavBase>(SlideNavBase);
|
|
250
|
+
|
|
251
|
+
export default class SlideNav extends DomMixed<SlideNavEvents, typeof EventedSlideNavBase>(
|
|
252
|
+
EventedSlideNavBase,
|
|
253
|
+
) {
|
|
243
254
|
constructor(...args: ConstructorParameters<typeof SlideNavBase>) {
|
|
244
255
|
super(...args);
|
|
245
256
|
}
|