@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,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
|
+
}
|