@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/storymap/StoryMap.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
mergeData,
|
|
3
|
+
slideTransitionDuration,
|
|
4
|
+
updateData,
|
|
5
|
+
prefersReducedMotion,
|
|
6
|
+
REDUCED_MOTION_QUERY,
|
|
7
|
+
} from "../core/Util";
|
|
2
8
|
import { loadCSS } from "../core/Load";
|
|
3
9
|
import { validateStorymapAndReport } from "./validate";
|
|
4
10
|
import {
|
|
@@ -13,6 +19,14 @@ import {
|
|
|
13
19
|
loadSeeAlso,
|
|
14
20
|
manifestToStorymapData,
|
|
15
21
|
} from "./iiif";
|
|
22
|
+
import {
|
|
23
|
+
followSlideshowPages,
|
|
24
|
+
formatSlideshowWarning,
|
|
25
|
+
isSlideshowCollection,
|
|
26
|
+
slideshowToStorymapData,
|
|
27
|
+
type SlideshowWarning,
|
|
28
|
+
} from "./from-slideshow";
|
|
29
|
+
import { asClampedNumber } from "./iiif-shared";
|
|
16
30
|
import {
|
|
17
31
|
ConsentManager,
|
|
18
32
|
consentManagerOf,
|
|
@@ -31,6 +45,7 @@ import {
|
|
|
31
45
|
claimLanguage,
|
|
32
46
|
releaseLanguage,
|
|
33
47
|
isLanguageConflict,
|
|
48
|
+
Language,
|
|
34
49
|
} from "../language/Language";
|
|
35
50
|
import {
|
|
36
51
|
markInteraction,
|
|
@@ -39,78 +54,57 @@ import {
|
|
|
39
54
|
unregisterParticipant,
|
|
40
55
|
} from "../core/viewers";
|
|
41
56
|
import MediaType from "../media/MediaType";
|
|
42
|
-
import { Evented, type EventedInstance } from "../core/mixins";
|
|
57
|
+
import { Evented, type EventedInstance, type FiredEvent } from "../core/mixins";
|
|
43
58
|
import OpenLayersMap from "../map/openlayers/Map.OpenLayers";
|
|
59
|
+
import type { ImagereadyPayload } from "../map/openlayers/Map.OpenLayers";
|
|
44
60
|
import MenuBar from "../ui/MenuBar";
|
|
61
|
+
import type { LayersRow } from "../ui/LayersControl";
|
|
45
62
|
import StorySlider from "../slider/StorySlider";
|
|
46
63
|
import { Browser } from "../core/Browser";
|
|
47
64
|
import Animate from "../animation/tween";
|
|
48
65
|
import type { Map as OlMap } from "ol";
|
|
49
66
|
import type OlLayer from "ol/layer/Layer";
|
|
50
67
|
import type OpenLayersMapMarker from "../map/openlayers/MapMarker.OpenLayers";
|
|
68
|
+
export { resolveFontCssUrl, isExternalUrl, fontCssOriginal } from "./font-css";
|
|
69
|
+
import { resolveFontCssUrl, isExternalUrl, fontCssOriginal } from "./font-css";
|
|
51
70
|
import type {
|
|
52
71
|
AnimationHandle,
|
|
72
|
+
StorymapBasemap,
|
|
53
73
|
StorymapData,
|
|
54
74
|
StorymapDataWrapper,
|
|
55
75
|
StorymapOptions,
|
|
56
76
|
StorymapSlide,
|
|
77
|
+
StorymapSlideNarration,
|
|
57
78
|
} from "../types";
|
|
58
79
|
|
|
59
80
|
/** Map height in pixels while the menubar has collapsed the map (portrait only). */
|
|
60
81
|
const COLLAPSED_MAP_HEIGHT = 1;
|
|
61
82
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
);
|
|
83
|
-
return new URL(/* @vite-ignore */ "../css/fonts/font.default.css", import.meta.url)
|
|
84
|
-
.href;
|
|
85
|
-
}
|
|
86
|
-
// resolved against the library location: one directory up from
|
|
87
|
-
// src/main.ts (dev) and js/storymap.js (build) in both cases
|
|
88
|
-
return new URL("../css/fonts/font." + font_name + ".css", import.meta.url).href;
|
|
89
|
-
}
|
|
90
|
-
// Absolute URL, protocol-relative, or a data: URL. Note this must be a
|
|
91
|
-
// real URL test, not a `/^(http|https|\/\/)/` prefix check: the old prefix
|
|
92
|
-
// test false-positived on a relative path that merely *starts* with those
|
|
93
|
-
// letters (e.g. "httpfonts.css"), which raised a spurious consent prompt
|
|
94
|
-
// for a same-origin file.
|
|
95
|
-
if (/^[a-z][a-z0-9+.-]*:/i.test(font) || font.startsWith("//")) {
|
|
96
|
-
return font;
|
|
97
|
-
}
|
|
98
|
-
return new URL(font, document.baseURI).href;
|
|
83
|
+
export interface StoryMapEvents {
|
|
84
|
+
change: { current_slide: number; current_id: string | null };
|
|
85
|
+
loaded: StorymapData;
|
|
86
|
+
title: { title: string };
|
|
87
|
+
dataloaded: undefined;
|
|
88
|
+
fontLoaded: { font: string };
|
|
89
|
+
transitionstart: { current_slide: number; duration: number };
|
|
90
|
+
transitionend: { current_slide: number };
|
|
91
|
+
error: { message: string; source: string; conflict?: boolean };
|
|
92
|
+
imageready: ImagereadyPayload;
|
|
93
|
+
annotationsloaded: {
|
|
94
|
+
stops: StorymapSlide[];
|
|
95
|
+
searchService: string | null;
|
|
96
|
+
failed: string[];
|
|
97
|
+
};
|
|
98
|
+
markerclick: { marker_number: number; current_slide: number };
|
|
99
|
+
popupopen: { marker_number: number; current_slide: number };
|
|
100
|
+
popupclose: { marker_number: number; current_slide: number };
|
|
101
|
+
basemapchange: { map_type: string; previous: string };
|
|
102
|
+
overlaychange: { index: number; visible: boolean };
|
|
99
103
|
}
|
|
100
104
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (/^(data|blob):/i.test(url)) {
|
|
105
|
-
return true;
|
|
106
|
-
}
|
|
107
|
-
try {
|
|
108
|
-
return new URL(url, document.baseURI).origin !== window.location.origin;
|
|
109
|
-
} catch {
|
|
110
|
-
// unparseable: treat as external, so we err towards asking
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
105
|
+
type StoryMapListener<K extends keyof StoryMapEvents = keyof StoryMapEvents> = (
|
|
106
|
+
e: FiredEvent<StoryMapEvents, K>,
|
|
107
|
+
) => void;
|
|
114
108
|
|
|
115
109
|
/**
|
|
116
110
|
* Interactive StoryMap viewer.
|
|
@@ -129,7 +123,7 @@ export function isExternalUrl(url: string): boolean {
|
|
|
129
123
|
*/
|
|
130
124
|
class StoryMapBase {
|
|
131
125
|
declare "_loaded": { storyslider: boolean; map: boolean };
|
|
132
|
-
declare "on": EventedInstance["on"];
|
|
126
|
+
declare "on": EventedInstance<StoryMapEvents>["on"];
|
|
133
127
|
declare "version": string;
|
|
134
128
|
declare "ready": boolean;
|
|
135
129
|
declare "_el": {
|
|
@@ -177,6 +171,21 @@ class StoryMapBase {
|
|
|
177
171
|
declare "_autoplay_stopped": boolean;
|
|
178
172
|
/** The narration player: one <audio> for the whole story, reused. */
|
|
179
173
|
declare "_narration_el": HTMLAudioElement | null;
|
|
174
|
+
/**
|
|
175
|
+
* The ambient player: a second <audio> for a narration bed that outlives
|
|
176
|
+
* its slide (`narration.stopOnExit: false`, slideshow ambient audio).
|
|
177
|
+
* At most one ambient track plays; a new one replaces it.
|
|
178
|
+
*/
|
|
179
|
+
declare "_ambient_el": HTMLAudioElement | null;
|
|
180
|
+
/** What each player is (or was) playing, so same-URL beds continue. */
|
|
181
|
+
declare "_narration_meta": { url: string; stopOnExit: boolean } | null;
|
|
182
|
+
declare "_ambient_meta": { url: string } | null;
|
|
183
|
+
/**
|
|
184
|
+
* Whether the ambient bed is actually playing: `HTMLMediaElement.paused`
|
|
185
|
+
* cannot be trusted here (test doubles and pre-gesture elements report
|
|
186
|
+
* stale state), so the viewer tracks it explicitly.
|
|
187
|
+
*/
|
|
188
|
+
declare "_ambient_playing": boolean;
|
|
180
189
|
/** Guards a stale play() from a slide change that already moved on. */
|
|
181
190
|
declare "_narration_token": number;
|
|
182
191
|
/** Whether narration consent was granted (asked once per story). */
|
|
@@ -191,12 +200,16 @@ class StoryMapBase {
|
|
|
191
200
|
declare "_collapsed": boolean;
|
|
192
201
|
/** the data source was a IIIF Presentation manifest (legacy zoomify options are ignored) */
|
|
193
202
|
declare "_data_from_manifest": boolean;
|
|
203
|
+
/** the data source was a slideshow tour (slideshow-only features apply) */
|
|
204
|
+
declare "_data_from_slideshow": boolean;
|
|
194
205
|
declare "_resize_observer": ResizeObserver | null;
|
|
195
206
|
/** Stored so `dispose()` can remove them again (see AGENTS: no leaks) */
|
|
196
207
|
declare "_on_resize": (() => void) | null;
|
|
197
208
|
declare "_on_keydown_global": ((e: KeyboardEvent) => void) | null;
|
|
198
209
|
declare "_on_fullscreen": (() => void) | null;
|
|
199
210
|
declare "_on_hashchange": (() => void) | null;
|
|
211
|
+
/** Reduced-motion toggles mid-story stop the advance timer. */
|
|
212
|
+
declare "_on_reduced_motion": ((e: MediaQueryListEvent) => void) | null;
|
|
200
213
|
/** Set by `dispose()` so a second call returns early. */
|
|
201
214
|
declare "_disposed": boolean;
|
|
202
215
|
/** identity of this viewer's claim on the page-wide locale */
|
|
@@ -207,11 +220,7 @@ class StoryMapBase {
|
|
|
207
220
|
declare "_interaction": number;
|
|
208
221
|
declare "_onInteraction": (() => void) | null;
|
|
209
222
|
declare "_resize_timer": ReturnType<typeof setTimeout> | null;
|
|
210
|
-
declare "fire": EventedInstance["fire"];
|
|
211
|
-
declare "hasEventListeners": EventedInstance["hasEventListeners"];
|
|
212
|
-
|
|
213
|
-
// TODO: mixin
|
|
214
|
-
// includes: VCO.Events,
|
|
223
|
+
declare "fire": EventedInstance<StoryMapEvents>["fire"];
|
|
215
224
|
|
|
216
225
|
/* Private Methods
|
|
217
226
|
================================================== */
|
|
@@ -231,26 +240,25 @@ class StoryMapBase {
|
|
|
231
240
|
elem: string | HTMLElement,
|
|
232
241
|
data: string | StorymapDataWrapper | Record<string, unknown>,
|
|
233
242
|
options?: Partial<StorymapOptions>,
|
|
234
|
-
listeners?:
|
|
243
|
+
listeners?: { [K in keyof StoryMapEvents]?: StoryMapListener<K> | StoryMapListener<K>[] },
|
|
235
244
|
) {
|
|
236
245
|
for (const key in listeners) {
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
246
|
+
const type = key as keyof StoryMapEvents;
|
|
247
|
+
const callbacks = listeners[type];
|
|
248
|
+
if (callbacks === undefined) continue;
|
|
249
|
+
const list = typeof callbacks == "function" ? [callbacks] : callbacks;
|
|
250
|
+
for (const callback of list) {
|
|
251
|
+
if (typeof callback == "function") {
|
|
252
|
+
this.on(type, callback as StoryMapListener);
|
|
253
|
+
} else {
|
|
254
|
+
console.warn(
|
|
255
|
+
"WARNING: Ignoring invalid callback '" +
|
|
256
|
+
callback +
|
|
257
|
+
"' defined for " +
|
|
258
|
+
"listener '" +
|
|
259
|
+
key +
|
|
260
|
+
"' in StoryMap constructor",
|
|
261
|
+
);
|
|
254
262
|
}
|
|
255
263
|
}
|
|
256
264
|
}
|
|
@@ -333,11 +341,25 @@ class StoryMapBase {
|
|
|
333
341
|
trackResize: true,
|
|
334
342
|
keyboard: false,
|
|
335
343
|
nocache: false,
|
|
344
|
+
slideshow: true,
|
|
336
345
|
autoplay: 0,
|
|
337
346
|
autoplay_media: false,
|
|
338
347
|
show_progress: false,
|
|
348
|
+
progressbar: undefined,
|
|
339
349
|
marker_labels: false,
|
|
340
350
|
text_align: "left",
|
|
351
|
+
textmode: "right",
|
|
352
|
+
textsize: undefined,
|
|
353
|
+
fxmode: "slide",
|
|
354
|
+
mode: "standard",
|
|
355
|
+
hudcolor: "",
|
|
356
|
+
hudbgcolor: "",
|
|
357
|
+
hudopacity: undefined,
|
|
358
|
+
shownav: true,
|
|
359
|
+
show_headings: true,
|
|
360
|
+
show_scrollbars: true,
|
|
361
|
+
viewerheight: "",
|
|
362
|
+
show_info: true,
|
|
341
363
|
map_overview_center: null,
|
|
342
364
|
map_type: "", // "osm:standard",
|
|
343
365
|
tile_source_factory: null,
|
|
@@ -355,6 +377,8 @@ class StoryMapBase {
|
|
|
355
377
|
// that already exist here, so storymap JSON could not reach them
|
|
356
378
|
overview_extent: null,
|
|
357
379
|
overlays: [],
|
|
380
|
+
show_layers_control: false,
|
|
381
|
+
basemaps: undefined,
|
|
358
382
|
// TileJSON 2.1 metadata for a tile-source basemap; a keyword
|
|
359
383
|
// basemap has none (interop §2.9)
|
|
360
384
|
tilejson: undefined,
|
|
@@ -396,6 +420,7 @@ class StoryMapBase {
|
|
|
396
420
|
this._on_keydown_global = null;
|
|
397
421
|
this._on_fullscreen = null;
|
|
398
422
|
this._on_hashchange = null;
|
|
423
|
+
this._on_reduced_motion = null;
|
|
399
424
|
this._raw_manifest = null;
|
|
400
425
|
this._initial_deep_link = null;
|
|
401
426
|
this._disposed = false;
|
|
@@ -408,6 +433,10 @@ class StoryMapBase {
|
|
|
408
433
|
this._transition_timer = null;
|
|
409
434
|
this._autoplay_stopped = false;
|
|
410
435
|
this._narration_el = null;
|
|
436
|
+
this._ambient_el = null;
|
|
437
|
+
this._narration_meta = null;
|
|
438
|
+
this._ambient_meta = null;
|
|
439
|
+
this._ambient_playing = false;
|
|
411
440
|
this._narration_token = 0;
|
|
412
441
|
this._narration_allowed = true;
|
|
413
442
|
this._user_gestured = false;
|
|
@@ -445,6 +474,10 @@ class StoryMapBase {
|
|
|
445
474
|
// legacy zoomify options are only honored for storymap JSON sources —
|
|
446
475
|
// a IIIF Presentation manifest cannot carry them
|
|
447
476
|
this._data_from_manifest = false;
|
|
477
|
+
// slideshow tours unlock the slideshow-only presentation fields; the
|
|
478
|
+
// translator branch that sets this lives with the translator phase —
|
|
479
|
+
// until then only an explicit host flag (see slideshow_source) sets it
|
|
480
|
+
this._data_from_slideshow = false;
|
|
448
481
|
if (typeof data === "string") {
|
|
449
482
|
// issue #417: optional cache-busting re-fetch of the source file
|
|
450
483
|
const url =
|
|
@@ -459,6 +492,12 @@ class StoryMapBase {
|
|
|
459
492
|
this._data_from_manifest = true;
|
|
460
493
|
this._raw_manifest = data;
|
|
461
494
|
this.data = manifestToStorymapData(data);
|
|
495
|
+
} else if (this.options.slideshow !== false && isSlideshowCollection(data)) {
|
|
496
|
+
this._data_from_slideshow = true;
|
|
497
|
+
const { data: storymap, warnings } = slideshowToStorymapData(data);
|
|
498
|
+
this._warnSlideshow(warnings);
|
|
499
|
+
validateStorymapAndReport({ storymap }, "slideshow");
|
|
500
|
+
this.data = storymap;
|
|
462
501
|
} else {
|
|
463
502
|
const wrapper = data as StorymapDataWrapper;
|
|
464
503
|
validateStorymapAndReport(wrapper);
|
|
@@ -477,6 +516,56 @@ class StoryMapBase {
|
|
|
477
516
|
|
|
478
517
|
/* Load storymap data from a URL
|
|
479
518
|
================================================== */
|
|
519
|
+
/**
|
|
520
|
+
* One `console.warn` per warning kind: count, detail and sample ids.
|
|
521
|
+
* Translation losses are loud exactly once, never per slide. Shared
|
|
522
|
+
* formatting with the CLI (see `formatSlideshowWarning`).
|
|
523
|
+
*/
|
|
524
|
+
_warnSlideshow(warnings: SlideshowWarning[]): void {
|
|
525
|
+
for (const warning of warnings) {
|
|
526
|
+
console.warn(`StoryMapJS: ${formatSlideshowWarning(warning)}`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Follow a tour's `first.next` page chain, returning the following pages
|
|
532
|
+
* for the translator. Bounded (20) and cycle-guarded; a broken chain
|
|
533
|
+
* reports incompletely so the caller warns instead of truncating
|
|
534
|
+
* silently. Relative links resolve against the tour URL.
|
|
535
|
+
*/
|
|
536
|
+
async _followSlideshowPages(
|
|
537
|
+
doc: unknown,
|
|
538
|
+
url: string,
|
|
539
|
+
): Promise<{ pages: unknown[]; complete: boolean }> {
|
|
540
|
+
// relative tour URLs (harness, subpath deploys) resolve against the
|
|
541
|
+
// page; an unresolvable base still lets absolute links through
|
|
542
|
+
let base = url;
|
|
543
|
+
try {
|
|
544
|
+
base = new URL(url, window.location.href).href;
|
|
545
|
+
} catch {
|
|
546
|
+
// keep the raw url
|
|
547
|
+
}
|
|
548
|
+
const first = (doc as Record<string, unknown> | null)?.first;
|
|
549
|
+
return followSlideshowPages({
|
|
550
|
+
first,
|
|
551
|
+
base,
|
|
552
|
+
resolve: (link, current) => {
|
|
553
|
+
try {
|
|
554
|
+
return new URL(link, current).href;
|
|
555
|
+
} catch {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
load: async (absolute: string) => {
|
|
560
|
+
const response = await fetch(absolute);
|
|
561
|
+
if (!response.ok) {
|
|
562
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
563
|
+
}
|
|
564
|
+
return response.json() as Promise<unknown>;
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
480
569
|
async _loadDataFromUrl(url: string, source: string) {
|
|
481
570
|
try {
|
|
482
571
|
const response = await fetch(url);
|
|
@@ -495,6 +584,28 @@ class StoryMapBase {
|
|
|
495
584
|
this._data_from_manifest = true;
|
|
496
585
|
this._raw_manifest = result;
|
|
497
586
|
this.data = manifestToStorymapData(result);
|
|
587
|
+
} else if (this.options.slideshow !== false && isSlideshowCollection(result)) {
|
|
588
|
+
this._data_from_slideshow = true;
|
|
589
|
+
const followed = await this._followSlideshowPages(result, url);
|
|
590
|
+
if (this._disposed) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const { data: storymap, warnings } = slideshowToStorymapData(result, {
|
|
594
|
+
extraPages: followed.pages,
|
|
595
|
+
});
|
|
596
|
+
this._warnSlideshow(warnings);
|
|
597
|
+
if (!followed.complete) {
|
|
598
|
+
this._warnSlideshow([
|
|
599
|
+
{
|
|
600
|
+
kind: "slideshow.paging",
|
|
601
|
+
count: 1,
|
|
602
|
+
sampleIds: [],
|
|
603
|
+
detail: "page chain broke, tour may be truncated",
|
|
604
|
+
},
|
|
605
|
+
]);
|
|
606
|
+
}
|
|
607
|
+
validateStorymapAndReport({ storymap }, source);
|
|
608
|
+
this.data = storymap;
|
|
498
609
|
} else {
|
|
499
610
|
validateStorymapAndReport(result, source);
|
|
500
611
|
const wrapper = result as StorymapDataWrapper;
|
|
@@ -520,10 +631,62 @@ class StoryMapBase {
|
|
|
520
631
|
|
|
521
632
|
/* Initialize the options
|
|
522
633
|
================================================== */
|
|
634
|
+
/**
|
|
635
|
+
* Reset every slideshow-only option to its long-standing default unless
|
|
636
|
+
* the loaded tour came from a slideshow source. Internal documents keep
|
|
637
|
+
* the pre-existing rendering even when they (or a host, via
|
|
638
|
+
* setMapOptions) name the new keys; slideshow tours keep what the
|
|
639
|
+
* translator emitted. Runs on every load and every runtime option change,
|
|
640
|
+
* over the viewer options and (at runtime) the map's own options copy.
|
|
641
|
+
*/
|
|
642
|
+
_normalizeNonSlideshowOptions(options: StorymapOptions = this.options): void {
|
|
643
|
+
if (options.slideshow_source === true) return;
|
|
644
|
+
options.textmode = "right";
|
|
645
|
+
options.textsize = undefined;
|
|
646
|
+
options.fxmode = "slide";
|
|
647
|
+
options.mode = "standard";
|
|
648
|
+
options.hudcolor = "";
|
|
649
|
+
options.hudbgcolor = "";
|
|
650
|
+
options.hudopacity = undefined;
|
|
651
|
+
options.shownav = true;
|
|
652
|
+
options.show_headings = true;
|
|
653
|
+
options.show_scrollbars = true;
|
|
654
|
+
options.viewerheight = "";
|
|
655
|
+
options.show_info = true;
|
|
656
|
+
options.progressbar = undefined;
|
|
657
|
+
}
|
|
658
|
+
|
|
523
659
|
_initOptions() {
|
|
524
660
|
// Grab options from storymap data
|
|
525
661
|
updateData(this.options, this.data);
|
|
526
662
|
|
|
663
|
+
// Source marker for the slideshow-only presentation fields: set
|
|
664
|
+
// programmatically from the load path, never from data (the key is
|
|
665
|
+
// absent from defaults and schema, so updateData above cannot inject
|
|
666
|
+
// it — a host may still pass it explicitly as an escape hatch).
|
|
667
|
+
if (this._data_from_slideshow) {
|
|
668
|
+
this.options.slideshow_source = true;
|
|
669
|
+
}
|
|
670
|
+
// Slideshow-only chrome stays inert for internal documents: reset
|
|
671
|
+
// every such option to its default unless the tour came from a
|
|
672
|
+
// slideshow source. this.data itself is never touched (it is the
|
|
673
|
+
// host's object — see data-immutable.test.ts).
|
|
674
|
+
this._normalizeNonSlideshowOptions();
|
|
675
|
+
// Story credit (slideshow `creator`/`rights`) joins the map
|
|
676
|
+
// attribution line for slideshow tours only; the line re-sanitizes
|
|
677
|
+
// its input, so author strings render as text. Internal documents
|
|
678
|
+
// keep their own attribution untouched.
|
|
679
|
+
if (this._data_from_slideshow && this.data.credit) {
|
|
680
|
+
const credit = [this.data.credit.creator, this.data.credit.rights].filter(
|
|
681
|
+
(part): part is string => typeof part === "string" && part !== "",
|
|
682
|
+
);
|
|
683
|
+
if (credit.length > 0) {
|
|
684
|
+
this.options.attribution = [this.options.attribution, ...credit]
|
|
685
|
+
.filter((part) => part !== "")
|
|
686
|
+
.join(" · ");
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
527
690
|
// Capture the deep link now, before the slider and map exist and can
|
|
528
691
|
// navigate — and so rewrite the URL we would read it from later.
|
|
529
692
|
this._initial_deep_link = {
|
|
@@ -550,9 +713,19 @@ class StoryMapBase {
|
|
|
550
713
|
// map_area "left": the map is limited to the visible half with an
|
|
551
714
|
// opaque slide panel — no offset needed (the view center is the
|
|
552
715
|
// visible center already); "full" (default) offsets the view so
|
|
553
|
-
// markers clear the panel that fades in over the map
|
|
716
|
+
// markers clear the panel that fades in over the map. A left
|
|
717
|
+
// text panel (textmode "left") mirrors the offset.
|
|
554
718
|
if (this.options.map_area !== "left") {
|
|
555
|
-
|
|
719
|
+
// side docks offset the view past the panel (mirrored for a
|
|
720
|
+
// left panel); a bottom dock recenters slightly upward so
|
|
721
|
+
// point stops clear the panel (fits use measured padding)
|
|
722
|
+
if (this.options.textmode === "left") {
|
|
723
|
+
this.options.map_center_offset = { left: 200, top: 0 };
|
|
724
|
+
} else if (this.options.textmode === "bottom") {
|
|
725
|
+
this.options.map_center_offset = { left: 0, top: -120 };
|
|
726
|
+
} else {
|
|
727
|
+
this.options.map_center_offset = { left: -200, top: 0 };
|
|
728
|
+
}
|
|
556
729
|
} else {
|
|
557
730
|
this.options.map_center_offset = { left: 0, top: 0 };
|
|
558
731
|
}
|
|
@@ -591,6 +764,21 @@ class StoryMapBase {
|
|
|
591
764
|
this.options.map_type = "";
|
|
592
765
|
}
|
|
593
766
|
|
|
767
|
+
// Entry 0 of `basemaps` is the initial basemap unless `map_type` says
|
|
768
|
+
// otherwise ("" counts as "not saying": it is the default, not a
|
|
769
|
+
// choice). Omit `basemaps` for an overlays-only control.
|
|
770
|
+
const basemaps = this.options.basemaps;
|
|
771
|
+
if (
|
|
772
|
+
this.options.map_type === "" &&
|
|
773
|
+
Array.isArray(basemaps) &&
|
|
774
|
+
basemaps.length > 0 &&
|
|
775
|
+
typeof basemaps[0]?.map_type === "string" &&
|
|
776
|
+
basemaps[0].map_type !== "" &&
|
|
777
|
+
basemaps[0].map_type !== "none"
|
|
778
|
+
) {
|
|
779
|
+
this.options.map_type = basemaps[0].map_type;
|
|
780
|
+
}
|
|
781
|
+
|
|
594
782
|
// "none" is the only way to say there is no map. An absent key or ""
|
|
595
783
|
// keeps meaning OSM, so no existing document changes meaning.
|
|
596
784
|
this._map_disabled = this.options.map_type === "none";
|
|
@@ -603,7 +791,7 @@ class StoryMapBase {
|
|
|
603
791
|
} else {
|
|
604
792
|
this.options.map_type = "osm:standard";
|
|
605
793
|
}
|
|
606
|
-
console.
|
|
794
|
+
console.warn(`Deprecated map_type ${old_type}; using ${this.options.map_type}`);
|
|
607
795
|
}
|
|
608
796
|
|
|
609
797
|
this._loadLanguage();
|
|
@@ -684,8 +872,14 @@ class StoryMapBase {
|
|
|
684
872
|
async _loadFontCss() {
|
|
685
873
|
// only genuinely external URLs ask for consent: stock: themes and
|
|
686
874
|
// relative paths resolve to same-origin / library assets
|
|
687
|
-
const original = this.options.font_css
|
|
875
|
+
const original = fontCssOriginal(this.options.font_css);
|
|
876
|
+
if (original === null) {
|
|
877
|
+
// the theme ships in the host bundle (font_css: false): nothing
|
|
878
|
+
// to inject and no consent row for it
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
688
881
|
const font = resolveFontCssUrl(original);
|
|
882
|
+
if (font === null) return; // only "none" maps to null, handled above
|
|
689
883
|
const manager = consentManagerOf(this.options);
|
|
690
884
|
const external = /^(http|https|\/\/)/.test(original);
|
|
691
885
|
if (external && this.options.consent_required && manager) {
|
|
@@ -760,6 +954,10 @@ class StoryMapBase {
|
|
|
760
954
|
if (navigate !== "map") {
|
|
761
955
|
this._map?.goTo(this.current_slide);
|
|
762
956
|
}
|
|
957
|
+
// Per-slide image overlay (slideshow imgoverlay): shown while its
|
|
958
|
+
// slide is active, cleared otherwise. Null-safe: mapless stories
|
|
959
|
+
// have no map, slides without one clear a previous overlay.
|
|
960
|
+
this._map?.setSlideImageOverlay(this.data.slides?.[this.current_slide]?.imgoverlay ?? null);
|
|
763
961
|
this._beginTransition(duration);
|
|
764
962
|
if (!navigated) {
|
|
765
963
|
// programmatic navigation reports outward like interaction does;
|
|
@@ -768,17 +966,14 @@ class StoryMapBase {
|
|
|
768
966
|
// `current_id` so a host can tell *which stop* it is on, not just
|
|
769
967
|
// how far along it is: an index moves when a slide is inserted, an
|
|
770
968
|
// id does not (§5.1)
|
|
771
|
-
this.fire(
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
current_id: this._currentSlideId(),
|
|
776
|
-
},
|
|
777
|
-
this,
|
|
778
|
-
);
|
|
969
|
+
this.fire("change", {
|
|
970
|
+
current_slide: this.current_slide,
|
|
971
|
+
current_id: this._currentSlideId(),
|
|
972
|
+
});
|
|
779
973
|
}
|
|
780
974
|
this._syncHash();
|
|
781
975
|
this._playNarration(this.data.slides?.[this.current_slide]);
|
|
976
|
+
this._stopAmbientSlideMedia(this.data.slides?.[this.current_slide]);
|
|
782
977
|
this._scheduleAutoplay();
|
|
783
978
|
this._updateProgress();
|
|
784
979
|
}
|
|
@@ -793,10 +988,10 @@ class StoryMapBase {
|
|
|
793
988
|
if (this._transition_timer) {
|
|
794
989
|
clearTimeout(this._transition_timer);
|
|
795
990
|
}
|
|
796
|
-
this.fire("transitionstart", { current_slide: this.current_slide, duration }
|
|
991
|
+
this.fire("transitionstart", { current_slide: this.current_slide, duration });
|
|
797
992
|
this._transition_timer = setTimeout(() => {
|
|
798
993
|
this._transition_timer = null;
|
|
799
|
-
this.fire("transitionend", { current_slide: this.current_slide }
|
|
994
|
+
this.fire("transitionend", { current_slide: this.current_slide });
|
|
800
995
|
}, duration);
|
|
801
996
|
}
|
|
802
997
|
|
|
@@ -883,13 +1078,21 @@ class StoryMapBase {
|
|
|
883
1078
|
}
|
|
884
1079
|
}
|
|
885
1080
|
mergeData(this.options, effective);
|
|
1081
|
+
// slideshow-only keys stay inert for internal documents at runtime too
|
|
1082
|
+
this._normalizeNonSlideshowOptions();
|
|
886
1083
|
if (this._map && this._map.options) {
|
|
887
1084
|
mergeData(this._map.options, effective);
|
|
1085
|
+
this._normalizeNonSlideshowOptions(this._map.options);
|
|
888
1086
|
this._map.applyOptions(Object.keys(effective));
|
|
889
1087
|
}
|
|
890
1088
|
if (this.ready) {
|
|
891
1089
|
// text color theming follows runtime option changes (issue #177)
|
|
892
1090
|
this._applyTextColors();
|
|
1091
|
+
// slideshow chrome that maps onto container state does too;
|
|
1092
|
+
// layout modes (textmode, mode) need a display pass to take effect
|
|
1093
|
+
this._applyChrome();
|
|
1094
|
+
// layers may have been added, removed or re-projected
|
|
1095
|
+
this._menubar.refreshLayers();
|
|
893
1096
|
this.updateDisplay();
|
|
894
1097
|
}
|
|
895
1098
|
}
|
|
@@ -1010,7 +1213,7 @@ class StoryMapBase {
|
|
|
1010
1213
|
this._updateDistance();
|
|
1011
1214
|
}
|
|
1012
1215
|
const result = { stops: added, searchService: loaded.searchService, failed: loaded.failed };
|
|
1013
|
-
this.fire("annotationsloaded", result
|
|
1216
|
+
this.fire("annotationsloaded", result);
|
|
1014
1217
|
return result;
|
|
1015
1218
|
}
|
|
1016
1219
|
|
|
@@ -1065,6 +1268,12 @@ class StoryMapBase {
|
|
|
1065
1268
|
window.removeEventListener("hashchange", this._on_hashchange);
|
|
1066
1269
|
this._on_hashchange = null;
|
|
1067
1270
|
}
|
|
1271
|
+
if (this._on_reduced_motion && typeof window.matchMedia === "function") {
|
|
1272
|
+
window
|
|
1273
|
+
.matchMedia(REDUCED_MOTION_QUERY)
|
|
1274
|
+
.removeEventListener("change", this._on_reduced_motion);
|
|
1275
|
+
this._on_reduced_motion = null;
|
|
1276
|
+
}
|
|
1068
1277
|
|
|
1069
1278
|
// slide transitions in flight (Web Animations API)
|
|
1070
1279
|
for (const el of [this._el?.container, this._el?.map]) {
|
|
@@ -1076,10 +1285,39 @@ class StoryMapBase {
|
|
|
1076
1285
|
consentManagerOf(this.options)?.dispose();
|
|
1077
1286
|
|
|
1078
1287
|
this._stopNarration();
|
|
1288
|
+
this._stopAmbient();
|
|
1079
1289
|
if (this._narration_el) {
|
|
1080
1290
|
this._narration_el.src = "";
|
|
1081
1291
|
this._narration_el = null;
|
|
1082
1292
|
}
|
|
1293
|
+
if (this._ambient_el) {
|
|
1294
|
+
this._ambient_el.src = "";
|
|
1295
|
+
this._ambient_el = null;
|
|
1296
|
+
}
|
|
1297
|
+
this._narration_meta = null;
|
|
1298
|
+
this._ambient_meta = null;
|
|
1299
|
+
// detach the child subscriptions so a host holding a documented
|
|
1300
|
+
// child handle (storymap._map) cannot keep this viewer alive
|
|
1301
|
+
// through the listener contexts
|
|
1302
|
+
this._map?.off("loaded", this._onMapLoaded, this);
|
|
1303
|
+
this._map?.off("change", this._onMapChange, this);
|
|
1304
|
+
this._map?.off("imageready", this._onImageReady, this);
|
|
1305
|
+
this._map?.off("popupopen", this._onMarkerPopupOpen, this);
|
|
1306
|
+
this._map?.off("popupclose", this._onMarkerPopupClose, this);
|
|
1307
|
+
this._map?.off("markerclick", this._onMarkerClick, this);
|
|
1308
|
+
this._map?.off("tilesallowed", this._onTilesAllowed, this);
|
|
1309
|
+
this._storyslider?.off("loaded", this._onStorySliderLoaded, this);
|
|
1310
|
+
this._storyslider?.off("title", this._onTitle, this);
|
|
1311
|
+
this._storyslider?.off("change", this._onSlideChange, this);
|
|
1312
|
+
this._storyslider?.off("colorchange", this._onColorChange, this);
|
|
1313
|
+
this._menubar?.off("collapse", this._onMenuBarCollapse, this);
|
|
1314
|
+
this._menubar?.off("back_to_start", this._onBackToStart, this);
|
|
1315
|
+
this._menubar?.off("overview", this._onOverview, this);
|
|
1316
|
+
this._menubar?.off("fullscreen", this._onFullscreenToggle, this);
|
|
1317
|
+
this._menubar?.off("basemapchange", this._onLayersBasemap, this);
|
|
1318
|
+
this._menubar?.off("overlaychange", this._onLayersOverlay, this);
|
|
1319
|
+
this._menubar?.off("autoplay_toggle", this._onAutoplayToggle, this);
|
|
1320
|
+
this._menubar?.off("progress_go", this._onProgressGo, this);
|
|
1083
1321
|
this._storyslider?.dispose?.();
|
|
1084
1322
|
this._menubar?.dispose?.();
|
|
1085
1323
|
this._map?.dispose?.();
|
|
@@ -1143,6 +1381,53 @@ class StoryMapBase {
|
|
|
1143
1381
|
return !this._disposed && this._map ? this._map.getMarker(index) : null;
|
|
1144
1382
|
}
|
|
1145
1383
|
|
|
1384
|
+
/**
|
|
1385
|
+
* Open the popup card for slide `n`'s marker, navigating there first
|
|
1386
|
+
* when it is not the current slide.
|
|
1387
|
+
*
|
|
1388
|
+
* Returns false (and changes nothing) when the viewer is disposed,
|
|
1389
|
+
* has no map (`map_type: "none"`), `n` is out of range, or the marker
|
|
1390
|
+
* has popups disabled. Navigating fires the usual `change` (and
|
|
1391
|
+
* transition) events; a successful open fires `popupopen`.
|
|
1392
|
+
*/
|
|
1393
|
+
openMarkerPopup(n: number): boolean {
|
|
1394
|
+
if (this._disposed) return false;
|
|
1395
|
+
if (!Number.isInteger(n) || n < 0 || n >= (this.data?.slides?.length ?? 0)) {
|
|
1396
|
+
return false;
|
|
1397
|
+
}
|
|
1398
|
+
if (this._map === null || this._map === undefined) return false;
|
|
1399
|
+
const marker = this._map.getMarker(n);
|
|
1400
|
+
if (!marker || !marker.isPopupEnabled()) return false;
|
|
1401
|
+
if (n !== this.current_slide) {
|
|
1402
|
+
this.goTo(n);
|
|
1403
|
+
}
|
|
1404
|
+
return marker.openPopup();
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
/**
|
|
1408
|
+
* Close one marker's popup card (`n` given) or every open card
|
|
1409
|
+
* (omitted). No-op when the viewer is disposed or mapless. Fires
|
|
1410
|
+
* `popupclose` per card actually closed.
|
|
1411
|
+
*/
|
|
1412
|
+
closeMarkerPopup(n?: number): void {
|
|
1413
|
+
if (this._disposed) return;
|
|
1414
|
+
if (this._map === null || this._map === undefined) return;
|
|
1415
|
+
if (n === undefined) {
|
|
1416
|
+
for (const marker of this._map.getMarkers()) {
|
|
1417
|
+
marker.closePopup();
|
|
1418
|
+
}
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
this._map.getMarker(n)?.closePopup();
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
/** Whether slide `n`'s marker card is currently open. */
|
|
1425
|
+
isPopupOpen(n: number): boolean {
|
|
1426
|
+
if (this._disposed) return false;
|
|
1427
|
+
if (this._map === null || this._map === undefined) return false;
|
|
1428
|
+
return this._map.getMarker(n)?.popupOpen ?? false;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1146
1431
|
/**
|
|
1147
1432
|
* Rebuild the minimap (`OverviewMap` control). The constructor already
|
|
1148
1433
|
* builds it; this is for hosts that recreate it after a `map_type` swap
|
|
@@ -1179,6 +1464,78 @@ class StoryMapBase {
|
|
|
1179
1464
|
}
|
|
1180
1465
|
}
|
|
1181
1466
|
|
|
1467
|
+
/**
|
|
1468
|
+
* Publish the slideshow-driven player chrome as container state.
|
|
1469
|
+
* Everything here is opt-in: unset options leave the container exactly
|
|
1470
|
+
* as the long-standing layout does, so existing stories are unaffected.
|
|
1471
|
+
*/
|
|
1472
|
+
_applyChrome(): void {
|
|
1473
|
+
const options = this.options;
|
|
1474
|
+
const container = this._el.container;
|
|
1475
|
+
// idempotent: runtime option changes re-run this, so previous state
|
|
1476
|
+
// is cleared first (unset means the long-standing layout)
|
|
1477
|
+
container.classList.remove(
|
|
1478
|
+
"vco-textmode-left",
|
|
1479
|
+
"vco-textmode-bottom",
|
|
1480
|
+
"vco-no-headings",
|
|
1481
|
+
"vco-no-scrollbars",
|
|
1482
|
+
"vco-mode-static",
|
|
1483
|
+
);
|
|
1484
|
+
if (options.hudcolor) {
|
|
1485
|
+
container.style.setProperty("--vco-hud-fg", options.hudcolor);
|
|
1486
|
+
} else {
|
|
1487
|
+
container.style.removeProperty("--vco-hud-fg");
|
|
1488
|
+
}
|
|
1489
|
+
if (options.hudbgcolor) {
|
|
1490
|
+
const opacity = asClampedNumber(options.hudopacity, 0, 100);
|
|
1491
|
+
container.style.setProperty(
|
|
1492
|
+
"--vco-hud-bg",
|
|
1493
|
+
opacity === null
|
|
1494
|
+
? options.hudbgcolor
|
|
1495
|
+
: `color-mix(in srgb, ${options.hudbgcolor} ${Math.round(opacity)}%, transparent)`,
|
|
1496
|
+
);
|
|
1497
|
+
} else {
|
|
1498
|
+
container.style.removeProperty("--vco-hud-bg");
|
|
1499
|
+
}
|
|
1500
|
+
if (typeof options.viewerheight === "string" && options.viewerheight !== "") {
|
|
1501
|
+
container.style.height = options.viewerheight;
|
|
1502
|
+
} else {
|
|
1503
|
+
container.style.removeProperty("height");
|
|
1504
|
+
}
|
|
1505
|
+
if (options.textmode === "left" || options.textmode === "bottom") {
|
|
1506
|
+
container.classList.add(`vco-textmode-${options.textmode}`);
|
|
1507
|
+
}
|
|
1508
|
+
if (options.show_headings === false) {
|
|
1509
|
+
container.classList.add("vco-no-headings");
|
|
1510
|
+
}
|
|
1511
|
+
if (options.show_scrollbars === false) {
|
|
1512
|
+
container.classList.add("vco-no-scrollbars");
|
|
1513
|
+
}
|
|
1514
|
+
if (options.mode === "static") {
|
|
1515
|
+
container.classList.add("vco-mode-static");
|
|
1516
|
+
}
|
|
1517
|
+
const textsize = options.textsize;
|
|
1518
|
+
if (typeof textsize === "number" && Number.isFinite(textsize)) {
|
|
1519
|
+
const clamped = asClampedNumber(textsize, 10, 80) ?? 50;
|
|
1520
|
+
container.style.setProperty("--vco-panel-size", `${clamped}%`);
|
|
1521
|
+
} else {
|
|
1522
|
+
container.style.removeProperty("--vco-panel-size");
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
/**
|
|
1527
|
+
* The slide panel share of the width (side docks) or height (bottom
|
|
1528
|
+
* dock) as a fraction: `textsize` percent when valid (10..80), else the
|
|
1529
|
+
* built-in split (half for sides, 40% height for the bottom dock).
|
|
1530
|
+
*/
|
|
1531
|
+
_panelShare(): number {
|
|
1532
|
+
const textsize = this.options.textsize;
|
|
1533
|
+
if (typeof textsize === "number" && Number.isFinite(textsize)) {
|
|
1534
|
+
return (asClampedNumber(textsize, 10, 80) ?? 50) / 100;
|
|
1535
|
+
}
|
|
1536
|
+
return this.options.textmode === "bottom" ? 0.4 : 0.5;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1182
1539
|
/* Private Methods
|
|
1183
1540
|
================================================== */
|
|
1184
1541
|
|
|
@@ -1190,6 +1547,18 @@ class StoryMapBase {
|
|
|
1190
1547
|
this._el.container.className += " vco-storymap";
|
|
1191
1548
|
this.options.base_class = this._el.container.className;
|
|
1192
1549
|
|
|
1550
|
+
// slideshow-driven player chrome first: viewerheight is an inline
|
|
1551
|
+
// height that the measurement below must see (classes are re-applied
|
|
1552
|
+
// in _updateDisplay, which rebuilds className from base_class)
|
|
1553
|
+
this._applyChrome();
|
|
1554
|
+
|
|
1555
|
+
// Colour theme (dark theme plan): an explicit option pins the
|
|
1556
|
+
// palette via data-vco-theme; unset, the CSS falls back to
|
|
1557
|
+
// prefers-color-scheme. Construction-time only.
|
|
1558
|
+
if (this.options.theme === "dark" || this.options.theme === "light") {
|
|
1559
|
+
this._el.container.setAttribute("data-vco-theme", this.options.theme);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1193
1562
|
// Text color theming (issue #177): expose the text colors as CSS
|
|
1194
1563
|
// custom properties consumed by the slide typography
|
|
1195
1564
|
this._applyTextColors();
|
|
@@ -1200,6 +1569,17 @@ class StoryMapBase {
|
|
|
1200
1569
|
? null
|
|
1201
1570
|
: (this._resolveMapElement() ?? Dom.create("div", "vco-map", this._el.container));
|
|
1202
1571
|
this._el.storyslider = Dom.create("div", "vco-storyslider", this._el.container);
|
|
1572
|
+
// Skip link (a11y): first tab stop inside the widget, jumping to
|
|
1573
|
+
// the slide content. Only when the host container has an id to
|
|
1574
|
+
// anchor both ends to (multi-viewer pages need unique targets).
|
|
1575
|
+
if (this._el.container.id !== "") {
|
|
1576
|
+
this._el.storyslider.id = `${this._el.container.id}-slides`;
|
|
1577
|
+
const skip = Dom.create("a", "vco-skip-link", this._el.container);
|
|
1578
|
+
skip.setAttribute("href", `#${this._el.storyslider.id}`);
|
|
1579
|
+
skip.textContent =
|
|
1580
|
+
(Language.messages.skip_link as string | undefined) ?? "Skip to story content";
|
|
1581
|
+
this._el.container.prepend(skip);
|
|
1582
|
+
}
|
|
1203
1583
|
|
|
1204
1584
|
// Initial Default Layout
|
|
1205
1585
|
this.options.width = this._el.container.offsetWidth;
|
|
@@ -1229,9 +1609,7 @@ class StoryMapBase {
|
|
|
1229
1609
|
// image readiness (IIIF/zoomify sources attach asynchronously) is
|
|
1230
1610
|
// re-fired on the StoryMap, the coordination point for hosts that
|
|
1231
1611
|
// overlay or measure their own layers
|
|
1232
|
-
this._map.on("imageready",
|
|
1233
|
-
this.fire("imageready", e);
|
|
1234
|
-
});
|
|
1612
|
+
this._map.on("imageready", this._onImageReady, this);
|
|
1235
1613
|
|
|
1236
1614
|
// Map Background Color
|
|
1237
1615
|
this._map_el().style.backgroundColor = this.options.map_background_color;
|
|
@@ -1239,6 +1617,10 @@ class StoryMapBase {
|
|
|
1239
1617
|
|
|
1240
1618
|
// Create Menu Bar
|
|
1241
1619
|
this._menubar = new MenuBar(this._el.menubar, this._el.container, this.options);
|
|
1620
|
+
// the progress chrome carries its initial state from construction,
|
|
1621
|
+
// not from the first map/slider load event — otherwise the bar (and
|
|
1622
|
+
// its ARIA) sits uninitialized until tiles arrive (issue #247)
|
|
1623
|
+
this._updateProgress();
|
|
1242
1624
|
|
|
1243
1625
|
// Create StorySlider
|
|
1244
1626
|
this._storyslider = new StorySlider(this._el.storyslider, this.data, this.options);
|
|
@@ -1287,6 +1669,16 @@ class StoryMapBase {
|
|
|
1287
1669
|
|
|
1288
1670
|
// Map Events
|
|
1289
1671
|
this._map?.on("change", this._onMapChange, this);
|
|
1672
|
+
this._map?.on("popupopen", this._onMarkerPopupOpen, this);
|
|
1673
|
+
this._map?.on("popupclose", this._onMarkerPopupClose, this);
|
|
1674
|
+
this._map?.on("markerclick", this._onMarkerClick, this);
|
|
1675
|
+
this._map?.on("tilesallowed", this._onTilesAllowed, this);
|
|
1676
|
+
this._menubar.on("basemapchange", this._onLayersBasemap, this);
|
|
1677
|
+
this._menubar.on("overlaychange", this._onLayersOverlay, this);
|
|
1678
|
+
this._menubar.on("autoplay_toggle", this._onAutoplayToggle, this);
|
|
1679
|
+
this._menubar.on("progress_go", this._onProgressGo, this);
|
|
1680
|
+
this._menubar.setLayersDelegate({ rows: () => this._layersRows() });
|
|
1681
|
+
this._menubar.refreshLayers();
|
|
1290
1682
|
|
|
1291
1683
|
// Global slide navigation (opt-in): the slider only listens on its
|
|
1292
1684
|
// own panel, which needs focus.
|
|
@@ -1313,6 +1705,18 @@ class StoryMapBase {
|
|
|
1313
1705
|
// Fullscreen state
|
|
1314
1706
|
this._on_fullscreen = () => this._onFullscreenChange();
|
|
1315
1707
|
document.addEventListener("fullscreenchange", this._on_fullscreen);
|
|
1708
|
+
|
|
1709
|
+
// An OS reduced-motion toggle mid-story stops automatic advancement;
|
|
1710
|
+
// the next navigation's _scheduleAutoplay re-checks anyway, but an
|
|
1711
|
+
// armed timer would otherwise fire once more first.
|
|
1712
|
+
if (typeof window.matchMedia === "function") {
|
|
1713
|
+
this._on_reduced_motion = (e: MediaQueryListEvent) => {
|
|
1714
|
+
if (e.matches) this._stopAutoplay();
|
|
1715
|
+
};
|
|
1716
|
+
window
|
|
1717
|
+
.matchMedia(REDUCED_MOTION_QUERY)
|
|
1718
|
+
.addEventListener("change", this._on_reduced_motion);
|
|
1719
|
+
}
|
|
1316
1720
|
}
|
|
1317
1721
|
|
|
1318
1722
|
_onKeyDownGlobal(e: KeyboardEvent) {
|
|
@@ -1511,23 +1915,53 @@ class StoryMapBase {
|
|
|
1511
1915
|
display_class += " vco-map-area-left";
|
|
1512
1916
|
this._map_el().style.width = Math.floor(this.options.width / 2) + "px";
|
|
1513
1917
|
this._map_required().setMapOffset(0, 0);
|
|
1918
|
+
} else if (this.options.textmode === "bottom") {
|
|
1919
|
+
// bottom dock: the map takes the top share, the slider the
|
|
1920
|
+
// bottom share (like the portrait split, but keeping the
|
|
1921
|
+
// landscape slide styling)
|
|
1922
|
+
display_class += " vco-textmode-bottom";
|
|
1923
|
+
const share = this._panelShare();
|
|
1924
|
+
this.options.map_height = Math.floor(this.options.height * (1 - share));
|
|
1925
|
+
this.options.storyslider_height = this.options.height - this.options.map_height - 1;
|
|
1926
|
+
this._map_el().style.width = "100%";
|
|
1927
|
+
this._map_el().style.height = this.options.map_height + "px";
|
|
1928
|
+
this._map_required().setMapOffset(0, 0);
|
|
1929
|
+
this._el.storyslider.style.top = this.options.map_height + "px";
|
|
1930
|
+
this._el.storyslider.style.height = this.options.storyslider_height + "px";
|
|
1931
|
+
this._menubar.updateDisplay(this.options.width, this.options.height, animate);
|
|
1932
|
+
this._map_required().updateDisplay(
|
|
1933
|
+
this.options.width,
|
|
1934
|
+
this.options.height,
|
|
1935
|
+
animate,
|
|
1936
|
+
d,
|
|
1937
|
+
);
|
|
1938
|
+
this._storyslider.updateDisplay(
|
|
1939
|
+
this.options.width,
|
|
1940
|
+
this.options.storyslider_height,
|
|
1941
|
+
animate,
|
|
1942
|
+
this.options.layout,
|
|
1943
|
+
);
|
|
1514
1944
|
} else {
|
|
1515
1945
|
this._map_el().style.width = "100%";
|
|
1516
1946
|
this._map_required().setMapOffset(-(this.options.width / 4), 0);
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
this._el.storyslider.style.top = "0";
|
|
1521
|
-
this._el.storyslider.style.height = this.options.storyslider_height + "px";
|
|
1947
|
+
// StorySlider
|
|
1948
|
+
this._el.storyslider.style.top = "0";
|
|
1949
|
+
this._el.storyslider.style.height = this.options.storyslider_height + "px";
|
|
1522
1950
|
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1951
|
+
this._menubar.updateDisplay(this.options.width, this.options.height, animate);
|
|
1952
|
+
this._map_required().updateDisplay(
|
|
1953
|
+
this.options.width,
|
|
1954
|
+
this.options.height,
|
|
1955
|
+
animate,
|
|
1956
|
+
d,
|
|
1957
|
+
);
|
|
1958
|
+
this._storyslider.updateDisplay(
|
|
1959
|
+
this.options.width * this._panelShare(),
|
|
1960
|
+
this.options.storyslider_height,
|
|
1961
|
+
animate,
|
|
1962
|
+
this.options.layout,
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1531
1965
|
}
|
|
1532
1966
|
|
|
1533
1967
|
// the resolved locale decides the layout direction (issues #211, #245)
|
|
@@ -1537,6 +1971,10 @@ class StoryMapBase {
|
|
|
1537
1971
|
|
|
1538
1972
|
// Apply class
|
|
1539
1973
|
this._el.container.className = display_class;
|
|
1974
|
+
// _updateDisplay rebuilds the container class from base_class, which
|
|
1975
|
+
// wipes the opt-in chrome state _initLayout applied — re-apply it
|
|
1976
|
+
// after every layout pass (init, resize, runtime option changes)
|
|
1977
|
+
this._applyChrome();
|
|
1540
1978
|
}
|
|
1541
1979
|
|
|
1542
1980
|
/* Events
|
|
@@ -1589,14 +2027,18 @@ class StoryMapBase {
|
|
|
1589
2027
|
// to have no slides at all)
|
|
1590
2028
|
const seen = new Set<string>();
|
|
1591
2029
|
for (const slide of this.data.slides ?? []) {
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
2030
|
+
// every visible media item is its own service (#358)
|
|
2031
|
+
const items = [slide.media, ...(slide.media_extra ?? [])];
|
|
2032
|
+
for (const item of items) {
|
|
2033
|
+
const url = (item as { url?: string | null } | null)?.url;
|
|
2034
|
+
if (!url) continue;
|
|
2035
|
+
const match = MediaType({ url } as never) as { type: string; name: string } | false;
|
|
2036
|
+
if (!match || seen.has(match.type)) continue;
|
|
2037
|
+
seen.add(match.type);
|
|
2038
|
+
// same registry the per-slide panel uses, so the two dialogs can
|
|
2039
|
+
// never disagree about a service's name
|
|
2040
|
+
services.push(mediaService(match.type, match.name));
|
|
2041
|
+
}
|
|
1600
2042
|
}
|
|
1601
2043
|
// Slide narration is its own service: the recording plays through a
|
|
1602
2044
|
// dedicated audio element, not through Media, so nothing else asks
|
|
@@ -1611,16 +2053,21 @@ class StoryMapBase {
|
|
|
1611
2053
|
// origin, not by a prefix test on the resolved URL: resolveFontCssUrl
|
|
1612
2054
|
// has already turned a relative path into an absolute one, so testing
|
|
1613
2055
|
// that for "http" made every relative font_css look external.
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
2056
|
+
// A host-bundled theme (font_css: false) loads no stylesheet, so it
|
|
2057
|
+
// contributes no font service either.
|
|
2058
|
+
const originalFontCss = fontCssOriginal(this.options.font_css);
|
|
2059
|
+
if (originalFontCss !== null) {
|
|
2060
|
+
const font = resolveFontCssUrl(originalFontCss);
|
|
2061
|
+
if (font !== null && isExternalUrl(font)) {
|
|
2062
|
+
services.push(fontService());
|
|
2063
|
+
}
|
|
1617
2064
|
}
|
|
1618
2065
|
manager.requestAll(services, this._el.container);
|
|
1619
2066
|
}
|
|
1620
2067
|
|
|
1621
2068
|
/* Autoplay (issue #380) and hash bookmarks (issue #146)
|
|
1622
2069
|
================================================== */
|
|
1623
|
-
/* Narration
|
|
2070
|
+
/* Narration
|
|
1624
2071
|
================================================== */
|
|
1625
2072
|
|
|
1626
2073
|
/**
|
|
@@ -1638,7 +2085,49 @@ class StoryMapBase {
|
|
|
1638
2085
|
* playback, and the story still works.
|
|
1639
2086
|
*/
|
|
1640
2087
|
_playNarration(slide: StorymapSlide | undefined, allow_without_gesture = false) {
|
|
1641
|
-
|
|
2088
|
+
// Slideshow-only playback flags render only for slideshow tours;
|
|
2089
|
+
// internal documents play the bare URL exactly as before (no data is
|
|
2090
|
+
// mutated — the bag is reduced to a local).
|
|
2091
|
+
const raw = slide?.narration ?? null;
|
|
2092
|
+
const bag =
|
|
2093
|
+
this.options.slideshow_source === true
|
|
2094
|
+
? raw
|
|
2095
|
+
: raw && typeof raw.url === "string"
|
|
2096
|
+
? { url: raw.url }
|
|
2097
|
+
: null;
|
|
2098
|
+
const url = typeof bag?.url === "string" && bag.url !== "" ? bag.url : null;
|
|
2099
|
+
const persistent = bag?.stopOnExit === false;
|
|
2100
|
+
if (url !== null && persistent) {
|
|
2101
|
+
// An audio bed that outlives its slide (slideshow ambient
|
|
2102
|
+
// audio): already playing this bed → keep it, dropping any
|
|
2103
|
+
// foreground left over from the slide we came from. At most one
|
|
2104
|
+
// ambient track plays; a new one replaces the old.
|
|
2105
|
+
if (this._ambient_meta?.url === url && this._ambient_playing) {
|
|
2106
|
+
this._stopNarration();
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
this._stopNarration();
|
|
2110
|
+
if (bag?.stopAllPrevious !== false) this._stopAmbient();
|
|
2111
|
+
if (this._disposed || !this._narration_allowed) return;
|
|
2112
|
+
if (!allow_without_gesture && !this._has_user_gesture()) {
|
|
2113
|
+
this._replayNarrationAfterGesture = true;
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
const el = this._ambientElement();
|
|
2117
|
+
this._ambient_meta = { url };
|
|
2118
|
+
this._startTrack(el, url, bag, allow_without_gesture);
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
// An ephemeral track (or none): a newcomer that does not opt out of
|
|
2122
|
+
// stopping replaces any ambient bed; a silent slide keeps a playing
|
|
2123
|
+
// bed (it belongs to the tour, not to the slide we left).
|
|
2124
|
+
if (url === null || bag?.stopAllPrevious !== false) {
|
|
2125
|
+
if (url === null && this._ambient_playing) {
|
|
2126
|
+
this._stopNarration();
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
this._stopAmbient();
|
|
2130
|
+
}
|
|
1642
2131
|
this._stopNarration();
|
|
1643
2132
|
if (!url || this._disposed || !this._narration_allowed) return;
|
|
1644
2133
|
if (!allow_without_gesture && !this._has_user_gesture()) {
|
|
@@ -1646,16 +2135,111 @@ class StoryMapBase {
|
|
|
1646
2135
|
return;
|
|
1647
2136
|
}
|
|
1648
2137
|
const el = this._narrationElement();
|
|
2138
|
+
this._narration_meta = { url, stopOnExit: bag?.stopOnExit !== false };
|
|
2139
|
+
this._startTrack(el, url, bag, allow_without_gesture);
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
/**
|
|
2143
|
+
* Start (or arm) a narration track: loop flag, start offset once the
|
|
2144
|
+
* metadata is available, and `play: "click"` arms paused for the
|
|
2145
|
+
* visitor instead of playing (see `playNarration()`). A rejected play()
|
|
2146
|
+
* (policy, network) must not surface as an unhandled rejection.
|
|
2147
|
+
*/
|
|
2148
|
+
_startTrack(
|
|
2149
|
+
el: HTMLAudioElement,
|
|
2150
|
+
url: string,
|
|
2151
|
+
bag: StorymapSlideNarration | null,
|
|
2152
|
+
allow_without_gesture: boolean,
|
|
2153
|
+
) {
|
|
2154
|
+
el.loop = bag?.loop === true;
|
|
1649
2155
|
el.src = url;
|
|
1650
|
-
el
|
|
2156
|
+
this._seekAudio(el, bag?.offset);
|
|
2157
|
+
if (bag?.play === "click") return;
|
|
2158
|
+
if (!allow_without_gesture && !this._has_user_gesture()) {
|
|
2159
|
+
this._replayNarrationAfterGesture = true;
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
if (el === this._ambient_el) {
|
|
2163
|
+
this._ambient_playing = true;
|
|
2164
|
+
}
|
|
2165
|
+
const token = ++this._narration_token;
|
|
2166
|
+
void el.play()?.catch?.(() => {
|
|
2167
|
+
if (token !== this._narration_token) return;
|
|
2168
|
+
// a failed start is not playing: the gesture retry must attempt
|
|
2169
|
+
// it again rather than take the same-URL fast path
|
|
2170
|
+
if (el === this._ambient_el) {
|
|
2171
|
+
this._ambient_playing = false;
|
|
2172
|
+
}
|
|
2173
|
+
this._replayNarrationAfterGesture = true;
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
/**
|
|
2178
|
+
* Start the current slide's click-armed narration (`narration.play:
|
|
2179
|
+
* "click"`): the control for visitors (or hosts) to start a track that
|
|
2180
|
+
* waits for them. Auto-armed tracks play on their own. No-op when
|
|
2181
|
+
* nothing is armed. Prefers the current slide's own track: with an
|
|
2182
|
+
* ambient bed running, the foreground it armed is the one to start.
|
|
2183
|
+
*/
|
|
2184
|
+
playNarration(): void {
|
|
2185
|
+
const bag = this.data.slides?.[this.current_slide]?.narration ?? null;
|
|
2186
|
+
const persistent = bag?.stopOnExit === false && typeof bag?.url === "string";
|
|
2187
|
+
const el =
|
|
2188
|
+
persistent && this._ambient_meta?.url === bag?.url
|
|
2189
|
+
? this._ambient_el
|
|
2190
|
+
: this._narration_el;
|
|
2191
|
+
if (!el || !el.getAttribute("src")) return;
|
|
2192
|
+
if (this._disposed || !this._narration_allowed || this._has_user_gesture() === false) {
|
|
2193
|
+
this._replayNarrationAfterGesture = true;
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
if (el === this._ambient_el) {
|
|
2197
|
+
this._ambient_playing = true;
|
|
2198
|
+
}
|
|
1651
2199
|
const token = ++this._narration_token;
|
|
1652
|
-
// a rejected play() (policy, network) must not surface as an
|
|
1653
|
-
// unhandled rejection
|
|
1654
2200
|
void el.play()?.catch?.(() => {
|
|
1655
|
-
if (token
|
|
2201
|
+
if (token !== this._narration_token) return;
|
|
2202
|
+
if (el === this._ambient_el) {
|
|
2203
|
+
this._ambient_playing = false;
|
|
2204
|
+
}
|
|
2205
|
+
this._replayNarrationAfterGesture = true;
|
|
1656
2206
|
});
|
|
1657
2207
|
}
|
|
1658
2208
|
|
|
2209
|
+
/**
|
|
2210
|
+
* Seek to a start offset in seconds (slideshow `audio.offset`):
|
|
2211
|
+
* immediately when the metadata is already there, otherwise once on
|
|
2212
|
+
* `loadedmetadata`. Guarded by the narration token, so a quick
|
|
2213
|
+
* pass-through cannot seek a newer track.
|
|
2214
|
+
*/
|
|
2215
|
+
_seekAudio(el: HTMLAudioElement, offset: number | null | undefined) {
|
|
2216
|
+
const target =
|
|
2217
|
+
typeof offset === "number" && Number.isFinite(offset) && offset > 0 ? offset : 0;
|
|
2218
|
+
const token = this._narration_token;
|
|
2219
|
+
const apply = () => {
|
|
2220
|
+
if (token !== this._narration_token || this._disposed) return;
|
|
2221
|
+
try {
|
|
2222
|
+
el.currentTime = target;
|
|
2223
|
+
} catch {
|
|
2224
|
+
// metadata not ready yet; the listener below retries
|
|
2225
|
+
}
|
|
2226
|
+
};
|
|
2227
|
+
if (el.readyState >= 1) {
|
|
2228
|
+
apply();
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
const on_metadata = () => {
|
|
2232
|
+
el.removeEventListener("loadedmetadata", on_metadata);
|
|
2233
|
+
apply();
|
|
2234
|
+
};
|
|
2235
|
+
el.addEventListener("loadedmetadata", on_metadata);
|
|
2236
|
+
try {
|
|
2237
|
+
el.currentTime = target;
|
|
2238
|
+
} catch {
|
|
2239
|
+
// pre-metadata seek throws in some browsers; the listener covers it
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
|
|
1659
2243
|
_narrationElement(): HTMLAudioElement {
|
|
1660
2244
|
if (this._narration_el === null) {
|
|
1661
2245
|
this._narration_el = document.createElement("audio");
|
|
@@ -1668,12 +2252,33 @@ class StoryMapBase {
|
|
|
1668
2252
|
|
|
1669
2253
|
_stopNarration() {
|
|
1670
2254
|
this._narration_token++;
|
|
2255
|
+
this._narration_meta = null;
|
|
1671
2256
|
const el = this._narration_el;
|
|
1672
2257
|
if (!el) return;
|
|
1673
2258
|
el.pause();
|
|
1674
2259
|
el.removeAttribute("src");
|
|
1675
2260
|
}
|
|
1676
2261
|
|
|
2262
|
+
_ambientElement(): HTMLAudioElement {
|
|
2263
|
+
if (this._ambient_el === null) {
|
|
2264
|
+
this._ambient_el = document.createElement("audio");
|
|
2265
|
+
this._ambient_el.className = "vco-media-item vco-ambient";
|
|
2266
|
+
this._ambient_el.preload = "none";
|
|
2267
|
+
this._ambient_el.setAttribute("aria-hidden", "true");
|
|
2268
|
+
}
|
|
2269
|
+
return this._ambient_el;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
_stopAmbient() {
|
|
2273
|
+
this._narration_token++;
|
|
2274
|
+
this._ambient_meta = null;
|
|
2275
|
+
this._ambient_playing = false;
|
|
2276
|
+
const el = this._ambient_el;
|
|
2277
|
+
if (!el) return;
|
|
2278
|
+
el.pause();
|
|
2279
|
+
el.removeAttribute("src");
|
|
2280
|
+
}
|
|
2281
|
+
|
|
1677
2282
|
/** A gesture has happened, so unmuted audio is allowed from now on. */
|
|
1678
2283
|
_note_user_gesture_for_narration() {
|
|
1679
2284
|
if (this._disposed) return;
|
|
@@ -1710,10 +2315,16 @@ class StoryMapBase {
|
|
|
1710
2315
|
}
|
|
1711
2316
|
// prefers-reduced-motion: no autoplay (WCAG 2.2.2)
|
|
1712
2317
|
if (this.options.autoplay > 0 && !prefersReducedMotion()) {
|
|
1713
|
-
// any user interaction stops autoplay permanently
|
|
1714
|
-
|
|
2318
|
+
// any user interaction stops autoplay permanently — except on
|
|
2319
|
+
// the pause/resume toggle itself, whose click would otherwise
|
|
2320
|
+
// stop and immediately restart (net no-op instead of pause)
|
|
2321
|
+
const stop = (e: Event) => {
|
|
2322
|
+
if ((e.target as HTMLElement | null)?.closest?.(".vco-menubar-autoplay")) {
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
1715
2325
|
this._autoplay_stopped = true;
|
|
1716
2326
|
this._stopAutoplay();
|
|
2327
|
+
this._menubar.setAutoplayState(true);
|
|
1717
2328
|
};
|
|
1718
2329
|
this._el.container.addEventListener("pointerdown", stop, { once: true });
|
|
1719
2330
|
this._el.container.addEventListener("keydown", stop, { once: true });
|
|
@@ -1722,6 +2333,45 @@ class StoryMapBase {
|
|
|
1722
2333
|
}
|
|
1723
2334
|
}
|
|
1724
2335
|
|
|
2336
|
+
/**
|
|
2337
|
+
* Stop other slides' ambient audio beds when the new slide claims the
|
|
2338
|
+
* audio: its narration (unless opted out via `stopAllPrevious: false`),
|
|
2339
|
+
* or playable media that is actually playing (a paused click-to-play
|
|
2340
|
+
* bed must not kill an inherited ambient track it never starts).
|
|
2341
|
+
* A silent slide inherits a playing bed.
|
|
2342
|
+
*/
|
|
2343
|
+
_stopAmbientSlideMedia(slide: StorymapSlide | undefined) {
|
|
2344
|
+
const claimsNarration =
|
|
2345
|
+
typeof slide?.narration?.url === "string" &&
|
|
2346
|
+
slide.narration.url !== "" &&
|
|
2347
|
+
slide.narration.stopAllPrevious !== false;
|
|
2348
|
+
const claimsMedia =
|
|
2349
|
+
(slide?.media as { stopAllPrevious?: boolean } | null | undefined)?.stopAllPrevious !==
|
|
2350
|
+
false && this._isSlideMediaPlaying(this.current_slide);
|
|
2351
|
+
if (!claimsNarration && !claimsMedia) return;
|
|
2352
|
+
const slides = this._storyslider?._slides;
|
|
2353
|
+
if (!slides) return;
|
|
2354
|
+
slides.forEach((entry, index) => {
|
|
2355
|
+
if (index !== this.current_slide) entry.stopAmbientMedia?.();
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
/**
|
|
2360
|
+
* True when the slide's media element exists and is currently playing
|
|
2361
|
+
* (not paused, not ended, not merely loadable).
|
|
2362
|
+
*/
|
|
2363
|
+
_isSlideMediaPlaying(index: number): boolean {
|
|
2364
|
+
const slide = this._storyslider?._slides?.[index];
|
|
2365
|
+
const medias = (
|
|
2366
|
+
slide as unknown as { _medias?: { player_element?: HTMLMediaElement | null }[] }
|
|
2367
|
+
)?._medias;
|
|
2368
|
+
if (!Array.isArray(medias)) return false;
|
|
2369
|
+
return medias.some((media) => {
|
|
2370
|
+
const el = media?.player_element;
|
|
2371
|
+
return !!el && typeof el.paused === "boolean" && !el.paused && !el.ended;
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
|
|
1725
2375
|
/**
|
|
1726
2376
|
* Arm autoplay for the current slide.
|
|
1727
2377
|
*
|
|
@@ -1737,6 +2387,10 @@ class StoryMapBase {
|
|
|
1737
2387
|
_scheduleAutoplay() {
|
|
1738
2388
|
this._stopAutoplay();
|
|
1739
2389
|
if (!(this.options.autoplay > 0) || this._autoplay_stopped) return;
|
|
2390
|
+
// the OS setting may have flipped mid-story: re-check on every arm
|
|
2391
|
+
// so a reduced-motion switch stops future advances (the in-flight
|
|
2392
|
+
// timer is cleared by the next navigation's _scheduleAutoplay)
|
|
2393
|
+
if (prefersReducedMotion()) return;
|
|
1740
2394
|
|
|
1741
2395
|
const advance = () => {
|
|
1742
2396
|
this._stopAutoplay();
|
|
@@ -1747,14 +2401,33 @@ class StoryMapBase {
|
|
|
1747
2401
|
};
|
|
1748
2402
|
this._autoplay_advance = advance;
|
|
1749
2403
|
|
|
2404
|
+
// Per-slide dwell (slideshow slidetimeout): overrides the global
|
|
2405
|
+
// interval for this slide; 0 holds here even with autoplay on.
|
|
2406
|
+
// Internal documents always use the global interval.
|
|
2407
|
+
const rawTimeout = this.data.slides?.[this.current_slide]?.slidetimeout;
|
|
2408
|
+
const slideTimeout = this.options.slideshow_source === true ? rawTimeout : undefined;
|
|
2409
|
+
const dwell =
|
|
2410
|
+
typeof slideTimeout === "number" && Number.isFinite(slideTimeout) && slideTimeout >= 0
|
|
2411
|
+
? slideTimeout
|
|
2412
|
+
: this.options.autoplay;
|
|
2413
|
+
if (!(dwell > 0)) return;
|
|
2414
|
+
|
|
1750
2415
|
if (this.options.autoplay_media) {
|
|
1751
2416
|
const slide = this._storyslider?._slides?.[this.current_slide];
|
|
1752
2417
|
if (slide?.hasPlayableMedia?.()) {
|
|
1753
|
-
|
|
2418
|
+
// re-arming clears the previous advance: without this,
|
|
2419
|
+
// revisits accumulate listeners and a stale ended (e.g. an
|
|
2420
|
+
// ambient bed outliving its slide) advances the wrong slide.
|
|
2421
|
+
// The index guard covers the race between clearing and firing.
|
|
2422
|
+
slide.clearMediaEnded();
|
|
2423
|
+
const armed = this.current_slide;
|
|
2424
|
+
slide.onMediaEnded(() => {
|
|
2425
|
+
if (this.current_slide === armed) advance();
|
|
2426
|
+
});
|
|
1754
2427
|
}
|
|
1755
2428
|
}
|
|
1756
2429
|
|
|
1757
|
-
this._autoplay_timer = setTimeout(advance,
|
|
2430
|
+
this._autoplay_timer = setTimeout(advance, dwell);
|
|
1758
2431
|
}
|
|
1759
2432
|
|
|
1760
2433
|
_stopAutoplay() {
|
|
@@ -1959,7 +2632,7 @@ class StoryMapBase {
|
|
|
1959
2632
|
return el;
|
|
1960
2633
|
}
|
|
1961
2634
|
|
|
1962
|
-
_onTitle(e:
|
|
2635
|
+
_onTitle(e: { title: string }) {
|
|
1963
2636
|
this.fire("title", e);
|
|
1964
2637
|
}
|
|
1965
2638
|
|
|
@@ -1985,8 +2658,148 @@ class StoryMapBase {
|
|
|
1985
2658
|
}
|
|
1986
2659
|
}
|
|
1987
2660
|
|
|
2661
|
+
_onImageReady(e: ImagereadyPayload) {
|
|
2662
|
+
this.fire("imageready", e);
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
_onMarkerPopupOpen(e: { marker_number: number }) {
|
|
2666
|
+
this.fire("popupopen", {
|
|
2667
|
+
marker_number: e.marker_number,
|
|
2668
|
+
current_slide: this.current_slide,
|
|
2669
|
+
});
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
_onMarkerPopupClose(e: { marker_number: number }) {
|
|
2673
|
+
this.fire("popupclose", {
|
|
2674
|
+
marker_number: e.marker_number,
|
|
2675
|
+
current_slide: this.current_slide,
|
|
2676
|
+
});
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
_onMarkerClick(e: { marker_number: number }) {
|
|
2680
|
+
// Re-fired from the map surface so a host listening on the viewer
|
|
2681
|
+
// sees marker clicks; the map already navigated itself.
|
|
2682
|
+
this.fire("markerclick", {
|
|
2683
|
+
marker_number: e.marker_number,
|
|
2684
|
+
current_slide: this.current_slide,
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
_onLayersBasemap(e: { map_type: string }) {
|
|
2689
|
+
const previous = this.options.map_type;
|
|
2690
|
+
this.setMapOption("map_type", e.map_type);
|
|
2691
|
+
this._menubar.refreshLayers();
|
|
2692
|
+
this.fire("basemapchange", { map_type: e.map_type, previous });
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
_onLayersOverlay(e: { index: number; visible: boolean }) {
|
|
2696
|
+
// setOverlayVisible re-syncs the attribution line; the checkbox
|
|
2697
|
+
// already shows the new state, so no refresh (which would steal
|
|
2698
|
+
// keyboard focus from the row just toggled)
|
|
2699
|
+
this.setOverlayVisible(e.index, e.visible);
|
|
2700
|
+
this.fire("overlaychange", { index: e.index, visible: e.visible });
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
_onAutoplayToggle() {
|
|
2704
|
+
if (this._autoplay_stopped) {
|
|
2705
|
+
this._autoplay_stopped = false;
|
|
2706
|
+
this._scheduleAutoplay();
|
|
2707
|
+
} else {
|
|
2708
|
+
this._autoplay_stopped = true;
|
|
2709
|
+
this._stopAutoplay();
|
|
2710
|
+
}
|
|
2711
|
+
this._menubar.setAutoplayState(this._autoplay_stopped);
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
_onProgressGo(e: { slide: number }) {
|
|
2715
|
+
this.goTo(e.slide);
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
_onTilesAllowed() {
|
|
2719
|
+
this._menubar.refreshLayers();
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
/**
|
|
2723
|
+
* The layer switcher rows: allowed basemaps first, then built overlays.
|
|
2724
|
+
* Empty while there is nothing to switch (mapless story, consent-denied
|
|
2725
|
+
* tiles), in which case the control hides itself.
|
|
2726
|
+
*/
|
|
2727
|
+
_layersRows(): LayersRow[] {
|
|
2728
|
+
if (this._map_disabled || !this._map || !this._tilesAllowed()) {
|
|
2729
|
+
return [];
|
|
2730
|
+
}
|
|
2731
|
+
const rows: LayersRow[] = [];
|
|
2732
|
+
for (const basemap of this._allowedBasemaps()) {
|
|
2733
|
+
const label = basemap.label;
|
|
2734
|
+
rows.push({
|
|
2735
|
+
kind: "basemap",
|
|
2736
|
+
map_type: basemap.map_type,
|
|
2737
|
+
label: typeof label === "function" ? label() : (label ?? basemap.map_type),
|
|
2738
|
+
checked: basemap.map_type === this.options.map_type,
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2741
|
+
const entries = this._map.getOverlayEntries();
|
|
2742
|
+
const layers = this._map.getOverlayLayers();
|
|
2743
|
+
entries.forEach((entry, index) => {
|
|
2744
|
+
if (entry.control === false) return;
|
|
2745
|
+
const label = entry.label;
|
|
2746
|
+
rows.push({
|
|
2747
|
+
kind: "overlay",
|
|
2748
|
+
index,
|
|
2749
|
+
label: typeof label === "function" ? label() : (label ?? entry.map_type ?? ""),
|
|
2750
|
+
checked: layers[index]?.getVisible() ?? entry.visible ?? true,
|
|
2751
|
+
locked: entry.locked === true,
|
|
2752
|
+
});
|
|
2753
|
+
});
|
|
2754
|
+
return rows;
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
/**
|
|
2758
|
+
* Basemap candidates compatible with the live view. A runtime basemap
|
|
2759
|
+
* swap rebuilds layers but never the view, so an image-space candidate
|
|
2760
|
+
* (`iiif` with `map_as_image`) on a mercator view — or the reverse —
|
|
2761
|
+
* would wrap the wrong projection around the tiles: markers land in the
|
|
2762
|
+
* wrong place and zoom is meaningless. Refused candidates are dropped
|
|
2763
|
+
* with a warning, like the existing `setMapOptions` declines.
|
|
2764
|
+
*/
|
|
2765
|
+
_allowedBasemaps(): StorymapBasemap[] {
|
|
2766
|
+
const basemaps = this.options.basemaps;
|
|
2767
|
+
if (!Array.isArray(basemaps)) return [];
|
|
2768
|
+
const live = this.map?.getView()?.getProjection()?.getCode() ?? null;
|
|
2769
|
+
return basemaps.filter((entry) => {
|
|
2770
|
+
const map_type = entry?.map_type;
|
|
2771
|
+
if (typeof map_type !== "string" || map_type === "" || map_type === "none") {
|
|
2772
|
+
console.warn(
|
|
2773
|
+
`StoryMapJS: ignoring basemaps entry without a usable map_type: ${String(map_type)}`,
|
|
2774
|
+
);
|
|
2775
|
+
return false;
|
|
2776
|
+
}
|
|
2777
|
+
// the same rule `_createMap` builds the view by: image-space is
|
|
2778
|
+
// `iiif` with `map_as_image`, everything else is mercator
|
|
2779
|
+
const candidate =
|
|
2780
|
+
map_type === "iiif" && this.options.map_as_image ? "EPSG:4326" : "EPSG:3857";
|
|
2781
|
+
if (live !== null && candidate !== live) {
|
|
2782
|
+
console.warn(
|
|
2783
|
+
`StoryMapJS: ignoring basemap ${map_type}: it needs a ${candidate} view but the live view is ${live}`,
|
|
2784
|
+
);
|
|
2785
|
+
return false;
|
|
2786
|
+
}
|
|
2787
|
+
return true;
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
/** Whether deferred tile layers may attach (no consent gate, or granted). */
|
|
2792
|
+
_tilesAllowed(): boolean {
|
|
2793
|
+
if (!this.options.consent_required) return true;
|
|
2794
|
+
const manager = consentManagerOf(this.options);
|
|
2795
|
+
return manager ? manager.isGranted(tileService().key) : true;
|
|
2796
|
+
}
|
|
2797
|
+
|
|
1988
2798
|
_updateProgress() {
|
|
1989
|
-
|
|
2799
|
+
// any enabled style updates (the classic bar via show_progress, the
|
|
2800
|
+
// variants via progressbar); a disabled indicator stays untouched so
|
|
2801
|
+
// its ARIA never goes stale
|
|
2802
|
+
if (this._menubar && this._menubar._progressStyle() !== "off") {
|
|
1990
2803
|
this._menubar.setProgress(this.current_slide, this.data.slides.length);
|
|
1991
2804
|
}
|
|
1992
2805
|
}
|
|
@@ -2102,7 +2915,7 @@ class StoryMapBase {
|
|
|
2102
2915
|
}
|
|
2103
2916
|
}
|
|
2104
2917
|
|
|
2105
|
-
export default class StoryMap extends Evented(StoryMapBase) {
|
|
2918
|
+
export default class StoryMap extends Evented<StoryMapEvents, typeof StoryMapBase>(StoryMapBase) {
|
|
2106
2919
|
/**
|
|
2107
2920
|
* The library base path (the directory containing the module).
|
|
2108
2921
|
* Derived from `import.meta.url`, which works both for the source module
|