@projektemacher/storymapjs 0.10.5 → 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 +14 -32
  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,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>&mdash;")[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>&mdash;")[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&amp;byline=0&amp;portrait=0&amp;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
+ }