@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,58 @@
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.DailyMotion
7
+ ================================================== */
8
+
9
+ export default class DailyMotion 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-iframe vco-media-dailymotion",
22
+ this._el.content,
23
+ );
24
+
25
+ // Get Media ID
26
+ const id = this.data.url.match("video")
27
+ ? this.data.url.split("video/")[1]
28
+ : this.data.url.split("embed/")[1];
29
+ if (!id) {
30
+ throw new Error("Invalid DailyMotion URL");
31
+ }
32
+ this.media_id = id.split(/[?&]/)[0];
33
+
34
+ // API URL
35
+ const api_url =
36
+ "https://www.dailymotion.com/embed/video/" + this.media_id + "?api=postMessage";
37
+
38
+ // API Call
39
+ this._el.content_item.innerHTML =
40
+ "<iframe autostart='false' frameborder='0' width='100%' height='100%' src='" +
41
+ api_url +
42
+ "'></iframe>";
43
+
44
+ // After Loaded
45
+ this.onLoaded();
46
+ }
47
+
48
+ // Update Media Display
49
+ _updateMediaDisplay() {
50
+ this._el.content_item.style.height =
51
+ ratio.r16_9({ w: this._el.content_item.offsetWidth }) + "px";
52
+ }
53
+
54
+ _stopMedia() {
55
+ const iframe = this._el.content_item?.querySelector("iframe") as HTMLIFrameElement | null;
56
+ iframe?.contentWindow?.postMessage('{"command":"pause","parameters":[]}', "*");
57
+ }
58
+ }
@@ -0,0 +1,41 @@
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.DocumentCloud
7
+ Embeds a DocumentCloud document viewer (issue #437)
8
+ ================================================= */
9
+
10
+ export default class DocumentCloud extends Media {
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
+ "div",
20
+ "vco-media-item vco-media-iframe vco-media-documentcloud",
21
+ this._el.content,
22
+ );
23
+
24
+ // the canonical document URL renders the standalone viewer; rebuilt
25
+ // from a validated src to keep stored XSS out of the storymap JSON
26
+ const src = validateWebURL(this.data.url);
27
+ if (!src) {
28
+ this.loadErrorDisplay("Invalid URL.");
29
+ return;
30
+ }
31
+ const iframe = document.createElement("iframe");
32
+ iframe.setAttribute("src", src);
33
+ this._el.content_item.appendChild(iframe);
34
+ this.onLoaded();
35
+ }
36
+
37
+ // Update Media Display
38
+ _updateMediaDisplay() {
39
+ this._el.content_item.style.height = this.options.height + "px";
40
+ }
41
+ }
@@ -0,0 +1,41 @@
1
+ import { Media } from "../Media";
2
+ import Dom from "../../dom/Dom";
3
+ import { Language } from "../../language/Language";
4
+
5
+ /* Media.Facebook
6
+ Embeds a Facebook post, video or reel via the plugins endpoint
7
+ (issue #272)
8
+ ================================================== */
9
+
10
+ export default class Facebook extends Media {
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
+ "div",
20
+ "vco-media-item vco-media-iframe vco-media-facebook",
21
+ this._el.content,
22
+ );
23
+
24
+ const url = this.data.url;
25
+ const href = encodeURIComponent(url);
26
+ const embed =
27
+ /\/videos?\//.test(url) || /\/watch/.test(url) || /\/reel\//.test(url)
28
+ ? `https://www.facebook.com/plugins/video.php?href=${href}&show_text=true`
29
+ : `https://www.facebook.com/plugins/post.php?href=${href}&show_text=true`;
30
+ this._el.content_item.innerHTML =
31
+ `<iframe src="${embed}" scrolling="no" frameborder="0" ` +
32
+ `allowfullscreen="true" allow="autoplay; clipboard-write; encrypted-media; ` +
33
+ `picture-in-picture; web-share"></iframe>`;
34
+ this.onLoaded();
35
+ }
36
+
37
+ // Update Media Display
38
+ _updateMediaDisplay() {
39
+ this._el.content_item.style.height = this.options.height + "px";
40
+ }
41
+ }
@@ -0,0 +1,115 @@
1
+ import { Media } from "../Media";
2
+ import Dom from "../../dom/Dom";
3
+ import { Language } from "../../language/Language";
4
+
5
+ /* Media.Flickr
6
+
7
+ ================================================== */
8
+
9
+ export default class Flickr 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
+ "img",
21
+ "vco-media-item vco-media-image vco-media-flickr vco-media-shadow",
22
+ this._el.content,
23
+ );
24
+
25
+ // Media Loaded Event
26
+ this._el.content_item.addEventListener("load", (e) => {
27
+ this.onMediaLoaded();
28
+ });
29
+
30
+ // Get Media ID
31
+ this.establishMediaID();
32
+
33
+ const api_url =
34
+ "https://api.flickr.com/services/rest/?method=flickr.photos.getSizes&api_key=" +
35
+ this.options.api_key_flickr +
36
+ "&photo_id=" +
37
+ this.media_id +
38
+ "&format=json&nojsoncallback=1";
39
+
40
+ void this._fetchSizes(api_url);
41
+ }
42
+
43
+ async _fetchSizes(api_url: string) {
44
+ try {
45
+ const response = await fetch(api_url);
46
+ const d = (await response.json()) as { stat: string };
47
+ if (d.stat === "ok") {
48
+ this.createMedia(d);
49
+ } else {
50
+ this.loadErrorDisplay("Photo not found or private.");
51
+ }
52
+ } catch {
53
+ this.loadErrorDisplay("Photo not found or private.");
54
+ }
55
+ }
56
+
57
+ establishMediaID() {
58
+ const marker = "flickr.com/photos/";
59
+ const idx = this.data.url.indexOf(marker);
60
+ if (idx === -1) {
61
+ throw "Invalid Flickr URL";
62
+ }
63
+ const pos = idx + marker.length;
64
+ this.media_id = this.data.url.slice(pos).split("/")[1];
65
+ }
66
+
67
+ createMedia(d: unknown) {
68
+ const data = d as { sizes: { size: { label: string; source: string }[] } };
69
+ const best_size = this.sizes(this.options.height);
70
+ const sizes = data?.sizes?.size;
71
+ if (!sizes || !sizes.length) {
72
+ this.loadErrorDisplay("Photo not found or private.");
73
+ return;
74
+ }
75
+ let size = sizes[Math.max(0, sizes.length - 2)].source;
76
+
77
+ for (let i = 0; i < sizes.length; i++) {
78
+ if (sizes[i].label === best_size) {
79
+ size = sizes[i].source;
80
+ }
81
+ }
82
+
83
+ // Set Image Source
84
+ (this._el.content_item as HTMLImageElement).src = size;
85
+
86
+ // After Loaded
87
+ this.onLoaded();
88
+ }
89
+
90
+ sizes(s: number): string {
91
+ let _size;
92
+
93
+ if (s <= 75) {
94
+ if (s <= 0) {
95
+ _size = "Large";
96
+ } else {
97
+ _size = "Thumbnail";
98
+ }
99
+ } else if (s <= 180) {
100
+ _size = "Small";
101
+ } else if (s <= 240) {
102
+ _size = "Small 320";
103
+ } else if (s <= 375) {
104
+ _size = "Medium";
105
+ } else if (s <= 480) {
106
+ _size = "Medium 640";
107
+ } else if (s <= 600) {
108
+ _size = "Large";
109
+ } else {
110
+ _size = "Large";
111
+ }
112
+
113
+ return _size;
114
+ }
115
+ }
@@ -0,0 +1,59 @@
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.GoogleDoc
7
+
8
+ ================================================= */
9
+
10
+ export default class GoogleDoc extends Media {
11
+ declare "media_id": string;
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",
23
+ this._el.content,
24
+ );
25
+
26
+ // Get Media ID
27
+ this.media_id = this.data.url;
28
+
29
+ // Rebuild a clean iframe from a validated src: injecting the raw
30
+ // URL into markup would allow stored XSS via the storymap JSON
31
+ const src = validateWebURL(this.media_id);
32
+ if (!src) {
33
+ this.loadErrorDisplay("Invalid URL.");
34
+ return;
35
+ }
36
+ const iframe = document.createElement("iframe");
37
+ iframe.className = "doc";
38
+ iframe.setAttribute("frameborder", "0");
39
+ iframe.setAttribute("width", "100%");
40
+ iframe.setAttribute("height", "100%");
41
+ if (this.media_id.match(/docs.google.com/i)) {
42
+ iframe.setAttribute("src", src + "&embedded=true");
43
+ } else {
44
+ iframe.setAttribute(
45
+ "src",
46
+ "http://docs.google.com/viewer?url=" + encodeURIComponent(src) + "&embedded=true",
47
+ );
48
+ }
49
+ this._el.content_item.appendChild(iframe);
50
+
51
+ // After Loaded
52
+ this.onLoaded();
53
+ }
54
+
55
+ // Update Media Display
56
+ _updateMediaDisplay() {
57
+ this._el.content_item.style.height = this.options.height + "px";
58
+ }
59
+ }
@@ -0,0 +1,51 @@
1
+ import { Media } from "../Media";
2
+ import Dom from "../../dom/Dom";
3
+ import { Language } from "../../language/Language";
4
+ import { buildIframe } from "../EmbedUtil";
5
+
6
+ /* Media.IFrame
7
+ ================================================== */
8
+
9
+ export default class IFrame 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-iframe",
22
+ this._el.content,
23
+ );
24
+
25
+ // Get Media ID
26
+ this.media_id = this.data.url;
27
+
28
+ // The url field holds a user-pasted embed snippet. Rebuild a
29
+ // clean iframe from its src instead of injecting the raw markup,
30
+ // which would allow stored XSS via the storymap JSON.
31
+ const iframe = buildIframe(this.media_id);
32
+
33
+ if (!iframe) {
34
+ this.loadErrorDisplay(
35
+ "Invalid embed code. Paste an iframe embed code with an http(s) source URL.",
36
+ );
37
+ return;
38
+ }
39
+
40
+ // API Call
41
+ this._el.content_item.appendChild(iframe);
42
+
43
+ // After Loaded
44
+ this.onLoaded();
45
+ }
46
+
47
+ // Update Media Display
48
+ _updateMediaDisplay() {
49
+ this._el.content_item.style.height = this.options.height + "px";
50
+ }
51
+ }
@@ -0,0 +1,49 @@
1
+ import { Media } from "../Media";
2
+ import Dom from "../../dom/Dom";
3
+ import { Language } from "../../language/Language";
4
+
5
+ /* Media.Image
6
+ Produces image assets.
7
+ Takes a data object and populates a dom object
8
+ ================================================== */
9
+
10
+ export default class Image extends Media {
11
+ /* Load the media
12
+ ================================================== */
13
+ _loadMedia() {
14
+ // Loading Message
15
+ this.message.updateMessage(Language.messages.loading + " " + this.options.media_name);
16
+
17
+ // Link
18
+ if (this.data.link) {
19
+ this._el.content_link = Dom.create("a", "", this._el.content);
20
+ const content_link = this._el.content_link as HTMLAnchorElement;
21
+ content_link.href = this.data.link;
22
+ content_link.target = "_blank";
23
+ this._el.content_item = Dom.create(
24
+ "img",
25
+ "vco-media-item vco-media-image vco-media-shadow",
26
+ this._el.content_link,
27
+ );
28
+ } else {
29
+ this._el.content_item = Dom.create(
30
+ "img",
31
+ "vco-media-item vco-media-image vco-media-shadow",
32
+ this._el.content,
33
+ );
34
+ }
35
+
36
+ // Media Loaded Event
37
+ this._el.content_item.addEventListener("load", (e) => {
38
+ this.onMediaLoaded();
39
+ });
40
+
41
+ (this._el.content_item as HTMLImageElement).src = this.data.url;
42
+
43
+ this.onLoaded();
44
+ }
45
+
46
+ _updateMediaDisplay(layout?: string) {
47
+ // modern browsers size media correctly without engine-specific fixes
48
+ }
49
+ }
@@ -0,0 +1,32 @@
1
+ import { Media } from "../Media";
2
+ import Dom from "../../dom/Dom";
3
+ import { Language } from "../../language/Language";
4
+
5
+ /* Media.Juxtapose
6
+ Embeds a JuxtaposeJS before/after slider (issue #360)
7
+ ================================================== */
8
+
9
+ export default class Juxtapose extends Media {
10
+ /* Load the media
11
+ ================================================== */
12
+ _loadMedia() {
13
+ // Loading Message
14
+ this.message.updateMessage(Language.messages.loading + " " + this.options.media_name);
15
+
16
+ // Create Dom element
17
+ this._el.content_item = Dom.create(
18
+ "div",
19
+ "vco-media-item vco-media-iframe vco-media-juxtapose",
20
+ this._el.content,
21
+ );
22
+
23
+ // published Juxtapose URLs are iframe-ready embeds
24
+ this._el.content_item.innerHTML = `<iframe src="${this.data.url}" />`;
25
+ this.onLoaded();
26
+ }
27
+
28
+ // Update Media Display
29
+ _updateMediaDisplay() {
30
+ this._el.content_item.style.height = this.options.height + "px";
31
+ }
32
+ }
@@ -0,0 +1,77 @@
1
+ import { Media } from "../Media";
2
+ import Dom from "../../dom/Dom";
3
+ import { Language } from "../../language/Language";
4
+
5
+ /* Media.SoundCloud
6
+ ================================================== */
7
+
8
+ /* Minimal surface of the SoundCloud widget API used here. */
9
+ interface SoundCloudWidget {
10
+ pause: () => void;
11
+ }
12
+
13
+ export default class SoundCloud extends Media {
14
+ declare "media_id": string;
15
+ declare "soundCloudCreated": boolean;
16
+ declare "widget": SoundCloudWidget;
17
+
18
+ /* Load the media
19
+ ================================================== */
20
+ async _loadMedia() {
21
+ // Loading Message
22
+ this.message.updateMessage(Language.messages.loading + " " + this.options.media_name);
23
+
24
+ // Create Dom element
25
+ this._el.content_item = Dom.create(
26
+ "div",
27
+ "vco-media-item vco-media-iframe vco-media-soundcloud vco-media-shadow",
28
+ this._el.content,
29
+ );
30
+
31
+ // Get Media ID
32
+ this.media_id = this.data.url;
33
+
34
+ // API URL
35
+ const api_url = "https://soundcloud.com/oembed?url=" + this.media_id + "&format=json";
36
+
37
+ // API Call
38
+ try {
39
+ const r = await fetch(api_url);
40
+ const d = await r.json();
41
+ await this.loadScript("https://w.soundcloud.com/player/api.js");
42
+ //load soundcloud api for pausing.
43
+ this.createMedia(d);
44
+ } catch (err) {
45
+ // aborted loads stay silent (the visitor navigated away);
46
+ // real failures show the media error display
47
+ if ((err as DOMException)?.name !== "AbortError") {
48
+ this.loadErrorDisplay("Unable to load this track.");
49
+ }
50
+ }
51
+ }
52
+
53
+ createMedia(d: unknown) {
54
+ const data = d as { html: string };
55
+ if (!data?.html) {
56
+ this.loadErrorDisplay("Unable to load this track.");
57
+ return;
58
+ }
59
+ this._el.content_item.innerHTML = data.html;
60
+
61
+ this.soundCloudCreated = true;
62
+
63
+ const sc = SC as { Widget: (iframe: Element | null) => SoundCloudWidget };
64
+ // per-instance widget: a global would be overwritten by every
65
+ // SoundCloud slide and _stopMedia would pause the wrong one
66
+ this.widget = sc.Widget(this._el.content_item.querySelector("iframe"));
67
+
68
+ // After Loaded
69
+ this.onLoaded();
70
+ }
71
+
72
+ _stopMedia() {
73
+ if (this.soundCloudCreated) {
74
+ this.widget?.pause();
75
+ }
76
+ }
77
+ }