@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.
Files changed (148) hide show
  1. package/CHANGELOG +11 -0
  2. package/dist/assets/{demo-CVQ7kFek.js → demo-D6cTmzn6.js} +1 -1
  3. package/dist/assets/{harness-C-VZV-ou.js → harness-BPFlzutp.js} +1 -1
  4. package/dist/assets/main-Y-MYUSJ4.js +17 -0
  5. package/dist/css/fonts/font.oldstandard.css +18 -22
  6. package/dist/demo.html +2 -2
  7. package/dist/docs/migration.html +1 -1
  8. package/dist/embed/index.html +129 -4
  9. package/dist/harness.html +2 -2
  10. package/dist/index.html +17 -0
  11. package/dist/js/storymap.d.ts +126 -7
  12. package/dist/js/storymap.js +4518 -3613
  13. package/dist/js/storymap.js.map +1 -1
  14. package/docs/migration-from-knightlab.md +139 -0
  15. package/docs/storymap-as-iiif-manifest.md +510 -0
  16. package/package.json +6 -3
  17. package/src/animation/easings.ts +18 -0
  18. package/src/animation/morpheus.d.ts +41 -0
  19. package/src/core/Browser.ts +19 -0
  20. package/src/core/Load.ts +180 -0
  21. package/src/core/Util.ts +354 -0
  22. package/src/core/mixins.ts +171 -0
  23. package/src/dom/Dom.ts +31 -0
  24. package/src/dom/DomEvent.ts +61 -0
  25. package/src/globals.d.ts +16 -0
  26. package/src/language/Language.ts +107 -0
  27. package/src/language/locale/be.json +18 -0
  28. package/src/language/locale/bg.json +18 -0
  29. package/src/language/locale/cs.json +18 -0
  30. package/src/language/locale/de.json +20 -0
  31. package/src/language/locale/el.json +18 -0
  32. package/src/language/locale/en.json +28 -0
  33. package/src/language/locale/es.json +18 -0
  34. package/src/language/locale/et.json +18 -0
  35. package/src/language/locale/fr.json +18 -0
  36. package/src/language/locale/he.json +18 -0
  37. package/src/language/locale/hu.json +18 -0
  38. package/src/language/locale/is.json +18 -0
  39. package/src/language/locale/it.json +18 -0
  40. package/src/language/locale/jp.json +18 -0
  41. package/src/language/locale/ko.json +18 -0
  42. package/src/language/locale/nl.json +18 -0
  43. package/src/language/locale/nn.json +18 -0
  44. package/src/language/locale/no.json +18 -0
  45. package/src/language/locale/pl.json +18 -0
  46. package/src/language/locale/pt.json +18 -0
  47. package/src/language/locale/ru.json +18 -0
  48. package/src/language/locale/sk.json +18 -0
  49. package/src/language/locale/sr.json +17 -0
  50. package/src/language/locale/sv.json +18 -0
  51. package/src/language/locale/tr.json +18 -0
  52. package/src/language/locale/uk.json +18 -0
  53. package/src/language/locale/ur.json +18 -0
  54. package/src/language/locale/zh-cn.json +18 -0
  55. package/src/language/locale/zh-tw.json +18 -0
  56. package/src/main.ts +51 -0
  57. package/src/map/Map.ts +717 -0
  58. package/src/map/MapMarker.ts +149 -0
  59. package/src/map/openlayers/Map.OpenLayers.ts +1879 -0
  60. package/src/map/openlayers/MapMarker.OpenLayers.ts +176 -0
  61. package/src/map/openlayers/zoomifyTiles.ts +28 -0
  62. package/src/map/types.ts +17 -0
  63. package/src/media/EmbedUtil.ts +168 -0
  64. package/src/media/Media.ts +409 -0
  65. package/src/media/MediaType.ts +163 -0
  66. package/src/media/types/Audio.ts +87 -0
  67. package/src/media/types/Blockquote.ts +40 -0
  68. package/src/media/types/DailyMotion.ts +58 -0
  69. package/src/media/types/DocumentCloud.ts +41 -0
  70. package/src/media/types/Facebook.ts +41 -0
  71. package/src/media/types/Flickr.ts +115 -0
  72. package/src/media/types/GoogleDoc.ts +59 -0
  73. package/src/media/types/IFrame.ts +51 -0
  74. package/src/media/types/Image.ts +49 -0
  75. package/src/media/types/Juxtapose.ts +32 -0
  76. package/src/media/types/SoundCloud.ts +77 -0
  77. package/src/media/types/Text.ts +172 -0
  78. package/src/media/types/Twitter.ts +96 -0
  79. package/src/media/types/Video.ts +84 -0
  80. package/src/media/types/Vimeo.ts +65 -0
  81. package/src/media/types/Website.ts +36 -0
  82. package/src/media/types/Wikipedia.ts +124 -0
  83. package/src/media/types/YouTube.ts +192 -0
  84. package/src/scss/Typography.scss +237 -0
  85. package/src/scss/VCO.StoryMap.Dark.scss +138 -0
  86. package/src/scss/VCO.StoryMap.scss +164 -0
  87. package/src/scss/Variables.Dark.scss +56 -0
  88. package/src/scss/Variables.scss +47 -0
  89. package/src/scss/core/Mixins.scss +112 -0
  90. package/src/scss/core/Reset.scss +206 -0
  91. package/src/scss/fonts/_font.base.scss +113 -0
  92. package/src/scss/fonts/font.abril-droidsans.scss +75 -0
  93. package/src/scss/fonts/font.amatic-andika.scss +73 -0
  94. package/src/scss/fonts/font.bitter-raleway.scss +86 -0
  95. package/src/scss/fonts/font.clicker-garamond.scss +92 -0
  96. package/src/scss/fonts/font.dancing-ledger.scss +74 -0
  97. package/src/scss/fonts/font.default.scss +9 -0
  98. package/src/scss/fonts/font.fjalla-average.scss +80 -0
  99. package/src/scss/fonts/font.georgia-helvetica.scss +58 -0
  100. package/src/scss/fonts/font.knightlab.scss +97 -0
  101. package/src/scss/fonts/font.lustria-lato.scss +80 -0
  102. package/src/scss/fonts/font.medula-lato.scss +73 -0
  103. package/src/scss/fonts/font.oldstandard.scss +88 -0
  104. package/src/scss/fonts/font.opensans-gentiumbook.scss +80 -0
  105. package/src/scss/fonts/font.playfair-faunaone.scss +86 -0
  106. package/src/scss/fonts/font.playfair.scss +85 -0
  107. package/src/scss/fonts/font.pt.scss +90 -0
  108. package/src/scss/fonts/font.roboto-megrim.scss +73 -0
  109. package/src/scss/fonts/font.rufina-sintony.scss +73 -0
  110. package/src/scss/fonts/font.ubuntu.scss +78 -0
  111. package/src/scss/fonts/font.unicaone-vollkorn.scss +85 -0
  112. package/src/scss/icons/Icons.scss +239 -0
  113. package/src/scss/map/VCO.ImageMarker.scss +23 -0
  114. package/src/scss/map/VCO.Map.scss +21 -0
  115. package/src/scss/map/VCO.MapMarker.scss +147 -0
  116. package/src/scss/map/openlayers/VCO.Map.OpenLayers.scss +126 -0
  117. package/src/scss/media/VCO.Media.scss +222 -0
  118. package/src/scss/media/types/VCO.Media.Blockquote.scss +76 -0
  119. package/src/scss/media/types/VCO.Media.IFrame.scss +11 -0
  120. package/src/scss/media/types/VCO.Media.Image.scss +13 -0
  121. package/src/scss/media/types/VCO.Media.Instagram.scss +7 -0
  122. package/src/scss/media/types/VCO.Media.Profile.scss +5 -0
  123. package/src/scss/media/types/VCO.Media.Text.scss +90 -0
  124. package/src/scss/media/types/VCO.Media.Twitter.scss +87 -0
  125. package/src/scss/media/types/VCO.Media.Wikipedia.scss +64 -0
  126. package/src/scss/site/site.scss +748 -0
  127. package/src/scss/slider/VCO.Slide.scss +447 -0
  128. package/src/scss/slider/VCO.SlideNav.scss +338 -0
  129. package/src/scss/slider/VCO.StorySlider.scss +64 -0
  130. package/src/scss/snapmap/VCO.SnapMap.scss +103 -0
  131. package/src/scss/ui/VCO.Consent.scss +77 -0
  132. package/src/scss/ui/VCO.MenuBar.Button.scss +99 -0
  133. package/src/scss/ui/VCO.MenuBar.scss +20 -0
  134. package/src/scss/ui/VCO.Message.scss +104 -0
  135. package/src/site/site.ts +87 -0
  136. package/src/slider/Slide.ts +460 -0
  137. package/src/slider/SlideNav.ts +212 -0
  138. package/src/slider/StorySlider.ts +722 -0
  139. package/src/storymap/Consent.ts +146 -0
  140. package/src/storymap/StoryMap.ts +1108 -0
  141. package/src/storymap/iiif.ts +386 -0
  142. package/src/storymap/validate.ts +184 -0
  143. package/src/types.ts +297 -0
  144. package/src/ui/Draggable.ts +374 -0
  145. package/src/ui/MenuBar.ts +323 -0
  146. package/src/ui/Message.ts +118 -0
  147. package/src/ui/Swipable.ts +388 -0
  148. package/dist/assets/main-DwbXrrYn.js +0 -17
