@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,172 @@
|
|
|
1
|
+
import { mergeData, setData, htmlify, convertUnixTime } from "../../core/Util";
|
|
2
|
+
import { Evented, type EventedInstance } from "../../core/mixins";
|
|
3
|
+
import Dom from "../../dom/Dom";
|
|
4
|
+
|
|
5
|
+
interface TextData {
|
|
6
|
+
uniqueid?: string | null;
|
|
7
|
+
headline?: string;
|
|
8
|
+
text?: string;
|
|
9
|
+
text_align?: string;
|
|
10
|
+
date?: { created_time?: string; [key: string]: unknown } | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface TextOptions {
|
|
14
|
+
title?: boolean;
|
|
15
|
+
text_align?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class TextBase {
|
|
19
|
+
declare "_el": Record<string, HTMLElement>;
|
|
20
|
+
declare "data": TextData;
|
|
21
|
+
declare "options": TextOptions;
|
|
22
|
+
declare "fire": EventedInstance["fire"];
|
|
23
|
+
|
|
24
|
+
/* Constructor
|
|
25
|
+
================================================== */
|
|
26
|
+
constructor(data: TextData, options?: TextOptions, add_to_container?: HTMLElement) {
|
|
27
|
+
// DOM ELEMENTS
|
|
28
|
+
this._el = {
|
|
29
|
+
container: {} as HTMLElement,
|
|
30
|
+
content_container: {} as HTMLElement,
|
|
31
|
+
content: {} as HTMLElement,
|
|
32
|
+
headline: {} as HTMLElement,
|
|
33
|
+
date: {} as HTMLElement,
|
|
34
|
+
start_btn: {} as HTMLElement,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Data
|
|
38
|
+
this.data = {
|
|
39
|
+
uniqueid: "",
|
|
40
|
+
headline: "headline",
|
|
41
|
+
text: "text",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Options
|
|
45
|
+
this.options = {
|
|
46
|
+
title: false,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
setData(this, data);
|
|
50
|
+
|
|
51
|
+
// Merge Options
|
|
52
|
+
mergeData(this.options, options);
|
|
53
|
+
|
|
54
|
+
this._el.container = Dom.create("div", "vco-text");
|
|
55
|
+
this._el.container.id = this.data.uniqueid;
|
|
56
|
+
|
|
57
|
+
this._initLayout();
|
|
58
|
+
|
|
59
|
+
if (add_to_container) {
|
|
60
|
+
add_to_container.appendChild(this._el.container);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Adding, Hiding, Showing etc
|
|
65
|
+
================================================== */
|
|
66
|
+
show() {}
|
|
67
|
+
|
|
68
|
+
hide() {}
|
|
69
|
+
|
|
70
|
+
addTo(container: HTMLElement) {
|
|
71
|
+
container.appendChild(this._el.container);
|
|
72
|
+
//this.onAdd();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
removeFrom(container: HTMLElement) {
|
|
76
|
+
container.removeChild(this._el.container);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
headlineHeight() {
|
|
80
|
+
return this._el.headline.offsetHeight + 40;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
addDateText(str: string) {
|
|
84
|
+
if (this._el.date) {
|
|
85
|
+
this._el.date.innerHTML = str;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Events
|
|
90
|
+
================================================== */
|
|
91
|
+
onLoaded() {
|
|
92
|
+
this.fire("loaded", this.data);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
onAdd() {
|
|
96
|
+
this.fire("added", this.data);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
onRemove() {
|
|
100
|
+
this.fire("removed", this.data);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* Private Methods
|
|
104
|
+
================================================== */
|
|
105
|
+
_initLayout() {
|
|
106
|
+
// Create Layout
|
|
107
|
+
this._el.content_container = Dom.create(
|
|
108
|
+
"div",
|
|
109
|
+
"vco-text-content-container",
|
|
110
|
+
this._el.container,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Text alignment: per-slide override wins, then the storymap option
|
|
114
|
+
// (issue #244)
|
|
115
|
+
const align =
|
|
116
|
+
(this.data.text_align as string | undefined) ??
|
|
117
|
+
(this.options.text_align as string | undefined) ??
|
|
118
|
+
"left";
|
|
119
|
+
if (align === "center" || align === "right") {
|
|
120
|
+
this._el.content_container.classList.add("vco-text-align-" + align);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Date (only rendered when the slide has one; issue #286)
|
|
124
|
+
if (this.data.date && this.data.date.created_time && this.data.date.created_time !== "") {
|
|
125
|
+
this._el.date = Dom.create("h3", "vco-headline-date", this._el.content_container);
|
|
126
|
+
this.addDateText(convertUnixTime(this.data.date.created_time));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Headline
|
|
130
|
+
if (this.data.headline !== "") {
|
|
131
|
+
let headline_class = "vco-headline";
|
|
132
|
+
if (this.options.title) {
|
|
133
|
+
headline_class = "vco-headline vco-headline-title";
|
|
134
|
+
}
|
|
135
|
+
this._el.headline = Dom.create("h2", headline_class, this._el.content_container);
|
|
136
|
+
this._el.headline.innerHTML = this.data.headline;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Text
|
|
140
|
+
if (this.data.text !== "") {
|
|
141
|
+
let text_content = "";
|
|
142
|
+
|
|
143
|
+
text_content += htmlify(this.data.text);
|
|
144
|
+
|
|
145
|
+
// Date
|
|
146
|
+
if (
|
|
147
|
+
this.data.date &&
|
|
148
|
+
this.data.date.created_time &&
|
|
149
|
+
this.data.date.created_time !== ""
|
|
150
|
+
) {
|
|
151
|
+
if (this.data.date.created_time.length > 10) {
|
|
152
|
+
text_content +=
|
|
153
|
+
"<div class='vco-text-date'>" +
|
|
154
|
+
convertUnixTime(this.data.date.created_time) +
|
|
155
|
+
"</div>";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this._el.content = Dom.create("div", "vco-text-content", this._el.content_container);
|
|
160
|
+
this._el.content.innerHTML = text_content;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Fire event that the slide is loaded
|
|
164
|
+
this.onLoaded();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export default class Text extends Evented(TextBase) {
|
|
169
|
+
constructor(...args: ConstructorParameters<typeof TextBase>) {
|
|
170
|
+
super(...args);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Media } from "../Media";
|
|
2
|
+
import Dom from "../../dom/Dom";
|
|
3
|
+
import { Language } from "../../language/Language";
|
|
4
|
+
import { loadJSONP, uniqueGlobalName } from "../../core/Load";
|
|
5
|
+
|
|
6
|
+
/* Media.Twitter
|
|
7
|
+
Produces Twitter Display
|
|
8
|
+
================================================== */
|
|
9
|
+
|
|
10
|
+
export default class Twitter extends Media {
|
|
11
|
+
declare "user_id": string;
|
|
12
|
+
declare "media_id": string;
|
|
13
|
+
|
|
14
|
+
/* Load the media
|
|
15
|
+
================================================== */
|
|
16
|
+
_loadMedia() {
|
|
17
|
+
// Loading Message
|
|
18
|
+
this.message.updateMessage(Language.messages.loading + " " + this.options.media_name);
|
|
19
|
+
|
|
20
|
+
// Create Dom element
|
|
21
|
+
this._el.content_item = Dom.create("div", "vco-media-twitter", this._el.content);
|
|
22
|
+
|
|
23
|
+
// Get Media ID
|
|
24
|
+
|
|
25
|
+
const r = /(?:twitter\.com|x\.com)\/(.+?)\/status\/(\d+)/;
|
|
26
|
+
const match = r.exec(this.data.url);
|
|
27
|
+
if (match) {
|
|
28
|
+
this.user_id = match[1];
|
|
29
|
+
this.media_id = match[2];
|
|
30
|
+
}
|
|
31
|
+
const callbackName = uniqueGlobalName(`twitterCallback_${this.media_id}`);
|
|
32
|
+
const api_url = `https://api.twitter.com/1/statuses/oembed.json?id=${this.media_id}&include_entities=true&callback=${callbackName}`;
|
|
33
|
+
void this._fetchEmbed(api_url, callbackName);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async _fetchEmbed(api_url: string, callbackName: string) {
|
|
37
|
+
try {
|
|
38
|
+
this.createMedia(await loadJSONP<unknown>(api_url, callbackName));
|
|
39
|
+
} catch {
|
|
40
|
+
this.loadErrorDisplay("Unable to load this tweet.");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
createMedia(d: unknown) {
|
|
45
|
+
const data = d as { html: string; author_url: string; author_name: string };
|
|
46
|
+
let tweet = "",
|
|
47
|
+
tweet_text;
|
|
48
|
+
|
|
49
|
+
// TWEET CONTENT
|
|
50
|
+
tweet_text = data.html.split("</p>—")[0] + "</p></blockquote>";
|
|
51
|
+
// the oembed endpoint returns x.com URLs since the rebrand — parse both
|
|
52
|
+
const tweetuser = data.author_url.split(/twitter\.com|x\.com\//)[1];
|
|
53
|
+
const tweet_status_temp = data.html.split("</p>—")[1].split('<a href="')[1];
|
|
54
|
+
const tweet_status_url = tweet_status_temp.split('">')[0];
|
|
55
|
+
const tweet_status_date = tweet_status_temp.split('">')[1].split("</a>")[0];
|
|
56
|
+
|
|
57
|
+
// Open links in new window
|
|
58
|
+
tweet_text = tweet_text.replace(/<a href/gi, '<a target="_blank" href');
|
|
59
|
+
|
|
60
|
+
// TWEET CONTENT
|
|
61
|
+
tweet += tweet_text;
|
|
62
|
+
|
|
63
|
+
// TWEET AUTHOR
|
|
64
|
+
tweet += "<div class='vcard'>";
|
|
65
|
+
tweet +=
|
|
66
|
+
"<a href='" +
|
|
67
|
+
tweet_status_url +
|
|
68
|
+
"' class='twitter-date' target='_blank'>" +
|
|
69
|
+
tweet_status_date +
|
|
70
|
+
"</a>";
|
|
71
|
+
tweet += "<div class='author'>";
|
|
72
|
+
tweet += "<a class='screen-name url' href='" + data.author_url + "' target='_blank'>";
|
|
73
|
+
tweet += "<span class='avatar'></span>";
|
|
74
|
+
tweet +=
|
|
75
|
+
"<span class='fn'>" +
|
|
76
|
+
data.author_name +
|
|
77
|
+
" <span class='vco-icon-twitter'></span></span>";
|
|
78
|
+
tweet +=
|
|
79
|
+
"<span class='nickname'>@" +
|
|
80
|
+
tweetuser +
|
|
81
|
+
"<span class='thumbnail-inline'></span></span>";
|
|
82
|
+
tweet += "</a>";
|
|
83
|
+
tweet += "</div>";
|
|
84
|
+
tweet += "</div>";
|
|
85
|
+
|
|
86
|
+
// Add to DOM
|
|
87
|
+
this._el.content_item.innerHTML = tweet;
|
|
88
|
+
|
|
89
|
+
// After Loaded
|
|
90
|
+
this.onLoaded();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
updateMediaDisplay() {}
|
|
94
|
+
|
|
95
|
+
_updateMediaDisplay() {}
|
|
96
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
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 Video 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
|
+
"video",
|
|
20
|
+
"vco-media-item vco-media-video 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 video with " + source_item.type,
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
this.player_element = media_item;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Update Media Display
|
|
55
|
+
_updateMediaDisplay() {
|
|
56
|
+
// video 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 = "video/";
|
|
68
|
+
if (!ext) {
|
|
69
|
+
return type;
|
|
70
|
+
}
|
|
71
|
+
switch (ext[1]) {
|
|
72
|
+
case "mp4":
|
|
73
|
+
type += "mp4";
|
|
74
|
+
break;
|
|
75
|
+
case "webm":
|
|
76
|
+
type += "webm";
|
|
77
|
+
break;
|
|
78
|
+
default:
|
|
79
|
+
type = "";
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
return type;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Media } from "../Media";
|
|
2
|
+
import Dom from "../../dom/Dom";
|
|
3
|
+
import { Language } from "../../language/Language";
|
|
4
|
+
import { ratio } from "../../core/Util";
|
|
5
|
+
|
|
6
|
+
/* Media.Vimeo
|
|
7
|
+
================================================== */
|
|
8
|
+
|
|
9
|
+
export default class Vimeo extends Media {
|
|
10
|
+
declare "media_id": string;
|
|
11
|
+
declare "player": HTMLIFrameElement;
|
|
12
|
+
|
|
13
|
+
/* Load the media
|
|
14
|
+
================================================== */
|
|
15
|
+
_loadMedia() {
|
|
16
|
+
// Loading Message
|
|
17
|
+
this.message.updateMessage(Language.messages.loading + " " + this.options.media_name);
|
|
18
|
+
|
|
19
|
+
// Create Dom element
|
|
20
|
+
this._el.content_item = Dom.create(
|
|
21
|
+
"div",
|
|
22
|
+
"vco-media-item vco-media-iframe vco-media-vimeo vco-media-shadow",
|
|
23
|
+
this._el.content,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Get Media ID
|
|
27
|
+
const parts = this.data.url.split(/video\/|\/\/vimeo\.com\//);
|
|
28
|
+
if (!parts[1]) {
|
|
29
|
+
throw new Error("Invalid Vimeo URL");
|
|
30
|
+
}
|
|
31
|
+
this.media_id = parts[1].split(/[?&]/)[0];
|
|
32
|
+
|
|
33
|
+
// API URL
|
|
34
|
+
const api_url =
|
|
35
|
+
"https://player.vimeo.com/video/" +
|
|
36
|
+
this.media_id +
|
|
37
|
+
"?api=1&title=0&byline=0&portrait=0&color=ffffff";
|
|
38
|
+
|
|
39
|
+
this.player = Dom.create("iframe", "", this._el.content_item) as HTMLIFrameElement;
|
|
40
|
+
this.player.width = "100%";
|
|
41
|
+
this.player.height = "100%";
|
|
42
|
+
this.player.frameBorder = "0";
|
|
43
|
+
this.player.src = api_url;
|
|
44
|
+
|
|
45
|
+
// After Loaded
|
|
46
|
+
this.onLoaded();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Update Media Display
|
|
50
|
+
_updateMediaDisplay() {
|
|
51
|
+
this._el.content_item.style.height =
|
|
52
|
+
ratio.r16_9({ w: this._el.content_item.offsetWidth }) + "px";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_stopMedia() {
|
|
56
|
+
try {
|
|
57
|
+
this.player?.contentWindow?.postMessage(
|
|
58
|
+
JSON.stringify({ method: "pause" }),
|
|
59
|
+
"https://player.vimeo.com",
|
|
60
|
+
);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.log(err);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Media } from "../Media";
|
|
2
|
+
import Dom from "../../dom/Dom";
|
|
3
|
+
import { Language } from "../../language/Language";
|
|
4
|
+
import { validateWebURL } from "../EmbedUtil";
|
|
5
|
+
|
|
6
|
+
/* Media.Website
|
|
7
|
+
================================================= */
|
|
8
|
+
|
|
9
|
+
export default class Website extends Media {
|
|
10
|
+
declare "media_id": string;
|
|
11
|
+
|
|
12
|
+
_loadMedia() {
|
|
13
|
+
this.message.updateMessage(Language.messages.loading + " " + this.options.media_name);
|
|
14
|
+
this._el.content_item = Dom.create(
|
|
15
|
+
"div",
|
|
16
|
+
"vco-media-item vco-media-iframe",
|
|
17
|
+
this._el.content,
|
|
18
|
+
);
|
|
19
|
+
this.media_id = this.data.url;
|
|
20
|
+
// rebuild a clean iframe from a validated src: injecting the raw
|
|
21
|
+
// URL as markup would allow stored XSS via the storymap JSON
|
|
22
|
+
const src = validateWebURL(this.media_id);
|
|
23
|
+
if (!src) {
|
|
24
|
+
this.loadErrorDisplay("Invalid URL.");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const iframe = document.createElement("iframe");
|
|
28
|
+
iframe.setAttribute("src", src);
|
|
29
|
+
this._el.content_item.appendChild(iframe);
|
|
30
|
+
this.onLoaded();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_updateMediaDisplay() {
|
|
34
|
+
this._el.content_item.style.height = this.options.height + "px";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Media } from "../Media";
|
|
2
|
+
import Dom from "../../dom/Dom";
|
|
3
|
+
import { Language } from "../../language/Language";
|
|
4
|
+
import { getObjectAttributeByIndex } from "../../core/Util";
|
|
5
|
+
import { loadJSONP, uniqueGlobalName } from "../../core/Load";
|
|
6
|
+
|
|
7
|
+
/* Media.Wikipedia
|
|
8
|
+
================================================== */
|
|
9
|
+
|
|
10
|
+
const CALLBACK_PREFIX = "wikipediaCallback_";
|
|
11
|
+
const MAX_ID_LENGTH = 512 - CALLBACK_PREFIX.length;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Split a Wikipedia article URL into its decoded title and language subdomain.
|
|
15
|
+
* Underscores and percent-encoding both decode to spaces; hash fragments are
|
|
16
|
+
* stripped. Falls back to the raw path segment when decoding fails.
|
|
17
|
+
*/
|
|
18
|
+
export function parseWikipediaUrl(pageUrl: string): { title: string; language: string } {
|
|
19
|
+
const raw = pageUrl.split("wiki/")[1].split("#")[0];
|
|
20
|
+
const underscored = raw.replace(/_/g, " ");
|
|
21
|
+
let title: string;
|
|
22
|
+
try {
|
|
23
|
+
title = decodeURIComponent(underscored);
|
|
24
|
+
} catch {
|
|
25
|
+
title = underscored;
|
|
26
|
+
}
|
|
27
|
+
const language = pageUrl.split("//")[1].split(".wikipedia")[0];
|
|
28
|
+
return { title, language };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The deterministic part of the JSONP callback name for an article title. */
|
|
32
|
+
export function wikipediaCallbackBase(title: string): string {
|
|
33
|
+
return CALLBACK_PREFIX + title.replace(/[^0-9a-z]/gi, "").slice(0, MAX_ID_LENGTH);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The MediaWiki extracts API URL for a title, wrapping its answer in `callbackName`. */
|
|
37
|
+
export function wikipediaApiUrl(language: string, title: string, callbackName: string): string {
|
|
38
|
+
return `https://${language}.wikipedia.org/w/api.php?action=query&prop=extracts&redirects=&titles=${encodeURIComponent(title)}&exintro=1&format=json&callback=${callbackName}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default class Wikipedia extends Media {
|
|
42
|
+
declare "media_id": string;
|
|
43
|
+
|
|
44
|
+
/* Load the media
|
|
45
|
+
================================================== */
|
|
46
|
+
_loadMedia() {
|
|
47
|
+
// Loading Message
|
|
48
|
+
this.message.updateMessage(Language.messages.loading + " " + this.options.media_name);
|
|
49
|
+
|
|
50
|
+
// Create Dom element
|
|
51
|
+
this._el.content_item = Dom.create(
|
|
52
|
+
"div",
|
|
53
|
+
"vco-media-item vco-media-wikipedia",
|
|
54
|
+
this._el.content,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Get Media ID
|
|
58
|
+
const { title, language } = parseWikipediaUrl(this.data.url);
|
|
59
|
+
this.media_id = title;
|
|
60
|
+
// Claim a unique global slot: same-article concurrent loads must not
|
|
61
|
+
// share one (the loser's script would call a deleted global).
|
|
62
|
+
const callbackName = uniqueGlobalName(wikipediaCallbackBase(title));
|
|
63
|
+
const api_url = wikipediaApiUrl(language, title, callbackName);
|
|
64
|
+
void this._fetchExtract(api_url, callbackName);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async _fetchExtract(api_url: string, callbackName: string) {
|
|
68
|
+
try {
|
|
69
|
+
this.createMedia(await loadJSONP<unknown>(api_url, callbackName));
|
|
70
|
+
} catch {
|
|
71
|
+
this.loadErrorDisplay("Unable to load this article.");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
createMedia(d: unknown) {
|
|
76
|
+
const data = d as { query?: unknown };
|
|
77
|
+
if (data.query) {
|
|
78
|
+
let content;
|
|
79
|
+
const wiki = {
|
|
80
|
+
entry: {} as Record<string, string>,
|
|
81
|
+
title: "",
|
|
82
|
+
text: "",
|
|
83
|
+
extract: "",
|
|
84
|
+
paragraphs: 1,
|
|
85
|
+
text_array: [] as string[],
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const pages = data.query as { pages: Record<string, unknown> };
|
|
89
|
+
wiki.entry = getObjectAttributeByIndex(pages.pages, 0) as Record<string, string>;
|
|
90
|
+
wiki.extract = wiki.entry.extract;
|
|
91
|
+
wiki.title = wiki.entry.title;
|
|
92
|
+
|
|
93
|
+
if (wiki.extract.match("<p>")) {
|
|
94
|
+
wiki.text_array = wiki.extract.split("<p>");
|
|
95
|
+
} else {
|
|
96
|
+
wiki.text_array.push(wiki.extract);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < wiki.text_array.length; i++) {
|
|
100
|
+
if (i + 1 <= wiki.paragraphs && i + 1 < wiki.text_array.length) {
|
|
101
|
+
wiki.text += "<p>" + wiki.text_array[i + 1];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
content =
|
|
106
|
+
"<h4><a href='" + this.data.url + "' target='_blank'>" + wiki.title + "</a></h4>";
|
|
107
|
+
content += "<span class='wiki-source'>" + Language.messages.wikipedia + "</span>";
|
|
108
|
+
content += wiki.text;
|
|
109
|
+
|
|
110
|
+
if (wiki.extract.match("REDIRECT")) {
|
|
111
|
+
// redirect page: leave content empty
|
|
112
|
+
} else {
|
|
113
|
+
// Add to DOM
|
|
114
|
+
this._el.content_item.innerHTML = content;
|
|
115
|
+
// After Loaded
|
|
116
|
+
this.onLoaded();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
updateMediaDisplay() {}
|
|
122
|
+
|
|
123
|
+
_updateMediaDisplay() {}
|
|
124
|
+
}
|