@projektemacher/storymapjs 0.10.6 → 0.10.7
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 +11 -0
- package/dist/assets/{demo-CVQ7kFek.js → demo-D6cTmzn6.js} +1 -1
- package/dist/assets/{harness-C-VZV-ou.js → harness-BPFlzutp.js} +1 -1
- package/dist/assets/main-Y-MYUSJ4.js +17 -0
- package/dist/css/fonts/font.oldstandard.css +18 -22
- package/dist/demo.html +2 -2
- package/dist/docs/migration.html +1 -1
- package/dist/embed/index.html +129 -4
- package/dist/harness.html +2 -2
- package/dist/index.html +17 -0
- package/dist/js/storymap.d.ts +126 -7
- package/dist/js/storymap.js +4518 -3613
- package/dist/js/storymap.js.map +1 -1
- package/docs/migration-from-knightlab.md +139 -0
- package/docs/storymap-as-iiif-manifest.md +510 -0
- package/package.json +6 -3
- package/src/animation/easings.ts +18 -0
- package/src/animation/morpheus.d.ts +41 -0
- package/src/core/Browser.ts +19 -0
- package/src/core/Load.ts +180 -0
- package/src/core/Util.ts +354 -0
- package/src/core/mixins.ts +171 -0
- package/src/dom/Dom.ts +31 -0
- package/src/dom/DomEvent.ts +61 -0
- package/src/globals.d.ts +16 -0
- package/src/language/Language.ts +107 -0
- package/src/language/locale/be.json +18 -0
- package/src/language/locale/bg.json +18 -0
- package/src/language/locale/cs.json +18 -0
- package/src/language/locale/de.json +20 -0
- package/src/language/locale/el.json +18 -0
- package/src/language/locale/en.json +28 -0
- package/src/language/locale/es.json +18 -0
- package/src/language/locale/et.json +18 -0
- package/src/language/locale/fr.json +18 -0
- package/src/language/locale/he.json +18 -0
- package/src/language/locale/hu.json +18 -0
- package/src/language/locale/is.json +18 -0
- package/src/language/locale/it.json +18 -0
- package/src/language/locale/jp.json +18 -0
- package/src/language/locale/ko.json +18 -0
- package/src/language/locale/nl.json +18 -0
- package/src/language/locale/nn.json +18 -0
- package/src/language/locale/no.json +18 -0
- package/src/language/locale/pl.json +18 -0
- package/src/language/locale/pt.json +18 -0
- package/src/language/locale/ru.json +18 -0
- package/src/language/locale/sk.json +18 -0
- package/src/language/locale/sr.json +17 -0
- package/src/language/locale/sv.json +18 -0
- package/src/language/locale/tr.json +18 -0
- package/src/language/locale/uk.json +18 -0
- package/src/language/locale/ur.json +18 -0
- package/src/language/locale/zh-cn.json +18 -0
- package/src/language/locale/zh-tw.json +18 -0
- package/src/main.ts +51 -0
- package/src/map/Map.ts +717 -0
- package/src/map/MapMarker.ts +149 -0
- package/src/map/openlayers/Map.OpenLayers.ts +1879 -0
- package/src/map/openlayers/MapMarker.OpenLayers.ts +176 -0
- package/src/map/openlayers/zoomifyTiles.ts +28 -0
- package/src/map/types.ts +17 -0
- package/src/media/EmbedUtil.ts +168 -0
- package/src/media/Media.ts +409 -0
- package/src/media/MediaType.ts +163 -0
- package/src/media/types/Audio.ts +87 -0
- package/src/media/types/Blockquote.ts +40 -0
- package/src/media/types/DailyMotion.ts +58 -0
- package/src/media/types/DocumentCloud.ts +41 -0
- package/src/media/types/Facebook.ts +41 -0
- package/src/media/types/Flickr.ts +115 -0
- package/src/media/types/GoogleDoc.ts +59 -0
- package/src/media/types/IFrame.ts +51 -0
- package/src/media/types/Image.ts +49 -0
- package/src/media/types/Juxtapose.ts +32 -0
- package/src/media/types/SoundCloud.ts +77 -0
- package/src/media/types/Text.ts +172 -0
- package/src/media/types/Twitter.ts +96 -0
- package/src/media/types/Video.ts +84 -0
- package/src/media/types/Vimeo.ts +65 -0
- package/src/media/types/Website.ts +36 -0
- package/src/media/types/Wikipedia.ts +124 -0
- package/src/media/types/YouTube.ts +192 -0
- package/src/scss/Typography.scss +237 -0
- package/src/scss/VCO.StoryMap.Dark.scss +138 -0
- package/src/scss/VCO.StoryMap.scss +164 -0
- package/src/scss/Variables.Dark.scss +56 -0
- package/src/scss/Variables.scss +47 -0
- package/src/scss/core/Mixins.scss +112 -0
- package/src/scss/core/Reset.scss +206 -0
- package/src/scss/fonts/_font.base.scss +113 -0
- package/src/scss/fonts/font.abril-droidsans.scss +75 -0
- package/src/scss/fonts/font.amatic-andika.scss +73 -0
- package/src/scss/fonts/font.bitter-raleway.scss +86 -0
- package/src/scss/fonts/font.clicker-garamond.scss +92 -0
- package/src/scss/fonts/font.dancing-ledger.scss +74 -0
- package/src/scss/fonts/font.default.scss +9 -0
- package/src/scss/fonts/font.fjalla-average.scss +80 -0
- package/src/scss/fonts/font.georgia-helvetica.scss +58 -0
- package/src/scss/fonts/font.knightlab.scss +97 -0
- package/src/scss/fonts/font.lustria-lato.scss +80 -0
- package/src/scss/fonts/font.medula-lato.scss +73 -0
- package/src/scss/fonts/font.oldstandard.scss +88 -0
- package/src/scss/fonts/font.opensans-gentiumbook.scss +80 -0
- package/src/scss/fonts/font.playfair-faunaone.scss +86 -0
- package/src/scss/fonts/font.playfair.scss +85 -0
- package/src/scss/fonts/font.pt.scss +90 -0
- package/src/scss/fonts/font.roboto-megrim.scss +73 -0
- package/src/scss/fonts/font.rufina-sintony.scss +73 -0
- package/src/scss/fonts/font.ubuntu.scss +78 -0
- package/src/scss/fonts/font.unicaone-vollkorn.scss +85 -0
- package/src/scss/icons/Icons.scss +239 -0
- package/src/scss/map/VCO.ImageMarker.scss +23 -0
- package/src/scss/map/VCO.Map.scss +21 -0
- package/src/scss/map/VCO.MapMarker.scss +147 -0
- package/src/scss/map/openlayers/VCO.Map.OpenLayers.scss +126 -0
- package/src/scss/media/VCO.Media.scss +222 -0
- package/src/scss/media/types/VCO.Media.Blockquote.scss +76 -0
- package/src/scss/media/types/VCO.Media.IFrame.scss +11 -0
- package/src/scss/media/types/VCO.Media.Image.scss +13 -0
- package/src/scss/media/types/VCO.Media.Instagram.scss +7 -0
- package/src/scss/media/types/VCO.Media.Profile.scss +5 -0
- package/src/scss/media/types/VCO.Media.Text.scss +90 -0
- package/src/scss/media/types/VCO.Media.Twitter.scss +87 -0
- package/src/scss/media/types/VCO.Media.Wikipedia.scss +64 -0
- package/src/scss/site/site.scss +748 -0
- package/src/scss/slider/VCO.Slide.scss +447 -0
- package/src/scss/slider/VCO.SlideNav.scss +338 -0
- package/src/scss/slider/VCO.StorySlider.scss +64 -0
- package/src/scss/snapmap/VCO.SnapMap.scss +103 -0
- package/src/scss/ui/VCO.Consent.scss +77 -0
- package/src/scss/ui/VCO.MenuBar.Button.scss +99 -0
- package/src/scss/ui/VCO.MenuBar.scss +20 -0
- package/src/scss/ui/VCO.Message.scss +104 -0
- package/src/site/site.ts +87 -0
- package/src/slider/Slide.ts +460 -0
- package/src/slider/SlideNav.ts +212 -0
- package/src/slider/StorySlider.ts +722 -0
- package/src/storymap/Consent.ts +146 -0
- package/src/storymap/StoryMap.ts +1108 -0
- package/src/storymap/iiif.ts +386 -0
- package/src/storymap/validate.ts +184 -0
- package/src/types.ts +297 -0
- package/src/ui/Draggable.ts +374 -0
- package/src/ui/MenuBar.ts +323 -0
- package/src/ui/Message.ts +118 -0
- package/src/ui/Swipable.ts +388 -0
- package/dist/assets/main-DwbXrrYn.js +0 -17
|
@@ -0,0 +1,1108 @@
|
|
|
1
|
+
import { mergeData, slideTransitionDuration, updateData } from "../core/Util";
|
|
2
|
+
import { loadCSS } from "../core/Load";
|
|
3
|
+
import { validateStorymapAndReport } from "./validate";
|
|
4
|
+
import { isPresentation3Manifest, manifestToStorymapData } from "./iiif";
|
|
5
|
+
import { ConsentManager, consentManagerOf, consentMessage } from "./Consent";
|
|
6
|
+
import Dom from "../dom/Dom";
|
|
7
|
+
import { easeInOutQuint, easeOutStrong } from "../animation/easings";
|
|
8
|
+
import { setLanguage, Language } from "../language/Language";
|
|
9
|
+
import { Evented, type EventedInstance } from "../core/mixins";
|
|
10
|
+
import OpenLayersMap from "../map/openlayers/Map.OpenLayers";
|
|
11
|
+
import MenuBar from "../ui/MenuBar";
|
|
12
|
+
import StorySlider from "../slider/StorySlider";
|
|
13
|
+
import { Browser } from "../core/Browser";
|
|
14
|
+
import Animate from "morpheus";
|
|
15
|
+
import type { Map as OlMap } from "ol";
|
|
16
|
+
import type { AnimationHandle, StorymapData, StorymapDataWrapper, StorymapOptions } from "../types";
|
|
17
|
+
|
|
18
|
+
type StoryMapListener = (e: unknown) => void;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a `font_css` value to an absolute stylesheet URL.
|
|
22
|
+
*
|
|
23
|
+
* - `stock:<name>` resolves against the library location.
|
|
24
|
+
* - Absolute URLs pass through untouched.
|
|
25
|
+
* - Anything else resolves against the page URL, so hosts can reference
|
|
26
|
+
* themes vendored into their own public dir (e.g. "fonts/font.css").
|
|
27
|
+
*/
|
|
28
|
+
export function resolveFontCssUrl(font: string): string {
|
|
29
|
+
if (font.startsWith("stock:")) {
|
|
30
|
+
const font_name = font.split(":")[1] || "default";
|
|
31
|
+
// resolved against the library location: one directory up from
|
|
32
|
+
// src/main.ts (dev) and js/storymap.js (build) in both cases
|
|
33
|
+
return new URL("../css/fonts/font." + font_name + ".css", import.meta.url).href;
|
|
34
|
+
}
|
|
35
|
+
if (!/^(http|https|\/\/)/.test(font)) {
|
|
36
|
+
return new URL(font, document.baseURI).href;
|
|
37
|
+
}
|
|
38
|
+
return font;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Interactive StoryMap viewer.
|
|
43
|
+
*
|
|
44
|
+
* Renders a storymap JSON document (or a IIIF Presentation 3 manifest) into an
|
|
45
|
+
* element: an OpenLayers map on one side and a slide slider on the other.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* import { StoryMap } from "storymapjs";
|
|
50
|
+
* // from an inline data object
|
|
51
|
+
* const map = new StoryMap("embed", { storymap: { ... } }, { start_at_slide: 2 });
|
|
52
|
+
* // or straight from a source file (storymap JSON or IIIF manifest)
|
|
53
|
+
* const map2 = new StoryMap(document.getElementById("embed"), "./my-storymap.json");
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
class StoryMapBase {
|
|
57
|
+
declare "_loaded": { storyslider: boolean; map: boolean };
|
|
58
|
+
declare "on": EventedInstance["on"];
|
|
59
|
+
declare "version": string;
|
|
60
|
+
declare "ready": boolean;
|
|
61
|
+
declare "_el": {
|
|
62
|
+
container: HTMLElement;
|
|
63
|
+
menubar: HTMLElement;
|
|
64
|
+
map: HTMLElement;
|
|
65
|
+
storyslider: HTMLElement;
|
|
66
|
+
};
|
|
67
|
+
declare "_storyslider": StorySlider;
|
|
68
|
+
declare "_map": OpenLayersMap;
|
|
69
|
+
declare "map": OlMap;
|
|
70
|
+
declare "_menubar": MenuBar;
|
|
71
|
+
declare "data": StorymapData;
|
|
72
|
+
declare "options": StorymapOptions;
|
|
73
|
+
declare "current_slide": number;
|
|
74
|
+
declare "animator_map": AnimationHandle | null;
|
|
75
|
+
declare "animator_storyslider": AnimationHandle | null;
|
|
76
|
+
declare "_autoplay_timer": ReturnType<typeof setTimeout> | null;
|
|
77
|
+
declare "_transition_timer": ReturnType<typeof setTimeout> | null;
|
|
78
|
+
declare "_autoplay_stopped": boolean;
|
|
79
|
+
declare "_hash_initialized": boolean;
|
|
80
|
+
/** the data source was a IIIF Presentation manifest (legacy zoomify options are ignored) */
|
|
81
|
+
declare "_data_from_manifest": boolean;
|
|
82
|
+
declare "_resize_observer": ResizeObserver | null;
|
|
83
|
+
declare "_resize_timer": ReturnType<typeof setTimeout> | null;
|
|
84
|
+
declare "fire": EventedInstance["fire"];
|
|
85
|
+
declare "hasEventListeners": EventedInstance["hasEventListeners"];
|
|
86
|
+
|
|
87
|
+
// TODO: mixin
|
|
88
|
+
// includes: VCO.Events,
|
|
89
|
+
|
|
90
|
+
/* Private Methods
|
|
91
|
+
================================================== */
|
|
92
|
+
/**
|
|
93
|
+
* Create a StoryMap inside the given element.
|
|
94
|
+
*
|
|
95
|
+
* @param elem - The container element, or its DOM id.
|
|
96
|
+
* @param data - Either an inline storymap document (`{ storymap: ... }`),
|
|
97
|
+
* an inline IIIF Presentation 3 manifest, or the URL of a source file
|
|
98
|
+
* (storymap JSON or IIIF manifest) that is fetched automatically.
|
|
99
|
+
* @param options - Optional {@link StorymapOptions} overrides (map type,
|
|
100
|
+
* lines, fonts, animation, raw OpenLayers options via `map_options`...).
|
|
101
|
+
* @param listeners - Optional event listeners keyed by event name
|
|
102
|
+
* (`change`, `title`, `loaded`, ...) attached on creation.
|
|
103
|
+
*/
|
|
104
|
+
constructor(
|
|
105
|
+
elem: string | HTMLElement,
|
|
106
|
+
data: string | StorymapDataWrapper | Record<string, unknown>,
|
|
107
|
+
options?: Partial<StorymapOptions>,
|
|
108
|
+
listeners?: Record<string, StoryMapListener | StoryMapListener[]>,
|
|
109
|
+
) {
|
|
110
|
+
for (const key in listeners) {
|
|
111
|
+
const callbacks = listeners[key];
|
|
112
|
+
if (typeof callbacks == "function") {
|
|
113
|
+
this.on(key, callbacks);
|
|
114
|
+
} else {
|
|
115
|
+
for (const idx in callbacks) {
|
|
116
|
+
if (typeof callbacks[idx] == "function") {
|
|
117
|
+
this.on(key, callbacks[idx]);
|
|
118
|
+
} else {
|
|
119
|
+
console.log(
|
|
120
|
+
"WARNING: Ignoring invalid callback '" +
|
|
121
|
+
callbacks[idx] +
|
|
122
|
+
"' defined for " +
|
|
123
|
+
"listener '" +
|
|
124
|
+
key +
|
|
125
|
+
"' in StoryMap constructor",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Version
|
|
133
|
+
this.version = "0.1.16";
|
|
134
|
+
|
|
135
|
+
// Ready
|
|
136
|
+
this.ready = false;
|
|
137
|
+
|
|
138
|
+
// DOM ELEMENTS
|
|
139
|
+
this._el = {
|
|
140
|
+
container: {} as HTMLElement,
|
|
141
|
+
storyslider: {} as HTMLElement,
|
|
142
|
+
map: {} as HTMLElement,
|
|
143
|
+
menubar: {} as HTMLElement,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Determine Container Element
|
|
147
|
+
if (typeof elem === "object") {
|
|
148
|
+
this._el.container = elem;
|
|
149
|
+
} else {
|
|
150
|
+
this._el.container = Dom.get(elem);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Slider
|
|
154
|
+
this._storyslider = {} as StorySlider;
|
|
155
|
+
|
|
156
|
+
// Map
|
|
157
|
+
this._map = {} as OpenLayersMap;
|
|
158
|
+
this.map = {} as OlMap; // direct access to the OpenLayers map
|
|
159
|
+
|
|
160
|
+
// Menu Bar
|
|
161
|
+
this._menubar = {} as MenuBar;
|
|
162
|
+
|
|
163
|
+
// Loaded State
|
|
164
|
+
this._loaded = { storyslider: false, map: false };
|
|
165
|
+
|
|
166
|
+
// Data Object
|
|
167
|
+
// Test Data compiled from http://www.pbs.org/marktwain/learnmore/chronology.html
|
|
168
|
+
this.data = {} as StorymapData;
|
|
169
|
+
|
|
170
|
+
this.options = {
|
|
171
|
+
script_path: StoryMap.SCRIPT_PATH,
|
|
172
|
+
height: this._el.container.offsetHeight,
|
|
173
|
+
width: this._el.container.offsetWidth,
|
|
174
|
+
layout: "landscape", // portrait or landscape
|
|
175
|
+
base_class: "",
|
|
176
|
+
default_bg_color: { r: 255, g: 255, b: 255 },
|
|
177
|
+
map_size_sticky: 2.5, // Set as division 1/3 etc
|
|
178
|
+
map_center_offset: null, // takes object {top:0,left:0}
|
|
179
|
+
less_bounce: false, // Less map bounce when calculating zoom, false is good when there are clusters of tightly grouped markers
|
|
180
|
+
start_at_slide: 0,
|
|
181
|
+
call_to_action: false,
|
|
182
|
+
call_to_action_text: "",
|
|
183
|
+
menubar_height: 0,
|
|
184
|
+
fullscreen: true,
|
|
185
|
+
show_overview: true,
|
|
186
|
+
show_back_to_start: true,
|
|
187
|
+
skinny_size: 650,
|
|
188
|
+
// animation
|
|
189
|
+
duration: 1000,
|
|
190
|
+
ease: easeInOutQuint,
|
|
191
|
+
// interaction
|
|
192
|
+
dragging: true,
|
|
193
|
+
trackResize: true,
|
|
194
|
+
keyboard: false,
|
|
195
|
+
nocache: false,
|
|
196
|
+
autoplay: 0,
|
|
197
|
+
show_progress: false,
|
|
198
|
+
marker_labels: false,
|
|
199
|
+
text_align: "left",
|
|
200
|
+
map_overview_center: null,
|
|
201
|
+
map_type: "", // "osm:standard",
|
|
202
|
+
attribution: "",
|
|
203
|
+
map_mini: true,
|
|
204
|
+
map_subdomains: "",
|
|
205
|
+
map_as_image: false,
|
|
206
|
+
// no bundled credentials: pass map_access_token in the options
|
|
207
|
+
// if you use Mapbox/Stadia tiles, api_key_flickr for flickr API
|
|
208
|
+
// URLs
|
|
209
|
+
map_access_token: "",
|
|
210
|
+
map_background_color: "#d9d9d9",
|
|
211
|
+
map_area: "full", // "left" limits the map to the visible half (landscape)
|
|
212
|
+
map_bbox: null,
|
|
213
|
+
consent_required: false,
|
|
214
|
+
zoomify: undefined,
|
|
215
|
+
text_color: "",
|
|
216
|
+
text_background_color: "",
|
|
217
|
+
show_distance: false,
|
|
218
|
+
use_custom_markers: false,
|
|
219
|
+
iiif: {
|
|
220
|
+
url: "",
|
|
221
|
+
attribution: "",
|
|
222
|
+
},
|
|
223
|
+
map_height: 300,
|
|
224
|
+
storyslider_height: 600,
|
|
225
|
+
slide_padding_lr: 45, // padding on slide of slide
|
|
226
|
+
slide_default_fade: "0%", // landscape fade
|
|
227
|
+
menubar_default_y: 0,
|
|
228
|
+
path_gfx: "gfx",
|
|
229
|
+
map_popup: false,
|
|
230
|
+
zoom_distance: 100,
|
|
231
|
+
calculate_zoom: true, // Allow map to determine best zoom level between markers (recommended)
|
|
232
|
+
line_follows_path: true, // Map history path follows default line, if false it will connect previous and current only
|
|
233
|
+
line_color: "#c34528", //"#DA0000",
|
|
234
|
+
line_color_inactive: "#CCC",
|
|
235
|
+
line_join: "miter",
|
|
236
|
+
line_weight: 3,
|
|
237
|
+
line_opacity: 0.8,
|
|
238
|
+
line_dash: "5,5",
|
|
239
|
+
show_lines: true,
|
|
240
|
+
show_history_line: true,
|
|
241
|
+
api_key_flickr: "", // no bundled key: pass api_key_flickr in the options
|
|
242
|
+
font_css: "stock:default",
|
|
243
|
+
language: "en",
|
|
244
|
+
} as StorymapOptions;
|
|
245
|
+
|
|
246
|
+
// Animation Objects
|
|
247
|
+
this.animator_map = null;
|
|
248
|
+
this.animator_storyslider = null;
|
|
249
|
+
this._resize_observer = null;
|
|
250
|
+
this._resize_timer = null;
|
|
251
|
+
this._autoplay_timer = null;
|
|
252
|
+
this._transition_timer = null;
|
|
253
|
+
this._autoplay_stopped = false;
|
|
254
|
+
this._hash_initialized = false;
|
|
255
|
+
|
|
256
|
+
// Merge Options -- legacy, in case people still need to pass in
|
|
257
|
+
mergeData(this.options, options);
|
|
258
|
+
|
|
259
|
+
// Current Slide (after the options merge so start_at_slide applies)
|
|
260
|
+
this.current_slide = this.options.start_at_slide;
|
|
261
|
+
|
|
262
|
+
this._initData(data);
|
|
263
|
+
|
|
264
|
+
return this;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* Initialize the data
|
|
268
|
+
================================================== */
|
|
269
|
+
_initData(data: string | StorymapDataWrapper | Record<string, unknown>) {
|
|
270
|
+
// legacy zoomify options are only honored for storymap JSON sources —
|
|
271
|
+
// a IIIF Presentation manifest cannot carry them
|
|
272
|
+
this._data_from_manifest = false;
|
|
273
|
+
if (typeof data === "string") {
|
|
274
|
+
// issue #417: optional cache-busting re-fetch of the source file
|
|
275
|
+
const url =
|
|
276
|
+
this.options.nocache === true
|
|
277
|
+
? data + (data.includes("?") ? "&" : "?") + "_=" + Date.now()
|
|
278
|
+
: data;
|
|
279
|
+
void this._loadDataFromUrl(url, data);
|
|
280
|
+
} else if (typeof data === "object") {
|
|
281
|
+
if (isPresentation3Manifest(data)) {
|
|
282
|
+
this._data_from_manifest = true;
|
|
283
|
+
this.data = manifestToStorymapData(data);
|
|
284
|
+
} else {
|
|
285
|
+
const wrapper = data as StorymapDataWrapper;
|
|
286
|
+
validateStorymapAndReport(wrapper);
|
|
287
|
+
if (wrapper.storymap) {
|
|
288
|
+
this.data = wrapper.storymap;
|
|
289
|
+
} else {
|
|
290
|
+
console.error("StoryMapJS: data must have a storymap property");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
this._initOptions();
|
|
294
|
+
} else {
|
|
295
|
+
console.error("StoryMapJS: data has unknown type");
|
|
296
|
+
this._initOptions();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* Load storymap data from a URL
|
|
301
|
+
================================================== */
|
|
302
|
+
async _loadDataFromUrl(url: string, source: string) {
|
|
303
|
+
try {
|
|
304
|
+
const response = await fetch(url);
|
|
305
|
+
if (!response.ok) {
|
|
306
|
+
throw new Error("HTTP " + response.status + " " + response.statusText);
|
|
307
|
+
}
|
|
308
|
+
const result: unknown = await response.json();
|
|
309
|
+
if (isPresentation3Manifest(result)) {
|
|
310
|
+
this._data_from_manifest = true;
|
|
311
|
+
this.data = manifestToStorymapData(result);
|
|
312
|
+
} else {
|
|
313
|
+
validateStorymapAndReport(result, source);
|
|
314
|
+
const wrapper = result as StorymapDataWrapper;
|
|
315
|
+
if (!wrapper.storymap) {
|
|
316
|
+
throw new Error("StoryMapJS: data must have a storymap property");
|
|
317
|
+
}
|
|
318
|
+
this.data = wrapper.storymap;
|
|
319
|
+
}
|
|
320
|
+
this._initOptions();
|
|
321
|
+
} catch (err: unknown) {
|
|
322
|
+
console.error("StoryMapJS: could not load storymap data from " + source, err);
|
|
323
|
+
this.fire("error", { message: String(err), source });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* Initialize the options
|
|
328
|
+
================================================== */
|
|
329
|
+
_initOptions() {
|
|
330
|
+
// Grab options from storymap data
|
|
331
|
+
updateData(this.options, this.data);
|
|
332
|
+
|
|
333
|
+
// legacy zoomify options only work with storymap JSON sources — a
|
|
334
|
+
// IIIF Presentation manifest cannot carry them
|
|
335
|
+
if (this._data_from_manifest) {
|
|
336
|
+
delete this.options.zoomify;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (this.options.layout === "landscape") {
|
|
340
|
+
// map_area "left": the map is limited to the visible half with an
|
|
341
|
+
// opaque slide panel — no offset needed (the view center is the
|
|
342
|
+
// visible center already); "full" (default) offsets the view so
|
|
343
|
+
// markers clear the panel that fades in over the map
|
|
344
|
+
if (this.options.map_area !== "left") {
|
|
345
|
+
this.options.map_center_offset = { left: -200, top: 0 };
|
|
346
|
+
} else {
|
|
347
|
+
this.options.map_center_offset = { left: 0, top: 0 };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (this.options.map_type === "iiif" && this.options.map_as_image) {
|
|
351
|
+
this.options.map_size_sticky = 2;
|
|
352
|
+
}
|
|
353
|
+
// legacy zoomify stories are image maps too — the map window is half
|
|
354
|
+
// the height (the original renderer's layout)
|
|
355
|
+
if (this.options.map_type === "zoomify") {
|
|
356
|
+
this.options.map_size_sticky = 2;
|
|
357
|
+
}
|
|
358
|
+
if (this.options.map_as_image) {
|
|
359
|
+
this.options.calculate_zoom = false;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// legacy zoomify support: rendered by the map via the image pyramid
|
|
363
|
+
// (options.zoomify) — deprecated, a JS warning points at iiif
|
|
364
|
+
if (this.options.map_type === "zoomify") {
|
|
365
|
+
console.warn(
|
|
366
|
+
"StoryMapJS: map_type 'zoomify' is a legacy image-pyramid basemap; consider map_type 'iiif' with options.iiif.url instead.",
|
|
367
|
+
);
|
|
368
|
+
const zoomify_opts = this.options.zoomify;
|
|
369
|
+
if (typeof zoomify_opts !== "object" || !zoomify_opts.path) {
|
|
370
|
+
console.error(
|
|
371
|
+
"StoryMapJS: map_type 'zoomify' needs a zoomify image pyramid (path, width, height) in the storymap data.",
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// handle Stamen change
|
|
377
|
+
if (this.options.map_type.startsWith("stamen")) {
|
|
378
|
+
const old_type = this.options.map_type;
|
|
379
|
+
if (old_type === "stamen:watercolor") {
|
|
380
|
+
this.options.map_type = "ch-watercolor";
|
|
381
|
+
} else {
|
|
382
|
+
this.options.map_type = "osm:standard";
|
|
383
|
+
}
|
|
384
|
+
console.log(`Deprecated map_type ${old_type}; using ${this.options.map_type}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
this._loadLanguage();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* Load Language
|
|
391
|
+
================================================== */
|
|
392
|
+
|
|
393
|
+
_loadLanguage() {
|
|
394
|
+
setLanguage(this.options.language);
|
|
395
|
+
// the resolved locale decides the layout direction (issues #211, #245)
|
|
396
|
+
this.options.language = Language as unknown as string;
|
|
397
|
+
this._onDataLoaded();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/* Switch the UI language at runtime, without a page reload.
|
|
401
|
+
================================================== */
|
|
402
|
+
|
|
403
|
+
refreshLanguage(code: string): void {
|
|
404
|
+
this.options.language = code;
|
|
405
|
+
setLanguage(code);
|
|
406
|
+
// the resolved locale decides the layout direction (issues #211, #245)
|
|
407
|
+
this.options.language = Language as unknown as string;
|
|
408
|
+
if (this._menubar && typeof this._menubar.refreshLabels === "function") {
|
|
409
|
+
this._menubar.refreshLabels();
|
|
410
|
+
}
|
|
411
|
+
this.updateDisplay();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/* Load the font theme stylesheet
|
|
415
|
+
================================================== */
|
|
416
|
+
async _loadFontCss() {
|
|
417
|
+
// only genuinely external URLs ask for consent: stock: themes and
|
|
418
|
+
// relative paths resolve to same-origin / library assets
|
|
419
|
+
const original = this.options.font_css || "stock:default";
|
|
420
|
+
const font = resolveFontCssUrl(original);
|
|
421
|
+
const manager = consentManagerOf(this.options);
|
|
422
|
+
const external = /^(http|https|\/\/)/.test(original);
|
|
423
|
+
if (external && this.options.consent_required && manager) {
|
|
424
|
+
// external font CSS is an external service — ask first
|
|
425
|
+
const host = new URL(font.startsWith("//") ? "https:" + font : font).host;
|
|
426
|
+
const container = this._el.map ?? (this._el.container as HTMLElement);
|
|
427
|
+
const allowed = await manager.request(
|
|
428
|
+
consentMessage("consent_service_fonts", "web fonts"),
|
|
429
|
+
host,
|
|
430
|
+
container,
|
|
431
|
+
);
|
|
432
|
+
if (!allowed) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (font) {
|
|
437
|
+
try {
|
|
438
|
+
await loadCSS(font);
|
|
439
|
+
} catch {
|
|
440
|
+
// a missing font theme must not block the storymap; the
|
|
441
|
+
// font-loaded event fires either way, as before
|
|
442
|
+
} finally {
|
|
443
|
+
this._onFontLoaded(font);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
_onFontLoaded(font: string) {
|
|
449
|
+
this.fire("fontLoaded", { font: font });
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/* Navigation
|
|
453
|
+
================================================== */
|
|
454
|
+
/**
|
|
455
|
+
* Navigate to a slide by index.
|
|
456
|
+
*
|
|
457
|
+
* @param n - Zero-based slide index; the map animates to the slide's
|
|
458
|
+
* marker (or the overview when `n` is 0 for storymaps with one).
|
|
459
|
+
*/
|
|
460
|
+
goTo(n: number) {
|
|
461
|
+
// out-of-range indices are ignored: they would desync the slider and
|
|
462
|
+
// map (no active slide) and write a broken #slide-N bookmark
|
|
463
|
+
if (n >= 0 && n < (this.data?.slides?.length ?? 0) && n !== this.current_slide) {
|
|
464
|
+
const duration = slideTransitionDuration(this.current_slide, n);
|
|
465
|
+
this.current_slide = n;
|
|
466
|
+
this._storyslider.goTo(this.current_slide);
|
|
467
|
+
this._map.goTo(this.current_slide);
|
|
468
|
+
this._beginTransition(duration);
|
|
469
|
+
// programmatic navigation reports outward like interaction does;
|
|
470
|
+
// the bubbled slider/map change events below are dropped by
|
|
471
|
+
// their equality guards, so this fires exactly once
|
|
472
|
+
this.fire("change", { current_slide: this.current_slide }, this);
|
|
473
|
+
this._syncHash();
|
|
474
|
+
this._scheduleAutoplay();
|
|
475
|
+
this._updateProgress();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Announce a slide transition: `transitionstart` now (with the glide
|
|
481
|
+
* duration both animations were started with) and `transitionend` once
|
|
482
|
+
* it elapses. Restarts on every navigation, so only the latest
|
|
483
|
+
* transition ever ends.
|
|
484
|
+
*/
|
|
485
|
+
_beginTransition(duration: number): void {
|
|
486
|
+
if (this._transition_timer) {
|
|
487
|
+
clearTimeout(this._transition_timer);
|
|
488
|
+
}
|
|
489
|
+
this.fire("transitionstart", { current_slide: this.current_slide, duration }, this);
|
|
490
|
+
this._transition_timer = setTimeout(() => {
|
|
491
|
+
this._transition_timer = null;
|
|
492
|
+
this.fire("transitionend", { current_slide: this.current_slide }, this);
|
|
493
|
+
}, duration);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Re-measure the container and re-layout the map, slider and menubar.
|
|
498
|
+
* Called automatically on resize (see the `trackResize` option).
|
|
499
|
+
*/
|
|
500
|
+
updateDisplay() {
|
|
501
|
+
if (this.ready) {
|
|
502
|
+
this._updateDisplay();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Change a single map option at runtime and apply its effect immediately.
|
|
508
|
+
*
|
|
509
|
+
* Runtime-changeable options: `map_type` (rebuilds the main + minimap tile
|
|
510
|
+
* layers, keeping the overview fitted to the marker bounds), `overlays`
|
|
511
|
+
* (rebuilds the stacked overlay layers), `show_lines`, `line_color`,
|
|
512
|
+
* `line_color_inactive`, `line_weight`, `line_opacity`, `line_dash`,
|
|
513
|
+
* `line_join`, `line_follows_path`, `show_history_line` (restyled
|
|
514
|
+
* instantly), `map_center_offset` (applied on the next navigation),
|
|
515
|
+
* `duration`, `ease`, `calculate_zoom`, `map_background_color`. All other
|
|
516
|
+
* options only take effect on the next navigation or require re-creating
|
|
517
|
+
* the StoryMap.
|
|
518
|
+
*/
|
|
519
|
+
setMapOption(name: string, value: unknown) {
|
|
520
|
+
this.setMapOptions({ [name]: value } as Partial<StorymapOptions>);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Show or hide a stacked overlay by index (see the `overlays` option).
|
|
525
|
+
*/
|
|
526
|
+
setOverlayVisible(index: number, visible: boolean): void {
|
|
527
|
+
this._map.setOverlayVisible(index, visible);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Set a stacked overlay's opacity by index (see the `overlays` option).
|
|
532
|
+
*/
|
|
533
|
+
setOverlayOpacity(index: number, opacity: number): void {
|
|
534
|
+
this._map.setOverlayOpacity(index, opacity);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Change several map options at runtime (see setMapOption).
|
|
539
|
+
*/
|
|
540
|
+
setMapOptions(options: Partial<StorymapOptions>) {
|
|
541
|
+
mergeData(this.options, options);
|
|
542
|
+
if (this._map && this._map.options) {
|
|
543
|
+
mergeData(this._map.options, options);
|
|
544
|
+
this._map.applyOptions(Object.keys(options));
|
|
545
|
+
} else {
|
|
546
|
+
for (const key of Object.keys(options)) {
|
|
547
|
+
(this.options as Record<string, unknown>)[key] = (
|
|
548
|
+
options as Record<string, unknown>
|
|
549
|
+
)[key];
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (this.ready) {
|
|
553
|
+
// text color theming follows runtime option changes (issue #177)
|
|
554
|
+
if (this.options.text_color) {
|
|
555
|
+
this._el.container.style.setProperty("--vco-color-text", this.options.text_color);
|
|
556
|
+
}
|
|
557
|
+
if (this.options.text_background_color) {
|
|
558
|
+
this._el.container.style.setProperty(
|
|
559
|
+
"--vco-color-text-background",
|
|
560
|
+
this.options.text_background_color,
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
this.updateDisplay();
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/* Private Methods
|
|
568
|
+
================================================== */
|
|
569
|
+
|
|
570
|
+
// Initialize the layout
|
|
571
|
+
_initLayout() {
|
|
572
|
+
this._el.container.className += " vco-storymap";
|
|
573
|
+
this.options.base_class = this._el.container.className;
|
|
574
|
+
|
|
575
|
+
// Text color theming (issue #177): expose the text colors as CSS
|
|
576
|
+
// custom properties consumed by the slide typography
|
|
577
|
+
if (this.options.text_color) {
|
|
578
|
+
this._el.container.style.setProperty("--vco-color-text", this.options.text_color);
|
|
579
|
+
}
|
|
580
|
+
if (this.options.text_background_color) {
|
|
581
|
+
this._el.container.style.setProperty(
|
|
582
|
+
"--vco-color-text-background",
|
|
583
|
+
this.options.text_background_color,
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Create Layout
|
|
588
|
+
this._el.menubar = Dom.create("div", "vco-menubar", this._el.container);
|
|
589
|
+
this._el.map = this._resolveMapElement();
|
|
590
|
+
if (!this._el.map) {
|
|
591
|
+
this._el.map = Dom.create("div", "vco-map", this._el.container);
|
|
592
|
+
}
|
|
593
|
+
this._el.storyslider = Dom.create("div", "vco-storyslider", this._el.container);
|
|
594
|
+
|
|
595
|
+
// Initial Default Layout
|
|
596
|
+
this.options.width = this._el.container.offsetWidth;
|
|
597
|
+
this.options.height = this._el.container.offsetHeight;
|
|
598
|
+
this._el.map.style.height = "1px";
|
|
599
|
+
this._el.storyslider.style.top = "1px";
|
|
600
|
+
|
|
601
|
+
// Create Map using preferred Map API
|
|
602
|
+
this._map = new OpenLayersMap(this._el.map, this.data, this.options);
|
|
603
|
+
this.map = this._map._map; // For access to the OpenLayers map.
|
|
604
|
+
this._map.on("loaded", this._onMapLoaded, this);
|
|
605
|
+
|
|
606
|
+
// Map Background Color
|
|
607
|
+
this._el.map.style.backgroundColor = this.options.map_background_color;
|
|
608
|
+
|
|
609
|
+
// Create Menu Bar
|
|
610
|
+
this._menubar = new MenuBar(this._el.menubar, this._el.container, this.options);
|
|
611
|
+
|
|
612
|
+
// Create StorySlider
|
|
613
|
+
this._storyslider = new StorySlider(this._el.storyslider, this.data, this.options);
|
|
614
|
+
this._storyslider.on("loaded", this._onStorySliderLoaded, this);
|
|
615
|
+
this._storyslider.on("title", this._onTitle, this);
|
|
616
|
+
this._storyslider.init();
|
|
617
|
+
|
|
618
|
+
// LAYOUT
|
|
619
|
+
if (this.options.layout === "portrait") {
|
|
620
|
+
// Set Default Component Sizes
|
|
621
|
+
this.options.map_height = this.options.height / this.options.map_size_sticky;
|
|
622
|
+
this.options.storyslider_height =
|
|
623
|
+
this.options.height - this._el.menubar.offsetHeight - this.options.map_height - 1;
|
|
624
|
+
this._menubar.setSticky(0);
|
|
625
|
+
} else {
|
|
626
|
+
this.options.menubar_height = this._el.menubar.offsetHeight;
|
|
627
|
+
// Set Default Component Sizes
|
|
628
|
+
this.options.map_height = this.options.height;
|
|
629
|
+
this.options.storyslider_height =
|
|
630
|
+
this.options.height - this._el.menubar.offsetHeight - 1;
|
|
631
|
+
this._menubar.setSticky(this.options.menubar_height);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Update Display
|
|
635
|
+
this._updateDisplay(this.options.map_height, true, 2000);
|
|
636
|
+
|
|
637
|
+
// Animate Menu Bar to Default Location
|
|
638
|
+
this._menubar.show(2000);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
_initEvents() {
|
|
642
|
+
// Sidebar Events
|
|
643
|
+
this._menubar.on("collapse", this._onMenuBarCollapse, this);
|
|
644
|
+
this._menubar.on("back_to_start", this._onBackToStart, this);
|
|
645
|
+
this._menubar.on("overview", this._onOverview, this);
|
|
646
|
+
this._menubar.on("fullscreen", this._onFullscreenToggle, this);
|
|
647
|
+
|
|
648
|
+
// StorySlider Events
|
|
649
|
+
this._storyslider.on("change", this._onSlideChange, this);
|
|
650
|
+
this._storyslider.on("colorchange", this._onColorChange, this);
|
|
651
|
+
|
|
652
|
+
// Map Events
|
|
653
|
+
this._map.on("change", this._onMapChange, this);
|
|
654
|
+
|
|
655
|
+
// Global slide navigation (opt-in): the slider only listens on its
|
|
656
|
+
// own panel, which needs focus
|
|
657
|
+
if (this.options.keyboard) {
|
|
658
|
+
window.addEventListener("keydown", this._onKeyDownGlobal.bind(this));
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Fullscreen state
|
|
662
|
+
document.addEventListener("fullscreenchange", this._onFullscreenChange.bind(this));
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
_onKeyDownGlobal(e: KeyboardEvent) {
|
|
666
|
+
if (e.defaultPrevented) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const target = e.target as HTMLElement | null;
|
|
670
|
+
const tag = target?.tagName?.toLowerCase();
|
|
671
|
+
if (tag === "input" || tag === "textarea" || tag === "select" || target?.isContentEditable) {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
// let OpenLayers keep arrow-key map panning when the map has focus
|
|
675
|
+
if (target instanceof Element && target.closest(".vco-map")) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
|
|
679
|
+
e.preventDefault();
|
|
680
|
+
this._storyslider.next();
|
|
681
|
+
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
|
|
682
|
+
e.preventDefault();
|
|
683
|
+
this._storyslider.previous();
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Update View
|
|
688
|
+
_updateDisplay(map_height?: number, animate?: boolean, d?: number) {
|
|
689
|
+
let duration = this.options.duration,
|
|
690
|
+
display_class = this.options.base_class;
|
|
691
|
+
|
|
692
|
+
if (d) {
|
|
693
|
+
duration = d;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Update width and height
|
|
697
|
+
this.options.width = this._el.container.offsetWidth;
|
|
698
|
+
this.options.height = this._el.container.offsetHeight;
|
|
699
|
+
|
|
700
|
+
// Check if skinny
|
|
701
|
+
if (this.options.width <= this.options.skinny_size) {
|
|
702
|
+
this.options.layout = "portrait";
|
|
703
|
+
} else {
|
|
704
|
+
this.options.layout = "landscape";
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Map Height
|
|
708
|
+
if (map_height) {
|
|
709
|
+
this.options.map_height = map_height;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Detect Mobile and Update Orientation on Touch devices
|
|
713
|
+
if (Browser.touch) {
|
|
714
|
+
this.options.layout = Browser.orientation();
|
|
715
|
+
display_class += " vco-mobile";
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// LAYOUT
|
|
719
|
+
if (this.options.layout === "portrait") {
|
|
720
|
+
display_class += " vco-skinny";
|
|
721
|
+
// Map Offset
|
|
722
|
+
this._map.setMapOffset(0, 0);
|
|
723
|
+
|
|
724
|
+
this.options.map_height = this.options.height / this.options.map_size_sticky;
|
|
725
|
+
this.options.storyslider_height = this.options.height - this.options.map_height - 1;
|
|
726
|
+
this._menubar.setSticky(0);
|
|
727
|
+
|
|
728
|
+
// Portrait: the map spans the full width again (a landscape
|
|
729
|
+
// map_area "left" pass narrowed it)
|
|
730
|
+
display_class += " vco-layout-portrait";
|
|
731
|
+
this._el.map.style.width = "100%";
|
|
732
|
+
|
|
733
|
+
if (animate) {
|
|
734
|
+
// Animate Map
|
|
735
|
+
if (this.animator_map) {
|
|
736
|
+
this.animator_map.stop();
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
this.animator_map = Animate(this._el.map, {
|
|
740
|
+
height: this.options.map_height + "px",
|
|
741
|
+
duration: duration,
|
|
742
|
+
easing: easeOutStrong,
|
|
743
|
+
complete: () => {
|
|
744
|
+
this._map.updateDisplay(
|
|
745
|
+
this.options.width,
|
|
746
|
+
this.options.map_height,
|
|
747
|
+
animate,
|
|
748
|
+
d,
|
|
749
|
+
this.options.menubar_height,
|
|
750
|
+
);
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
// Animate StorySlider
|
|
755
|
+
if (this.animator_storyslider) {
|
|
756
|
+
this.animator_storyslider.stop();
|
|
757
|
+
}
|
|
758
|
+
this.animator_storyslider = Animate(this._el.storyslider, {
|
|
759
|
+
height: this.options.storyslider_height + "px",
|
|
760
|
+
duration: duration,
|
|
761
|
+
easing: easeOutStrong,
|
|
762
|
+
});
|
|
763
|
+
} else {
|
|
764
|
+
// Map
|
|
765
|
+
this._el.map.style.height = Math.ceil(this.options.map_height) + "px";
|
|
766
|
+
|
|
767
|
+
// StorySlider
|
|
768
|
+
this._el.storyslider.style.height = this.options.storyslider_height + "px";
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Update Component Displays
|
|
772
|
+
this._menubar.updateDisplay(this.options.width, this.options.height, animate);
|
|
773
|
+
this._map.updateDisplay(this.options.width, this.options.height, false);
|
|
774
|
+
this._storyslider.updateDisplay(
|
|
775
|
+
this.options.width,
|
|
776
|
+
this.options.storyslider_height,
|
|
777
|
+
animate,
|
|
778
|
+
this.options.layout,
|
|
779
|
+
);
|
|
780
|
+
} else {
|
|
781
|
+
// Landscape
|
|
782
|
+
display_class += " vco-layout-landscape";
|
|
783
|
+
|
|
784
|
+
this.options.menubar_height = this._el.menubar.offsetHeight;
|
|
785
|
+
|
|
786
|
+
// Set Default Component Sizes
|
|
787
|
+
this.options.map_height = this.options.height;
|
|
788
|
+
this.options.storyslider_height = this.options.height;
|
|
789
|
+
this._menubar.setSticky(this.options.menubar_height);
|
|
790
|
+
|
|
791
|
+
// Set Sticky state of MenuBar
|
|
792
|
+
this._menubar.setSticky(this.options.menubar_height);
|
|
793
|
+
|
|
794
|
+
this._el.map.style.height = this.options.height + "px";
|
|
795
|
+
|
|
796
|
+
// map_area "left": the map element is limited to the left, visible
|
|
797
|
+
// half (the slide panel is opaque) — no view offset needed;
|
|
798
|
+
// "full" (default): the map spans the whole width behind the
|
|
799
|
+
// fading slide panel and the view is offset by a quarter width
|
|
800
|
+
if (this.options.map_area === "left") {
|
|
801
|
+
display_class += " vco-map-area-left";
|
|
802
|
+
this._el.map.style.width = Math.floor(this.options.width / 2) + "px";
|
|
803
|
+
this._map.setMapOffset(0, 0);
|
|
804
|
+
} else {
|
|
805
|
+
this._el.map.style.width = "100%";
|
|
806
|
+
this._map.setMapOffset(-(this.options.width / 4), 0);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// StorySlider
|
|
810
|
+
this._el.storyslider.style.top = "0";
|
|
811
|
+
this._el.storyslider.style.height = this.options.storyslider_height + "px";
|
|
812
|
+
|
|
813
|
+
this._menubar.updateDisplay(this.options.width, this.options.height, animate);
|
|
814
|
+
this._map.updateDisplay(this.options.width, this.options.height, animate, d);
|
|
815
|
+
this._storyslider.updateDisplay(
|
|
816
|
+
this.options.width / 2,
|
|
817
|
+
this.options.storyslider_height,
|
|
818
|
+
animate,
|
|
819
|
+
this.options.layout,
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if ((this.options.language as unknown as { direction?: string }).direction === "rtl") {
|
|
824
|
+
display_class += " vco-rtl";
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Apply class
|
|
828
|
+
this._el.container.className = display_class;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/* Events
|
|
832
|
+
================================================== */
|
|
833
|
+
|
|
834
|
+
_onDataLoaded(e?: unknown) {
|
|
835
|
+
// attach the consent manager BEFORE the layout is created, so the
|
|
836
|
+
// slider/map/media options copies all share it
|
|
837
|
+
(this.options as Record<string, unknown>).consent_manager = new ConsentManager();
|
|
838
|
+
this.fire("dataloaded");
|
|
839
|
+
this._initLayout();
|
|
840
|
+
// font themes load once the layout exists: the consent ask for
|
|
841
|
+
// external font CSS needs a real container to render into
|
|
842
|
+
void this._loadFontCss().catch((err: unknown) => {
|
|
843
|
+
console.warn("StoryMapJS: font theme could not be loaded", err);
|
|
844
|
+
});
|
|
845
|
+
this._initEvents();
|
|
846
|
+
this._initResizeHandling();
|
|
847
|
+
this.ready = true;
|
|
848
|
+
this._startAutoplay();
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/* Autoplay (issue #380) and hash bookmarks (issue #146)
|
|
852
|
+
================================================== */
|
|
853
|
+
_startAutoplay() {
|
|
854
|
+
this._stopAutoplay();
|
|
855
|
+
this._autoplay_stopped = false;
|
|
856
|
+
if (this.options.autoplay > 0) {
|
|
857
|
+
// any user interaction stops autoplay permanently
|
|
858
|
+
const stop = () => {
|
|
859
|
+
this._autoplay_stopped = true;
|
|
860
|
+
this._stopAutoplay();
|
|
861
|
+
};
|
|
862
|
+
this._el.container.addEventListener("pointerdown", stop, { once: true });
|
|
863
|
+
this._el.container.addEventListener("keydown", stop, { once: true });
|
|
864
|
+
this._el.container.addEventListener("touchstart", stop, { once: true });
|
|
865
|
+
this._scheduleAutoplay();
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
_scheduleAutoplay() {
|
|
870
|
+
this._stopAutoplay();
|
|
871
|
+
if (this.options.autoplay > 0 && !this._autoplay_stopped) {
|
|
872
|
+
this._autoplay_timer = setTimeout(() => {
|
|
873
|
+
this._autoplay_timer = null;
|
|
874
|
+
if (this.current_slide + 1 < this.data.slides.length) {
|
|
875
|
+
this.goTo(this.current_slide + 1);
|
|
876
|
+
this._scheduleAutoplay();
|
|
877
|
+
}
|
|
878
|
+
}, this.options.autoplay);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
_stopAutoplay() {
|
|
883
|
+
if (this._autoplay_timer) {
|
|
884
|
+
clearTimeout(this._autoplay_timer);
|
|
885
|
+
this._autoplay_timer = null;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/** Keep the URL hash in sync with the current slide (#slide-N). */
|
|
890
|
+
_syncHash() {
|
|
891
|
+
try {
|
|
892
|
+
// preserve the query string: pages like the embed player carry
|
|
893
|
+
// their configuration in it (?url=..., ?example=...)
|
|
894
|
+
history.replaceState(
|
|
895
|
+
null,
|
|
896
|
+
"",
|
|
897
|
+
window.location.pathname + window.location.search + "#slide-" + this.current_slide,
|
|
898
|
+
);
|
|
899
|
+
} catch {
|
|
900
|
+
// non-browser or sandboxed contexts
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/** A #slide-N hash deep-links the storymap (applied on load + hashchange). */
|
|
905
|
+
_applyHashSlide(): boolean {
|
|
906
|
+
const match = /^#slide-(\d+)$/.exec(window.location.hash);
|
|
907
|
+
if (!match) return false;
|
|
908
|
+
const n = parseInt(match[1], 10);
|
|
909
|
+
if (n >= 0 && n < this.data.slides.length && n !== this.current_slide) {
|
|
910
|
+
this.goTo(n);
|
|
911
|
+
return true;
|
|
912
|
+
}
|
|
913
|
+
return false;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/* Resize handling
|
|
917
|
+
================================================== */
|
|
918
|
+
_initResizeHandling() {
|
|
919
|
+
if (!this.options.trackResize) {
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
// Leading + trailing debounce: the first resize event re-layouts
|
|
923
|
+
// immediately (instant feedback), the trailing one settles after the
|
|
924
|
+
// resize burst ends.
|
|
925
|
+
const onResize = () => {
|
|
926
|
+
if (this._resize_timer) {
|
|
927
|
+
clearTimeout(this._resize_timer);
|
|
928
|
+
} else {
|
|
929
|
+
this.updateDisplay();
|
|
930
|
+
}
|
|
931
|
+
this._resize_timer = setTimeout(() => {
|
|
932
|
+
this._resize_timer = null;
|
|
933
|
+
this.updateDisplay();
|
|
934
|
+
}, 200);
|
|
935
|
+
};
|
|
936
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
937
|
+
// covers containers resized by their embedding layout
|
|
938
|
+
this._resize_observer = new ResizeObserver(onResize);
|
|
939
|
+
this._resize_observer.observe(this._el.container);
|
|
940
|
+
}
|
|
941
|
+
window.addEventListener("resize", onResize);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* Resolve a caller-supplied map element (options.map_options.element).
|
|
946
|
+
* The passed element is "replaced by the real one": it is adopted as the
|
|
947
|
+
* map container (given the vco-map class) and moved into place between
|
|
948
|
+
* the menubar and the story slider.
|
|
949
|
+
*/
|
|
950
|
+
_resolveMapElement(): HTMLElement | null {
|
|
951
|
+
const element = this.options.map_options?.element;
|
|
952
|
+
if (!element) {
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
const el = typeof element === "string" ? Dom.get(element) : element;
|
|
956
|
+
if (!el) {
|
|
957
|
+
console.error("StoryMapJS: map_options.element not found: " + String(element));
|
|
958
|
+
return null;
|
|
959
|
+
}
|
|
960
|
+
el.classList.add("vco-map");
|
|
961
|
+
this._el.menubar.after(el);
|
|
962
|
+
return el;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
_onTitle(e: unknown) {
|
|
966
|
+
this.fire("title", e);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
_onColorChange(e: { color?: unknown; image?: unknown }) {
|
|
970
|
+
if (e.color || e.image) {
|
|
971
|
+
this._menubar.setColor(true);
|
|
972
|
+
} else {
|
|
973
|
+
this._menubar.setColor(false);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
_onSlideChange(e: { current_slide: number }) {
|
|
978
|
+
if (this.current_slide !== e.current_slide) {
|
|
979
|
+
const duration = slideTransitionDuration(this.current_slide, e.current_slide);
|
|
980
|
+
this.current_slide = e.current_slide;
|
|
981
|
+
this._map.goTo(this.current_slide);
|
|
982
|
+
this._beginTransition(duration);
|
|
983
|
+
this.fire("change", { current_slide: this.current_slide }, this);
|
|
984
|
+
this._syncHash();
|
|
985
|
+
this._scheduleAutoplay();
|
|
986
|
+
this._updateProgress();
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
_onMapChange(e: { current_marker: number }) {
|
|
991
|
+
if (this.current_slide !== e.current_marker) {
|
|
992
|
+
const duration = slideTransitionDuration(this.current_slide, e.current_marker);
|
|
993
|
+
this.current_slide = e.current_marker;
|
|
994
|
+
this._storyslider.goTo(this.current_slide);
|
|
995
|
+
this._beginTransition(duration);
|
|
996
|
+
this.fire("change", { current_slide: this.current_slide }, this);
|
|
997
|
+
this._syncHash();
|
|
998
|
+
this._scheduleAutoplay();
|
|
999
|
+
this._updateProgress();
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
_updateProgress() {
|
|
1004
|
+
if (this.options.show_progress && this._menubar) {
|
|
1005
|
+
this._menubar.setProgress(this.current_slide, this.data.slides.length);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
_onOverview(e?: unknown) {
|
|
1010
|
+
this._map.markerOverview();
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Toggle the native fullscreen API on the storymap container and keep the
|
|
1015
|
+
* menubar button state in sync.
|
|
1016
|
+
*/
|
|
1017
|
+
_onFullscreenToggle(e?: unknown) {
|
|
1018
|
+
if (document.fullscreenElement === this._el.container) {
|
|
1019
|
+
document.exitFullscreen().catch((err: unknown) => {
|
|
1020
|
+
console.error("StoryMapJS: could not exit fullscreen", err);
|
|
1021
|
+
});
|
|
1022
|
+
} else {
|
|
1023
|
+
this._el.container.requestFullscreen().catch((err: unknown) => {
|
|
1024
|
+
console.error("StoryMapJS: could not enter fullscreen", err);
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
_onFullscreenChange() {
|
|
1030
|
+
const active = document.fullscreenElement === this._el.container;
|
|
1031
|
+
this._menubar.setFullscreenState(active);
|
|
1032
|
+
// re-measure once the browser has applied the fullscreen layout
|
|
1033
|
+
// (the container resizes asynchronously after the event)
|
|
1034
|
+
requestAnimationFrame(() => {
|
|
1035
|
+
requestAnimationFrame(() => {
|
|
1036
|
+
this.updateDisplay();
|
|
1037
|
+
});
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
_onBackToStart(e?: unknown) {
|
|
1042
|
+
this.current_slide = 0;
|
|
1043
|
+
this._map.goTo(this.current_slide);
|
|
1044
|
+
this._storyslider.goTo(this.current_slide);
|
|
1045
|
+
this.fire("change", { current_slide: this.current_slide }, this);
|
|
1046
|
+
// keep hash, progress and autoplay in sync like the other paths
|
|
1047
|
+
this._syncHash();
|
|
1048
|
+
this._scheduleAutoplay();
|
|
1049
|
+
this._updateProgress();
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
_onMenuBarCollapse(e: { y: number }) {
|
|
1053
|
+
this._updateDisplay(e.y, true);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
_onMapLoaded() {
|
|
1057
|
+
this._loaded.map = true;
|
|
1058
|
+
this._onLoaded();
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
_onStorySliderLoaded() {
|
|
1062
|
+
this._loaded.storyslider = true;
|
|
1063
|
+
this._onLoaded();
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* Compute and display the great-circle distance of the marker route
|
|
1068
|
+
* (issue #341); hidden when `show_distance` is off.
|
|
1069
|
+
*/
|
|
1070
|
+
_updateDistance() {
|
|
1071
|
+
if (!this.options.show_distance) {
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
const km = this._map.getRouteDistance();
|
|
1075
|
+
this._menubar.setDistance(km);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
_onLoaded() {
|
|
1079
|
+
if (this._loaded.storyslider && this._loaded.map) {
|
|
1080
|
+
this.fire("loaded", this.data);
|
|
1081
|
+
if (!this._hash_initialized) {
|
|
1082
|
+
this._hash_initialized = true;
|
|
1083
|
+
// a #slide-N hash deep-links the initial slide (issue #146)
|
|
1084
|
+
// and keeps working through the browser back/forward buttons
|
|
1085
|
+
this._applyHashSlide();
|
|
1086
|
+
window.addEventListener("hashchange", () => this._applyHashSlide());
|
|
1087
|
+
this._syncHash();
|
|
1088
|
+
}
|
|
1089
|
+
this._updateProgress();
|
|
1090
|
+
this._updateDistance();
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
export default class StoryMap extends Evented(StoryMapBase) {
|
|
1096
|
+
/**
|
|
1097
|
+
* The library base path (the directory containing the module).
|
|
1098
|
+
* Derived from `import.meta.url`, which works both for the source module
|
|
1099
|
+
* (src/main.ts) and the built ESM bundle (js/storymap.js).
|
|
1100
|
+
*/
|
|
1101
|
+
static SCRIPT_PATH = new URL("../", import.meta.url).href;
|
|
1102
|
+
|
|
1103
|
+
constructor(...args: ConstructorParameters<typeof StoryMapBase>) {
|
|
1104
|
+
super(...args);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
export { StoryMap };
|