@@ -0,0 +1,409 @@
1
+ import { mergeData } from "../core/Util";
2
+ import { Evented, type EventedInstance } from "../core/mixins";
3
+ import Dom from "../dom/Dom";
4
+ import Message from "../ui/Message";
5
+ import { Browser } from "../core/Browser";
6
+ import { MediaState, StorymapSlideMedia } from "../types";
7
+ import { consentManagerOf, consentMessage } from "../storymap/Consent";
8
+ import { loadJS } from "../core/Load";
9
+ /* VCO.Media
10
+ Main media template for media assets.
11
+ Takes a data object and populates a dom object
12
+ ================================================== */
13
+
14
+ /* Options for Media and its subclasses: the fields Media itself sets
15
+ or reads. Everything else merged in via mergeData is absorbed by the
16
+ index signature. */
17
+ export interface MediaOptions {
18
+ width?: number;
19
+ height?: number;
20
+ layout?: string;
21
+ media_name?: string;
22
+ media_type?: string;
23
+ credit_height?: number;
24
+ caption_height?: number;
25
+ api_key_flickr?: string;
26
+ [key: string]: unknown;
27
+ }
28
+
29
+ /* Data for media assets: the shared exchange format plus the link
30
+ fields the base layout renders. */
31
+ export interface MediaData extends StorymapSlideMedia {
32
+ uniqueid?: string;
33
+ link?: string;
34
+ link_target?: string;
35
+ [key: string]: unknown;
36
+ }
37
+
38
+ export class MediaBase {
39
+ declare "_el": Record<string, HTMLElement>;
40
+ declare "player": unknown;
41
+ declare "timer": ReturnType<typeof setTimeout>;
42
+ declare "load_timer": ReturnType<typeof setTimeout>;
43
+ declare "load_controller": AbortController | null;
44
+ declare "message": Message;
45
+ declare "media_id": unknown;
46
+ declare "_state": MediaState;
47
+ declare "data": MediaData;
48
+ declare "options": MediaOptions;
49
+ declare "animator": unknown;
50
+ declare "_media": unknown;
51
+ declare "_": (key: string) => string;
52
+ declare "fire": EventedInstance["fire"];
53
+
54
+ //_el: {},
55
+
56
+ /* Constructor
57
+ ================================================== */
58
+ constructor(data: MediaData, options?: MediaOptions, add_to_container?: HTMLElement) {
59
+ // DOM ELEMENTS
60
+ this._el = {
61
+ container: {} as HTMLElement,
62
+ content_container: {} as HTMLElement,
63
+ content: {} as HTMLElement,
64
+ content_item: {} as HTMLElement,
65
+ content_link: {} as HTMLElement,
66
+ caption: null,
67
+ credit: null,
68
+ parent: {} as HTMLElement,
69
+ link: null,
70
+ };
71
+
72
+ // Player (If Needed)
73
+ this.player = null;
74
+
75
+ // Timer (If Needed)
76
+ this.timer = null;
77
+ this.load_timer = null;
78
+ this.load_controller = null;
79
+
80
+ // Message
81
+ this.message = null;
82
+
83
+ // Media ID
84
+ this.media_id = null;
85
+
86
+ // State
87
+ this._state = {
88
+ loaded: false,
89
+ show_meta: false,
90
+ media_loaded: false,
91
+ };
92
+
93
+ // Data
94
+ this.data = {
95
+ uniqueid: null,
96
+ url: null,
97
+ credit: null,
98
+ caption: null,
99
+ link: null,
100
+ link_target: null,
101
+ };
102
+
103
+ //Options
104
+ this.options = {
105
+ api_key_flickr: "", // no bundled key: pass api_key_flickr in the options
106
+ credit_height: 0,
107
+ caption_height: 0,
108
+ };
109
+
110
+ this.animator = {};
111
+
112
+ // Merge Data and Options
113
+ mergeData(this.options, options);
114
+ mergeData(this.data, data);
115
+
116
+ this._el.container = Dom.create("div", "vco-media");
117
+
118
+ if (this.data.uniqueid) {
119
+ this._el.container.id = this.data.uniqueid;
120
+ }
121
+
122
+ this._initLayout();
123
+
124
+ if (add_to_container) {
125
+ add_to_container.appendChild(this._el.container);
126
+ this._el.parent = add_to_container;
127
+ }
128
+ }
129
+
130
+ async loadMedia() {
131
+ if (!this._state.loaded) {
132
+ const manager = consentManagerOf(this.options);
133
+ if (this.options.consent_required && manager && this.options.media_type) {
134
+ // GDPR consent mode: ask before loading anything from this
135
+ // external service
136
+ const service = this.options.media_type as string;
137
+ let host = "";
138
+ try {
139
+ host = new URL((this.data as { url?: string })?.url ?? "").host;
140
+ } catch {
141
+ // keep the empty host
142
+ }
143
+ // content_item is only created by the (deferred) media load —
144
+ // render the ask into the existing content container
145
+ const target = (this._el.content_container ?? this._el.container) as HTMLElement;
146
+ if (await manager.request(service, host, target)) {
147
+ this._beginLoad();
148
+ } else {
149
+ this._showBlocked();
150
+ }
151
+ return;
152
+ }
153
+ this._beginLoad();
154
+ }
155
+ }
156
+
157
+ _beginLoad() {
158
+ // a sync throw inside a media type's _loadMedia (e.g. an unparseable
159
+ // media URL) must not escape the timer callback as an uncaught error
160
+ this.load_timer = setTimeout(() => {
161
+ try {
162
+ this._loadMedia();
163
+ this._state.loaded = true;
164
+ this._updateDisplay();
165
+ } catch (e) {
166
+ console.log("Error loading media for ", this._media);
167
+ console.log(e);
168
+ this.loadErrorDisplay("Error loading media.");
169
+ }
170
+ }, 1200);
171
+ }
172
+
173
+ _showBlocked() {
174
+ // denied: show a placeholder instead of the media
175
+ const target = (this._el.content_container ?? this._el.container) as HTMLElement;
176
+ const blocked = document.createElement("div");
177
+ blocked.className = "vco-consent-blocked";
178
+ blocked.textContent = (
179
+ consentMessage("consent_blocked", "Content from {service} is blocked.") as string
180
+ ).replace("{service}", (this.options.media_type as string) ?? "");
181
+ target.append(blocked);
182
+ this.onLoaded(true);
183
+ }
184
+
185
+ loadingMessage() {
186
+ this.message.updateMessage(this._("loading") + " " + this.options.media_name);
187
+ }
188
+
189
+ updateMediaDisplay(layout?: string) {
190
+ if (this._state.loaded) {
191
+ this._updateMediaDisplay(layout);
192
+
193
+ if (!Browser.mobile && layout !== "portrait") {
194
+ this._el.content_item.style.maxHeight = this.options.height / 2 + "px";
195
+ }
196
+
197
+ if (this._state.media_loaded) {
198
+ if (this._el.credit) {
199
+ this._el.credit.style.width = "auto";
200
+ }
201
+ if (this._el.caption) {
202
+ this._el.caption.style.width = "auto";
203
+ }
204
+ }
205
+
206
+ if (layout === "portrait") {
207
+ this._el.content_item.style.maxHeight = "none";
208
+ }
209
+ if (this._state.media_loaded) {
210
+ if (this._el.credit) {
211
+ this._el.credit.style.width = this._el.content_item.offsetWidth + "px";
212
+ }
213
+ if (this._el.caption) {
214
+ this._el.caption.style.width = this._el.content_item.offsetWidth + "px";
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ /* Media Specific
221
+ ================================================== */
222
+ _loadMedia() {}
223
+
224
+ _updateMediaDisplay(l?: string) {
225
+ //this._el.content_item.style.maxHeight = (this.options.height - this.options.credit_height - this.options.caption_height - 16) + "px";
226
+ }
227
+
228
+ /* Public
229
+ ================================================== */
230
+ show() {}
231
+
232
+ hide() {}
233
+
234
+ addTo(container: HTMLElement) {
235
+ container.appendChild(this._el.container);
236
+ this.onAdd();
237
+ }
238
+
239
+ removeFrom(container: HTMLElement) {
240
+ container.removeChild(this._el.container);
241
+ this.onRemove();
242
+ }
243
+
244
+ // Update Display
245
+ updateDisplay(w?: number, h?: number, l?: string) {
246
+ this._updateDisplay(w, h, l);
247
+ }
248
+
249
+ stopMedia() {
250
+ // Cancel a load that hasn't started yet (navigated away within the
251
+ // load delay): avoids building iframes and injecting scripts for a
252
+ // slide the visitor already skipped past
253
+ if (!this._state.loaded && this.load_timer) {
254
+ clearTimeout(this.load_timer);
255
+ this.load_timer = null;
256
+ }
257
+ // Abort an in-flight external script load, if any
258
+ if (this.load_controller) {
259
+ this.load_controller.abort();
260
+ this.load_controller = null;
261
+ }
262
+ this._stopMedia();
263
+ }
264
+
265
+ /**
266
+ * Load an external script for this media, abortable via stopMedia().
267
+ * Rejects with AbortError when the load is cancelled.
268
+ */
269
+ async loadScript(url: string): Promise<void> {
270
+ this.load_controller?.abort();
271
+ const controller = new AbortController();
272
+ this.load_controller = controller;
273
+ try {
274
+ await loadJS(url, { signal: controller.signal });
275
+ } finally {
276
+ if (this.load_controller === controller) {
277
+ this.load_controller = null;
278
+ }
279
+ }
280
+ }
281
+
282
+ loadErrorDisplay(message: string) {
283
+ if (this._el.content_item && this._el.content_item.parentNode === this._el.content) {
284
+ this._el.content.removeChild(this._el.content_item);
285
+ }
286
+ this._el.content_item = Dom.create(
287
+ "div",
288
+ "vco-media-item vco-media-loaderror",
289
+ this._el.content,
290
+ );
291
+ this._el.content_item.innerHTML =
292
+ "<div class='vco-icon-" + this.options.media_type + "'></div><p>" + message + "</p>";
293
+
294
+ // After Loaded
295
+ this.onLoaded(true);
296
+ }
297
+
298
+ /* Events
299
+ ================================================== */
300
+ onLoaded(error?: boolean) {
301
+ this._state.loaded = true;
302
+ this.fire("loaded", this.data);
303
+ if (this.message) {
304
+ this.message.hide();
305
+ }
306
+ if (!error) {
307
+ this.showMeta();
308
+ }
309
+ this.updateDisplay();
310
+ }
311
+
312
+ onMediaLoaded(e?: unknown) {
313
+ this._state.media_loaded = true;
314
+ this.fire("media_loaded", this.data);
315
+ if (this._el.credit) {
316
+ this._el.credit.style.width = this._el.content_item.offsetWidth + "px";
317
+ }
318
+ if (this._el.caption) {
319
+ this._el.caption.style.width = this._el.content_item.offsetWidth + "px";
320
+ }
321
+ }
322
+
323
+ showMeta(credit?: unknown, caption?: unknown) {
324
+ this._state.show_meta = true;
325
+ // Credit
326
+ if (this.data.credit && this.data.credit !== "" && !this._el.credit) {
327
+ this._el.credit = Dom.create("div", "vco-credit", this._el.content_container);
328
+ this._el.credit.innerHTML = this.data.credit;
329
+ this.options.credit_height = this._el.credit.offsetHeight;
330
+ }
331
+
332
+ // Caption
333
+ if (this.data.caption && this.data.caption !== "" && !this._el.caption) {
334
+ this._el.caption = Dom.create("div", "vco-caption", this._el.content_container);
335
+ this._el.caption.innerHTML = this.data.caption;
336
+ this.options.caption_height = this._el.caption.offsetHeight;
337
+ }
338
+ }
339
+
340
+ onAdd() {
341
+ this.fire("added", this.data);
342
+ }
343
+
344
+ onRemove() {
345
+ this.fire("removed", this.data);
346
+ }
347
+
348
+ /* Private Methods
349
+ ================================================== */
350
+ _initLayout() {
351
+ // Message
352
+ this.message = new Message({}, this.options);
353
+ this.message.addTo(this._el.container);
354
+
355
+ // Create Layout
356
+ this._el.content_container = Dom.create(
357
+ "div",
358
+ "vco-media-content-container",
359
+ this._el.container,
360
+ );
361
+
362
+ // Link
363
+ if (this.data.link && this.data.link !== "") {
364
+ this._el.link = Dom.create("a", "vco-media-link", this._el.content_container);
365
+ const link = this._el.link as HTMLAnchorElement;
366
+ link.href = this.data.link;
367
+ if (this.data.link_target && this.data.link_target !== "") {
368
+ link.target = this.data.link_target;
369
+ } else {
370
+ link.target = "_blank";
371
+ }
372
+
373
+ this._el.content = Dom.create("div", "vco-media-content", this._el.link);
374
+ } else {
375
+ this._el.content = Dom.create("div", "vco-media-content", this._el.content_container);
376
+ }
377
+ }
378
+
379
+ // Update Display
380
+ _updateDisplay(w?: number, h?: number, l?: string) {
381
+ if (w) {
382
+ this.options.width = w;
383
+ }
384
+ if (h) {
385
+ this.options.height = h;
386
+ }
387
+
388
+ if (l) {
389
+ this.options.layout = l;
390
+ }
391
+
392
+ if (this._el.credit) {
393
+ this.options.credit_height = this._el.credit.offsetHeight;
394
+ }
395
+ if (this._el.caption) {
396
+ this.options.caption_height = this._el.caption.offsetHeight + 5;
397
+ }
398
+
399
+ this.updateMediaDisplay(this.options.layout);
400
+ }
401
+
402
+ _stopMedia() {}
403
+ }
404
+
405
+ export class Media extends Evented(MediaBase) {
406
+ constructor(...args: ConstructorParameters<typeof MediaBase>) {
407
+ super(...args);
408
+ }
409
+ }
@@ -0,0 +1,163 @@
1
+ import Image from "./types/Image";
2
+ import YouTube from "./types/YouTube";
3
+ import Blockquote from "./types/Blockquote";
4
+ import Wikipedia from "./types/Wikipedia";
5
+ import SoundCloud from "./types/SoundCloud";
6
+ import Vimeo from "./types/Vimeo";
7
+ import Video from "./types/Video";
8
+ import Audio from "./types/Audio";
9
+ import DailyMotion from "./types/DailyMotion";
10
+ import Twitter from "./types/Twitter";
11
+ import Flickr from "./types/Flickr";
12
+ import GoogleDoc from "./types/GoogleDoc";
13
+ import IFrame from "./types/IFrame";
14
+ import Website from "./types/Website";
15
+ import Facebook from "./types/Facebook";
16
+ import DocumentCloud from "./types/DocumentCloud";
17
+ import Juxtapose from "./types/Juxtapose";
18
+ import { Media } from "./Media";
19
+ import { MediaTypeMatch, StorymapSlideMedia } from "../types";
20
+
21
+ /* MediaType
22
+ Determines the type of media the url string is.
23
+ returns an object with .type and .id
24
+ You can add new media types by adding a regex
25
+ to match and the media class name to use to
26
+ render the media
27
+ ================================================== */
28
+
29
+ /* A media type table entry: like MediaTypeMatch, but match_str may be
30
+ a RegExp for pattern-based types (e.g. images). */
31
+ type MediaTypeEntry = Omit<MediaTypeMatch, "match_str"> & {
32
+ match_str: string | RegExp;
33
+ };
34
+
35
+ /**
36
+ * Resolve the media handler for a slide's media object: matches the URL (and
37
+ * optional explicit `type`) against the supported media types — YouTube,
38
+ * Vimeo, images, audio, video, ...
39
+ *
40
+ * @param m - The slide media definition.
41
+ * @returns The matching media type entry, or `false` for unknown media.
42
+ */
43
+ export default function MediaType(m: StorymapSlideMedia): MediaTypeMatch | false {
44
+ const media_types: MediaTypeEntry[] = [
45
+ {
46
+ type: "youtube",
47
+ name: "YouTube",
48
+ match_str: "(www.)?youtube|youtu.be",
49
+ cls: YouTube,
50
+ },
51
+ {
52
+ type: "vimeo",
53
+ name: "Vimeo",
54
+ match_str: "(player.)?vimeo.com",
55
+ cls: Vimeo,
56
+ },
57
+ {
58
+ type: "dailymotion",
59
+ name: "DailyMotion",
60
+ match_str: "(www.)?dailymotion.com",
61
+ cls: DailyMotion,
62
+ },
63
+ {
64
+ type: "soundcloud",
65
+ name: "SoundCloud",
66
+ match_str: "(player.)?soundcloud.com",
67
+ cls: SoundCloud,
68
+ },
69
+ {
70
+ type: "twitter",
71
+ name: "Twitter",
72
+ match_str: "^(https?:)?/+(www.)?(twitter|x).com",
73
+ cls: Twitter,
74
+ },
75
+ {
76
+ type: "flickr",
77
+ name: "Flickr",
78
+ match_str: "flickr.com/photos",
79
+ cls: Flickr,
80
+ },
81
+ {
82
+ type: "image",
83
+ name: "Image",
84
+ match_str: /jpg|jpeg|png|gif|webp/i,
85
+ cls: Image,
86
+ },
87
+ {
88
+ type: "video",
89
+ name: "Video",
90
+ match_str: /(mp4|webm)(\?.*)?$/i,
91
+ cls: Video,
92
+ },
93
+ {
94
+ type: "audio",
95
+ name: "Audio",
96
+ match_str: /(mp3|wav|m4a)(\?.*)?$/i,
97
+ cls: Audio,
98
+ },
99
+ {
100
+ type: "googledocs",
101
+ name: "Google Doc",
102
+ match_str:
103
+ "^(https?:)?/*[^.]*.google.com/[^/]*/d/[^/]*/[^/]*?usp=sharing|^(https?:)?/*drive.google.com/open?id=[^&]*&authuser=0|^(https?:)?//*drive.google.com/open\\?id=[^&]*|^(https?:)?/*[^.]*.googledrive.com/host/[^/]*/",
104
+ cls: GoogleDoc,
105
+ },
106
+ {
107
+ type: "wikipedia",
108
+ name: "Wikipedia",
109
+ match_str: "(www.)?wikipedia.org",
110
+ cls: Wikipedia,
111
+ },
112
+ {
113
+ type: "iframe",
114
+ name: "iFrame",
115
+ match_str: "iframe",
116
+ cls: IFrame,
117
+ },
118
+ {
119
+ type: "facebook",
120
+ name: "Facebook",
121
+ match_str: "(www.)?facebook.com",
122
+ cls: Facebook,
123
+ },
124
+ {
125
+ type: "documentcloud",
126
+ name: "DocumentCloud",
127
+ match_str: "documentcloud.org/documents/",
128
+ cls: DocumentCloud,
129
+ },
130
+ {
131
+ type: "juxtapose",
132
+ name: "Juxtapose",
133
+ match_str: "juxtapose",
134
+ cls: Juxtapose,
135
+ },
136
+ {
137
+ type: "blockquote",
138
+ name: "Quote",
139
+ match_str: "blockquote",
140
+ cls: Blockquote,
141
+ },
142
+ {
143
+ type: "website",
144
+ name: "Website",
145
+ match_str: "https?://",
146
+ cls: Website,
147
+ },
148
+ {
149
+ type: "",
150
+ name: "",
151
+ match_str: "",
152
+ cls: Media,
153
+ },
154
+ ];
155
+
156
+ for (const media_type of media_types) {
157
+ if (typeof m.url === "string" && m.url.match(media_type.match_str)) {
158
+ return media_type as MediaTypeMatch;
159
+ }
160
+ }
161
+
162
+ return false;
163
+ }
@@ -0,0 +1,87 @@
1
+ import { Media } from "../Media";
2
+ import Dom from "../../dom/Dom";
3
+ import { Language } from "../../language/Language";
4
+
5
+ /* Media.Vimeo
6
+ ================================================== */
7
+
8
+ export default class Audio extends Media {
9
+ declare "player_element": HTMLMediaElement;
10
+
11
+ /* Load the media
12
+ ================================================== */
13
+ _loadMedia() {
14
+ // Loading Message
15
+ this.message.updateMessage(Language.messages.loading + " " + this.options.media_name);
16
+
17
+ // Create Dom element
18
+ this._el.content_item = Dom.create(
19
+ "audio",
20
+ "vco-media-item vco-media-audio vco-media-shadow",
21
+ this._el.content,
22
+ );
23
+ const media_item = this._el.content_item as HTMLMediaElement;
24
+ media_item.controls = true;
25
+ this._el.source_item = Dom.create("source", "", this._el.content_item);
26
+ const source_item = this._el.source_item as HTMLSourceElement;
27
+
28
+ // Media Loaded Event
29
+ media_item.addEventListener("canplay", () => {
30
+ this.onLoaded();
31
+ });
32
+
33
+ // Load Error Event (the source element fires it, not the media element)
34
+ source_item.addEventListener("error", () => {
35
+ this.loadErrorDisplay(Language.messages.error + " " + this.options.media_name);
36
+ });
37
+
38
+ source_item.src = this.data.url;
39
+ const media_type = this._getType(this.data.url, this.data.mediatype.match_str);
40
+ if (media_type) {
41
+ source_item.type = media_type;
42
+ }
43
+ // append as a text node: re-serializing innerHTML would replace the
44
+ // source element and drop its error listener, leaving the loading
45
+ // message on screen forever
46
+ media_item.appendChild(
47
+ document.createTextNode(
48
+ "Your browser doesn't support HTML5 audio with " + source_item.type,
49
+ ),
50
+ );
51
+ this.player_element = media_item;
52
+ }
53
+
54
+ // Update Media Display
55
+ _updateMediaDisplay() {
56
+ // audio height is CSS-driven; nothing to compute here
57
+ }
58
+
59
+ _stopMedia() {
60
+ if (this.player_element) {
61
+ this.player_element.pause();
62
+ }
63
+ }
64
+
65
+ _getType(url: string, reg: string | RegExp) {
66
+ const ext = url.match(reg);
67
+ let type = "audio/";
68
+ if (!ext) {
69
+ return type;
70
+ }
71
+ switch (ext[1]) {
72
+ case "mp3":
73
+ type += "mpeg";
74
+ break;
75
+ case "wav":
76
+ type += "wav";
77
+ break;
78
+ case "m4a":
79
+ type += "mp4";
80
+ break;
81
+ default:
82
+ type = "";
83
+ break;
84
+ }
85
+ return type;
86
+ }
87
+ }
@@ -0,0 +1,40 @@
1
+ import { Media } from "../Media";
2
+ import Dom from "../../dom/Dom";
3
+ import { Language } from "../../language/Language";
4
+ import { sanitizeBlockquote } from "../EmbedUtil";
5
+
6
+ /* Media.Blockquote
7
+ ================================================== */
8
+
9
+ export default class Blockquote extends Media {
10
+ declare "media_id": string;
11
+
12
+ /* Load the media
13
+ ================================================== */
14
+ _loadMedia() {
15
+ // Loading Message
16
+ this.message.updateMessage(Language.messages.loading + " " + this.options.media_name);
17
+
18
+ // Create Dom element
19
+ this._el.content_item = Dom.create(
20
+ "div",
21
+ "vco-media-item vco-media-blockquote",
22
+ this._el.content,
23
+ );
24
+
25
+ // Get Media ID
26
+ this.media_id = this.data.url;
27
+
28
+ // The url field holds user-pasted blockquote markup. Sanitize it
29
+ // instead of injecting the raw markup, which would allow stored
30
+ // XSS via the storymap JSON.
31
+ this._el.content_item.appendChild(sanitizeBlockquote(this.media_id));
32
+
33
+ // After Loaded
34
+ this.onLoaded();
35
+ }
36
+
37
+ updateMediaDisplay() {}
38
+
39
+ _updateMediaDisplay() {}
40
+ }