embetter 2.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 cacheflowe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # Embetter
2
+
3
+ #### Because iframes are janky
4
+
5
+ Media embeds can quickly bog your site down, so let's lazy-load them! Embetter is a self-contained web component (`<embetter-media>`) that displays a thumbnail with a play button, then lazy-loads the actual iframe embed when clicked.
6
+
7
+ #### Demo
8
+
9
+ Check out the [demo](http://cacheflowe.github.io/embetter).
10
+
11
+ ## Installation
12
+
13
+ ### npm
14
+
15
+ ```
16
+ npm install embetter
17
+ ```
18
+
19
+ ```js
20
+ import "embetter";
21
+ ```
22
+
23
+ ### Script tag
24
+
25
+ ```html
26
+ <script type="module" src="dist/embetter-media.js"></script>
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ There are two ways to use Embetter:
32
+
33
+ ### Minimal markup (thumbnail auto-derived)
34
+
35
+ For services that can derive a thumbnail from the ID alone (YouTube, Dailymotion, Giphy, CodePen), you only need the service attribute:
36
+
37
+ ```html
38
+ <embetter-media youtube-id="l9XdkPsaynk"></embetter-media>
39
+
40
+ <embetter-media dailymotion-id="x14y6rv"></embetter-media>
41
+
42
+ <embetter-media giphy-id="26FfiRH4zeXJM7saY"></embetter-media>
43
+
44
+ <embetter-media codepen-id="cacheflowe/pen/domZpQ"></embetter-media>
45
+ ```
46
+
47
+ ### Full progressive-enhanced markup (recommended)
48
+
49
+ For the best experience, include an `<a><img>` inside the element. This serves as both a fallback for crawlers/no-JS browsers and the thumbnail source for the component. Use the URL builder (below) to generate this markup, or construct it by hand:
50
+
51
+ ```html
52
+ <!-- YouTube -->
53
+ <embetter-media youtube-id="l9XdkPsaynk">
54
+ <a href="https://www.youtube.com/watch?v=l9XdkPsaynk">
55
+ <img src="http://img.youtube.com/vi/l9XdkPsaynk/0.jpg" />
56
+ </a>
57
+ </embetter-media>
58
+
59
+ <!-- Vimeo (thumbnail must be fetched via API or provided manually) -->
60
+ <embetter-media vimeo-id="99276873">
61
+ <a href="https://vimeo.com/99276873">
62
+ <img src="https://i.vimeocdn.com/video/480405928-...-d_640" />
63
+ </a>
64
+ </embetter-media>
65
+
66
+ <!-- SoundCloud -->
67
+ <embetter-media soundcloud-id="swufm/dan-kelly-with-sinjin-hawke">
68
+ <a href="https://soundcloud.com/swufm/dan-kelly-with-sinjin-hawke">
69
+ <img src="https://i1.sndcdn.com/artworks-...-t500x500.jpg" />
70
+ </a>
71
+ </embetter-media>
72
+
73
+ <!-- Bandcamp (album/track ID and thumbnail must be scraped or provided manually) -->
74
+ <embetter-media bandcamp-id="album=462033739">
75
+ <a href="https://client03.bandcamp.com/album/testbed-assembly">
76
+ <img src="https://f4.bcbits.com/img/a2546679205_16.jpg" />
77
+ </a>
78
+ </embetter-media>
79
+
80
+ <!-- Mixcloud -->
81
+ <embetter-media mixcloud-id="Rumpel_Star/the-sound-of-the-eighties-7-a-funky-soulful-trip-through-the-80s-lost-treasures/">
82
+ <a href="https://www.mixcloud.com/Rumpel_Star/the-sound-of-the-eighties-7-a-funky-soulful-trip-through-the-80s-lost-treasures/">
83
+ <img src="https://thumbnailer.mixcloud.com/unsafe/600x600/extaudio/c/4/d/9/9235-5d20-4caf-8ec6-2f05c76dec34" />
84
+ </a>
85
+ </embetter-media>
86
+
87
+ <!-- Native video file -->
88
+ <embetter-media video-url="videos/my-video.mp4" loops>
89
+ <a href="videos/my-video.mp4">
90
+ <img src="videos/my-video-poster.jpg" />
91
+ </a>
92
+ </embetter-media>
93
+
94
+ <!-- Native GIF -->
95
+ <embetter-media gif-url="images/animation.gif">
96
+ <a href="images/animation.gif">
97
+ <img src="images/animation-poster.png" />
98
+ </a>
99
+ </embetter-media>
100
+ ```
101
+
102
+ ### Attributes
103
+
104
+ | Attribute | Description |
105
+ |----------------|-------------|
106
+ | `autoplay` | Set to `"false"` to disable autoplay on embed (default: `true`) |
107
+ | `loops` | Loop the media on completion (YouTube, Vimeo, native video) |
108
+ | `muted` | Mute the media on embed |
109
+
110
+ ### URL Builder
111
+
112
+ Build full `<embetter-media>` markup from a URL programmatically. This fetches thumbnails from service APIs and generates the complete progressive-enhanced markup:
113
+
114
+ ```js
115
+ import EmbetterMedia from "embetter";
116
+
117
+ EmbetterMedia.componentMarkupFromURL("https://www.youtube.com/watch?v=l9XdkPsaynk", (html, service) => {
118
+ // html contains the full <embetter-media> element with <a><img> fallback
119
+ document.body.insertAdjacentHTML("beforeend", html);
120
+ });
121
+ ```
122
+
123
+ ### Legacy Upgrade
124
+
125
+ If you have old Embetter embeds using the `<div class="embetter" data-*-id="...">` format, upgrade them all to web components with a single call:
126
+
127
+ ```js
128
+ import EmbetterMedia from "embetter";
129
+
130
+ EmbetterMedia.upgradeLegacyEmbeds(); // upgrades all .embetter divs on the page
131
+ EmbetterMedia.upgradeLegacyEmbeds(myContainer); // or scope to a specific container
132
+ ```
133
+
134
+ ### Mobile
135
+
136
+ On mobile devices, an IntersectionObserver automatically embeds media when it scrolls into view and unembeds it when it leaves the viewport.
137
+
138
+ ### Single Active Player
139
+
140
+ Only one embed plays at a time. Clicking a new player automatically closes the previously active one.
141
+
142
+ ## Supported Services
143
+
144
+ | Service | Attribute | Thumbnail | Notes |
145
+ |-------------|-------------------|------------------|-------|
146
+ | YouTube | `youtube-id` | Auto from ID | |
147
+ | Vimeo | `vimeo-id` | Needs `<a><img>` | Thumbnail URL fetched via Vimeo API or provided manually |
148
+ | SoundCloud | `soundcloud-id` | Needs `<a><img>` | Thumbnail via oEmbed proxy or provided manually |
149
+ | Dailymotion | `dailymotion-id` | Auto from ID | |
150
+ | Mixcloud | `mixcloud-id` | Needs `<a><img>` | Thumbnail via oEmbed proxy or provided manually |
151
+ | CodePen | `codepen-id` | Auto from ID | Format: `user/pen/slug` |
152
+ | Bandcamp | `bandcamp-id` | Needs `<a><img>` | ID is `album=123` or `track=456`; thumbnail provided manually |
153
+ | Giphy | `giphy-id` | Auto from ID | |
154
+ | Video | `video-url` | Needs `<a><img>` | Native .mp4/.mov/.m4v files |
155
+ | GIF | `gif-url` | Needs `<a><img>` | Native .gif files |
156
+
157
+ ## Development
158
+
159
+ ```
160
+ npm install
161
+ npm run dev # Start dev server
162
+ npm run build # Build for distribution
163
+ npm run preview # Preview production build
164
+ ```
@@ -0,0 +1,601 @@
1
+ let m = (
2
+ /* css */
3
+ `
4
+
5
+ :host {
6
+ --anim-speed: 0.25s;
7
+ transition: background-color var(--anim-speed) linear, max-width var(--anim-speed) linear, max-height var(--anim-speed) linear;
8
+ background-color: #000;
9
+ position: relative;
10
+ display: block;
11
+ overflow: hidden;
12
+ padding: 0;
13
+ }
14
+
15
+ :host(:hover) {
16
+ background-color: #000;
17
+ border-color: #ffc;
18
+
19
+ img {
20
+ opacity: 0.9;
21
+ transform: scale(1.02);
22
+ }
23
+ img.gif {
24
+ opacity: 1;
25
+ transform: initial;
26
+ }
27
+ }
28
+
29
+ :host([playing]) {
30
+ img {
31
+ opacity: 0;
32
+ }
33
+ img.gif {
34
+ opacity: 1;
35
+ }
36
+
37
+ .embetter-play-button {
38
+ opacity: 0;
39
+ pointer-events: none;
40
+ }
41
+
42
+ .embetter-loading {
43
+ opacity: 1;
44
+ }
45
+ }
46
+
47
+ :host([youtube-id]) {
48
+ padding-bottom: 56.25%;
49
+ height: 0;
50
+
51
+ img {
52
+ margin: -9.4% 0;
53
+ }
54
+ }
55
+
56
+ :host([soundcloud-id]),
57
+ :host([mixcloud-id]),
58
+ :host([bandcamp-id]) {
59
+ max-width: 660px;
60
+
61
+ .embetter-play-button:before {
62
+ background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2260%22%20height%3D%2260%22%20viewBox%3D%220%200%2060%2060%22%3E%3Ccircle%20cx%3D%2230%22%20cy%3D%2230%22%20r%3D%2228%22%20fill%3D%22%23010101%22/%3E%3Cpath%20fill%3D%22%23fff%22%20d%3D%22M24%2018v24l20-12z%22/%3E%3C/svg%3E');
63
+ background-size: 60px 60px;
64
+ border-radius: 50%;
65
+ }
66
+ }
67
+
68
+ a {
69
+ display: block;
70
+ line-height: 0;
71
+ margin: 0;
72
+ }
73
+
74
+ img {
75
+ transition: opacity var(--anim-speed) linear, transform var(--anim-speed) linear;
76
+ width: 100%;
77
+ margin: 0;
78
+ display: block;
79
+ }
80
+
81
+ iframe,
82
+ video,
83
+ img.gif {
84
+ position: absolute;
85
+ top: 0;
86
+ left: 0;
87
+ width: 100%;
88
+ height: 100%;
89
+ z-index: 5;
90
+ opacity: 1;
91
+ }
92
+
93
+ .embetter-play-button,
94
+ .embetter-loading {
95
+ transition: opacity 0.25s linear;
96
+ position: absolute;
97
+ top: 0;
98
+ left: 0;
99
+ width: 100%;
100
+ height: 100%;
101
+ overflow: hidden;
102
+ cursor: pointer;
103
+ }
104
+
105
+ .embetter-play-button:before {
106
+ background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2286%22%20height%3D%2260%22%20viewBox%3D%220%200%2086%2060%22%3E%3Cpath%20fill%3D%22%23010101%22%20d%3D%22M0%200h86v60h-86z%22/%3E%3Cpath%20fill%3D%22%23fff%22%20d%3D%22M35.422%2017.6v24.8l22.263-12.048z%22/%3E%3C/svg%3E');
107
+ background-repeat: no-repeat;
108
+ background-position: 50% 50%;
109
+ background-size: 33.333% auto;
110
+ width: 100%;
111
+ max-width: 258px;
112
+ height: 100%;
113
+ min-height: 100%;
114
+ content: " ";
115
+ margin: 0 auto;
116
+ display: block;
117
+ }
118
+
119
+ .embetter-loading {
120
+ background-color: #000;
121
+ opacity: 0;
122
+ display: flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ }
126
+
127
+ .embetter-loading:after {
128
+ content: "";
129
+ width: 40px;
130
+ height: 40px;
131
+ border: 4px solid rgba(255, 255, 255, 0.3);
132
+ border-top-color: #fff;
133
+ border-radius: 50%;
134
+ animation: embetter-spin 0.8s linear infinite;
135
+ }
136
+
137
+ @keyframes embetter-spin {
138
+ to { transform: rotate(360deg); }
139
+ }
140
+
141
+ `
142
+ );
143
+ class p {
144
+ static type = "youtube";
145
+ static dataAttribute = "youtube-id";
146
+ static regex = /(?:.+?)?(?:youtube\.com\/v\/|watch\/|\?v=|\&v=|youtu\.be\/|\/v=|^youtu\.be\/)([a-zA-Z0-9_-]{11})+/;
147
+ static embed(t) {
148
+ const i = t.autoplay === !0 ? "&autoplay=1" : "", e = t.loops === !0 ? `&loop=1&playlist=${t.id}` : "";
149
+ return `<iframe class="video" enablejsapi="1" width="${t.w}" height="${t.h}" src="https://www.youtube.com/embed/${t.id}?rel=0&suggestedQuality=hd720&enablejsapi=1${i}${e}" frameborder="0" scrolling="no" webkitAllowFullScreen mozallowfullscreen allowfullscreen allow=autoplay></iframe>`;
150
+ }
151
+ static thumbnail(t) {
152
+ return "http://img.youtube.com/vi/" + t + "/0.jpg";
153
+ }
154
+ static link(t) {
155
+ return "https://www.youtube.com/watch?v=" + t;
156
+ }
157
+ static buildFromText(t, i) {
158
+ const e = t.match(this.regex)[1];
159
+ if (e != null) {
160
+ const a = this.link(e), s = this.thumbnail(e);
161
+ i(e, a, s);
162
+ }
163
+ }
164
+ }
165
+ class b {
166
+ static type = "vimeo";
167
+ static dataAttribute = "vimeo-id";
168
+ // Matches vimeo.com/12345678 or player.vimeo.com/video/12345678
169
+ static regex = /(?:vimeo\.com\/(?:video\/)?|player\.vimeo\.com\/video\/)(\d+)/i;
170
+ static embed(t) {
171
+ const i = t.autoplay === !0 ? "&autoplay=1" : "", e = t.loops === !0 ? "&loop=1" : "";
172
+ return `<iframe id="${t.id}" src="https://player.vimeo.com/video/${t.id}?title=0&byline=0&portrait=0&color=ffffff&api=1&player_id=${t.id}${i}${e}" width="${t.w}" height="${t.h}" frameborder="0" scrolling="no" webkitallowfullscreen mozallowfullscreen allowfullscreen allow=autoplay></iframe>`;
173
+ }
174
+ static thumbnail(t) {
175
+ return "";
176
+ }
177
+ static link(t) {
178
+ return "https://vimeo.com/" + t;
179
+ }
180
+ static getData(t) {
181
+ return new Promise((i, e) => {
182
+ const a = `https://vimeo.com/api/v2/video/${t}.json`;
183
+ fetch(a).then((s) => s.json()).then((s) => i(s[0].thumbnail_large)).catch(() => i(""));
184
+ });
185
+ }
186
+ static buildFromText(t, i) {
187
+ const e = t.match(this.regex);
188
+ if (e && e[1]) {
189
+ const a = e[1], s = this.link(a);
190
+ this.getData(a).then((r) => {
191
+ i(a, s, r);
192
+ });
193
+ }
194
+ }
195
+ }
196
+ class g {
197
+ static type = "soundcloud";
198
+ static dataAttribute = "soundcloud-id";
199
+ static regex = /(?:https?:\/\/)?(?:w{3}\.)?(?:soundcloud\.com|snd\.sc)\/([a-zA-Z0-9_-]*(?:\/sets)?(?:\/groups)?\/[a-zA-Z0-9_-]*)/;
200
+ static embed(t) {
201
+ const i = t.autoplay === !0 ? "&auto_play=true" : "";
202
+ return `<iframe width="100%" height="600" scrolling="no" frameborder="no" src="https://w.soundcloud.com/player/?url=${encodeURIComponent(`https://soundcloud.com/${t.id}`)}${i}&hide_related=false&color=373737&show_comments=false&show_user=true&show_reposts=false&visual=true" allow="autoplay"></iframe>`;
203
+ }
204
+ static thumbnail(t) {
205
+ return "";
206
+ }
207
+ static link(t) {
208
+ return `https://soundcloud.com/${t}`;
209
+ }
210
+ static getData(t) {
211
+ const i = `/api/soundcloud?url=${encodeURIComponent(t)}&format=json`;
212
+ return fetch(i).then((e) => e.json()).then((e) => e.thumbnail_url || "").catch(() => "");
213
+ }
214
+ static buildFromText(t, i) {
215
+ const e = t.match(this.regex);
216
+ if (e && e[1]) {
217
+ const a = e[1], s = this.link(a);
218
+ this.getData(s).then((r) => {
219
+ i(a, s, r);
220
+ });
221
+ }
222
+ }
223
+ }
224
+ class A {
225
+ static type = "instagram";
226
+ static dataAttribute = "instagram-id";
227
+ static regex = /(?:https?:\/\/)?(?:w{3}\.)?(?:instagram\.com|instagr\.am)\/(p|reel|tv)\/([a-zA-Z0-9-_]+)/i;
228
+ static normalizePath(t) {
229
+ if (!t) return "p/";
230
+ const i = String(t).replace(/^\/+|\/+$/g, "");
231
+ return /^(p|reel|tv)\/[a-zA-Z0-9-_]+$/i.test(i) ? i : `p/${i}`;
232
+ }
233
+ static embed(t) {
234
+ const i = this.normalizePath(t.id), e = t.captioned === !1 ? "" : "captioned/";
235
+ return `<iframe width="100%" height="100%" scrolling="no" frameborder="0" src="https://www.instagram.com/${i}/embed/${e}?cr=1&v=14" allowfullscreen></iframe>`;
236
+ }
237
+ static thumbnail(t) {
238
+ return "";
239
+ }
240
+ static link(t) {
241
+ return `https://www.instagram.com/${this.normalizePath(t)}/`;
242
+ }
243
+ static buildFromText(t, i) {
244
+ const e = t.match(this.regex);
245
+ if (e && e[2]) {
246
+ const a = `${e[1]}/${e[2]}`, s = this.link(a);
247
+ i(a, s, "");
248
+ }
249
+ }
250
+ }
251
+ class f {
252
+ static type = "dailymotion";
253
+ static dataAttribute = "dailymotion-id";
254
+ static regex = /(?:https?:\/\/)?(?:w{3}\.)?dailymotion\.com\/video\/([a-zA-Z0-9-_]*)/;
255
+ static embed(t) {
256
+ const i = t.autoplay === !0 ? "?autoPlay=1" : "";
257
+ return `<iframe class="video" width="${t.w}" height="${t.h}" src="https://www.dailymotion.com/embed/video/${t.id}${i}" frameborder="0" scrolling="no" webkitAllowFullScreen mozallowfullscreen allowfullscreen allow=autoplay></iframe>`;
258
+ }
259
+ static thumbnail(t) {
260
+ return `https://www.dailymotion.com/thumbnail/video/${t}`;
261
+ }
262
+ static link(t) {
263
+ return `https://www.dailymotion.com/video/${t}`;
264
+ }
265
+ static buildFromText(t, i) {
266
+ t = t.split("_")[0];
267
+ const e = t.match(this.regex);
268
+ if (e && e[1]) {
269
+ const a = e[1], s = this.link(a), r = this.thumbnail(a);
270
+ i(a, s, r);
271
+ }
272
+ }
273
+ }
274
+ class y {
275
+ static type = "mixcloud";
276
+ static dataAttribute = "mixcloud-id";
277
+ static regex = /(?:https?:\/\/)?(?:w{3}\.)?(?:mixcloud\.com)\/(.*\/.*)/;
278
+ static embed(t) {
279
+ const i = t.autoplay === !0 ? "&autoplay=true" : "";
280
+ return `<iframe width="660" height="180" src="https://www.mixcloud.com/widget/iframe/?feed=${encodeURIComponent("https://www.mixcloud.com/" + t.id)}&replace=0&hide_cover=1&stylecolor=ffffff&embed_type=widget_standard${i}" frameborder="0" scrolling="no"></iframe>`;
281
+ }
282
+ static thumbnail(t) {
283
+ return "";
284
+ }
285
+ static link(t) {
286
+ return `https://www.mixcloud.com/${t}`;
287
+ }
288
+ static getData(t) {
289
+ const i = `/api/mixcloud/?url=${encodeURIComponent(t)}&format=json`;
290
+ return fetch(i).then((e) => e.json()).then((e) => e.image || "").catch(() => "");
291
+ }
292
+ static buildFromText(t, i) {
293
+ const e = t.match(this.regex);
294
+ if (e && e[1]) {
295
+ const a = e[1], s = this.link(a);
296
+ this.getData(s).then((r) => {
297
+ i(a, s, r);
298
+ });
299
+ }
300
+ }
301
+ }
302
+ class w {
303
+ static type = "codepen";
304
+ static dataAttribute = "codepen-id";
305
+ static regex = /(?:https?:\/\/)?(?:w{3}\.)?(?:codepen\.io)\/([a-zA-Z0-9_\-%]*\/[a-zA-Z0-9_\-%]*\/[a-zA-Z0-9_\-%]*)/;
306
+ static embed(t) {
307
+ const i = t.id.replace("/pen/", "/embed/"), e = i.split("/")[0], a = i.split("/")[2];
308
+ return `<iframe src="https://codepen.io/${i}?height=${t.h}&theme-id=0&slug-hash=${a}&default-tab=result&user=${e}" frameborder="0" scrolling="no" allowtransparency="true" allowfullscreen allow=autoplay></iframe>`;
309
+ }
310
+ static thumbnail(t) {
311
+ return `https://codepen.io/${t}/image/large.png`;
312
+ }
313
+ static link(t) {
314
+ return `https://codepen.io/${t.replace("/embed/", "/pen/")}`;
315
+ }
316
+ static buildFromText(t, i) {
317
+ const e = t.match(this.regex);
318
+ if (e && e[1]) {
319
+ let a = e[1].replace("/embed/", "/pen/");
320
+ const s = this.link(a), r = this.thumbnail(a);
321
+ i(a, s, r);
322
+ }
323
+ }
324
+ }
325
+ class v {
326
+ static type = "bandcamp";
327
+ static dataAttribute = "bandcamp-id";
328
+ static regex = /(?:https?:\/\/)?(?:w{3}\.)?([a-zA-Z0-9_\-]*\.bandcamp\.com\/(?:album|track)\/[a-zA-Z0-9_\-%]*)/;
329
+ static embed(t) {
330
+ return `<iframe src="https://bandcamp.com/EmbeddedPlayer/${t.id}/size=large/bgcol=ffffff/linkcol=333333/tracklist=true/artwork=small/transparent=true/" frameborder="0" scrolling="no" allowtransparency="true" allowfullscreen seamless></iframe>`;
331
+ }
332
+ static thumbnail(t) {
333
+ return "";
334
+ }
335
+ static link(t) {
336
+ return t.match(/^(album|track)=/) ? "" : `https://${t}`;
337
+ }
338
+ static getData(t) {
339
+ return fetch(`/api/bandcamp?url=${encodeURIComponent(t)}`).then((i) => i.json()).catch(() => ({ id: null, thumbnail: null }));
340
+ }
341
+ static buildFromText(t, i) {
342
+ const e = t.match(this.regex);
343
+ if (e && e[1]) {
344
+ const s = `https://${e[1]}`;
345
+ this.getData(s).then((r) => {
346
+ r.id && i(r.id, s, r.thumbnail || "");
347
+ });
348
+ }
349
+ }
350
+ }
351
+ class $ {
352
+ static type = "giphy";
353
+ static dataAttribute = "giphy-id";
354
+ static regex = /(?:https?:\/\/)?(?:w{3}\.)?giphy\.com\/gifs\/([a-zA-Z0-9_\-%]*)/;
355
+ static embed(t) {
356
+ return `<iframe width="${t.w}" height="${t.h}" src="https://giphy.com/embed/${t.id}/twitter/iframe" frameborder="0" webkitAllowFullScreen mozallowfullscreen allowfullscreen allow=autoplay></iframe>`;
357
+ }
358
+ static thumbnail(t) {
359
+ return `https://media.giphy.com/media/${t}/giphy_s.gif`;
360
+ }
361
+ static link(t) {
362
+ return `https://giphy.com/gifs/${t}`;
363
+ }
364
+ static buildFromText(t, i) {
365
+ const e = t.split("/"), a = e[e.length - 1], s = a.split("-"), r = s[s.length - 1];
366
+ if (r) {
367
+ const o = this.link(a), l = this.thumbnail(r);
368
+ i(r, o, l);
369
+ }
370
+ }
371
+ }
372
+ class k {
373
+ static type = "video";
374
+ static dataAttribute = "video-url";
375
+ static regex = /(?:https?:\/\/)?(?:w{3}\.)?(.+\.(?:mp4|mov|m4v))(?:\/|$|\s|\?|#)/;
376
+ static embed(t) {
377
+ const i = t.autoplay === !0 ? ' autoplay="true"' : "", e = t.loops === !0 ? ' loop="true"' : "", a = t.muted === !0 ? " muted" : "";
378
+ return `<video src="${t.id}" width="${t.w}" height="${t.h}"${i}${e}${a} controls playsinline webkitallowfullscreen mozallowfullscreen allowfullscreen></video>`;
379
+ }
380
+ static thumbnail(t) {
381
+ return t.replace(".mp4", "-poster.jpg").replace(".mov", "-poster.jpg").replace(".m4v", "-poster.jpg");
382
+ }
383
+ static link(t) {
384
+ return t;
385
+ }
386
+ static buildFromText(t, i) {
387
+ const e = t.match(this.regex);
388
+ if (e && e[1]) {
389
+ const a = e[1], s = this.thumbnail(a);
390
+ i(a, a, s);
391
+ }
392
+ }
393
+ }
394
+ class x {
395
+ static type = "gif";
396
+ static dataAttribute = "gif-url";
397
+ static regex = /(?:https?:\/\/)?(?:w{3}\.)?(.+\.gif)(?:\/|$|\s|\?|#)/;
398
+ static embed(t) {
399
+ return `<img class="gif" src="${t.id}" width="${t.w}" height="${t.h}">`;
400
+ }
401
+ static thumbnail(t) {
402
+ return t.replace(".gif", "-poster.jpg");
403
+ }
404
+ static link(t) {
405
+ return t;
406
+ }
407
+ static buildFromText(t, i) {
408
+ const e = t.match(this.regex);
409
+ if (e && e[1]) {
410
+ const a = e[1], s = this.thumbnail(a);
411
+ i(a, a, s);
412
+ }
413
+ }
414
+ }
415
+ const d = [
416
+ p,
417
+ b,
418
+ g,
419
+ A,
420
+ f,
421
+ y,
422
+ w,
423
+ v,
424
+ $,
425
+ // Shadertoy,
426
+ // Kuula,
427
+ k,
428
+ x
429
+ ];
430
+ class c extends HTMLElement {
431
+ defaultThumbnail = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAArwAAAGcAQMAAAABMOGrAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAADUExURQAAAKd6PdoAAAA6SURBVHja7cGBAAAAAMOg+VPf4ARVAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAN488AAGP4e1mAAAAAElFTkSuQmCC";
432
+ static EMBETTER_ACTIVATED = "embetter-activated";
433
+ connectedCallback() {
434
+ this.shadow = this.attachShadow({ mode: "open" }), this.el = this.shadow, this.initComponent(), this.render(), this.checkThumbnail(), this.addListeners(), this.setupMobileObserver();
435
+ }
436
+ disconnectedCallback() {
437
+ this.unembedMedia(), this.removeAttribute("ready"), this.playButton && this.playButton.removeEventListener("click", this.clickListener), document.removeEventListener(c.EMBETTER_ACTIVATED, this.embedListener), this.observer && (this.observer.disconnect(), this.observer = null);
438
+ }
439
+ initComponent() {
440
+ this.markup = "embetter-media component not initialized properly.", this.loops = this.hasAttribute("loops"), this.muted = this.hasAttribute("muted"), this.posterURL = null;
441
+ const t = this.querySelector("img");
442
+ t && t.src && (this.posterURL = t.src), this.innerHTML = "", this.findAndActivateService();
443
+ }
444
+ getElements() {
445
+ this.thumbnail = this.el.querySelector("img");
446
+ }
447
+ addListeners() {
448
+ this.clickListener = this.onClick.bind(this), this.playButton = this.el.querySelector(".embetter-play-button"), this.playButton && this.playButton.addEventListener("click", this.clickListener), this.embedListener = this.onEmbedActivated.bind(this), document.addEventListener(c.EMBETTER_ACTIVATED, this.embedListener);
449
+ }
450
+ onClick(t) {
451
+ t.preventDefault(), this.embedMedia();
452
+ }
453
+ onEmbedActivated(t) {
454
+ t.detail !== this && this.unembedMedia();
455
+ }
456
+ findAndActivateService() {
457
+ for (let t of d) {
458
+ let i = t.dataAttribute;
459
+ if (this.hasAttribute(i)) {
460
+ this.service = t, this.serviceType = t.type, this.serviceId = this.getAttribute(i);
461
+ let e = this.posterURL || t.thumbnail(this.serviceId);
462
+ if (this.markup = this.playerHTML(t.link(this.serviceId), e), t.getData) {
463
+ const a = t.link(this.serviceId);
464
+ t.getData(a).then((s) => {
465
+ const r = typeof s == "string" ? s : s?.thumbnail;
466
+ !e && !this.posterURL && r && this.thumbnail && (this.thumbnail.src = r);
467
+ });
468
+ }
469
+ break;
470
+ }
471
+ }
472
+ }
473
+ checkThumbnail() {
474
+ this.thumbnail && (this.setAttribute("loading", ""), this.thumbnail.onload = () => {
475
+ this.removeAttribute("loading"), this.setAttribute("ready", "");
476
+ }, this.thumbnail.onerror = () => {
477
+ this.thumbnail.src = this.defaultThumbnail, this.removeAttribute("loading"), this.setAttribute("ready", "");
478
+ }, setTimeout(() => {
479
+ this.thumbnail.height < 50 && (this.thumbnail.src = this.defaultThumbnail), this.removeAttribute("loading"), this.setAttribute("ready", "");
480
+ }, 4e3));
481
+ }
482
+ setupMobileObserver() {
483
+ this.isMobile() && (this.observer = new IntersectionObserver(
484
+ (t) => {
485
+ t.forEach((i) => {
486
+ i.isIntersecting ? this.embedMedia(!1) : this.unembedMedia();
487
+ });
488
+ },
489
+ { threshold: 0.3 }
490
+ ), this.observer.observe(this));
491
+ }
492
+ embedMedia(t) {
493
+ if (document.dispatchEvent(
494
+ new CustomEvent(c.EMBETTER_ACTIVATED, {
495
+ bubbles: !0,
496
+ composed: !0,
497
+ detail: this
498
+ })
499
+ ), this.hasAttribute("playing")) return;
500
+ t === void 0 && (t = this.hasAttribute("autoplay") ? this.getAttribute("autoplay") !== "false" : !0);
501
+ let i = this.service.embed({
502
+ id: this.serviceId,
503
+ w: this.thumbnail && this.thumbnail.width || "100%",
504
+ h: this.thumbnail && this.thumbnail.height || "100%",
505
+ autoplay: t,
506
+ loops: this.loops,
507
+ muted: this.muted || t
508
+ });
509
+ this.playerEl = c.stringToDomElement(i), this.el.appendChild(this.playerEl), this.setAttribute("playing", "");
510
+ }
511
+ unembedMedia() {
512
+ this.playerEl != null && this.playerEl.parentNode != null && this.playerEl.parentNode.removeChild(this.playerEl), this.removeAttribute("playing");
513
+ }
514
+ isMobile() {
515
+ return /iphone|ipad|ipod|android/i.test(navigator.userAgent);
516
+ }
517
+ static stringToDomElement(t) {
518
+ var i = document.createElement("div");
519
+ return i.innerHTML = t, i.firstChild;
520
+ }
521
+ static componentHTML(t, i, e = null, a = null) {
522
+ let s = "";
523
+ if (a || e) {
524
+ const r = a || "#", o = e ? `<img src="${e}">` : "";
525
+ s = `<a href="${r}">${o}</a>`;
526
+ }
527
+ return `<embetter-media ${t}="${i}">${s}</embetter-media>`;
528
+ }
529
+ playerHTML(t, i) {
530
+ return (
531
+ /* html */
532
+ `
533
+ <a href="${t}">
534
+ <img src="${i}" />
535
+ <div class="embetter-loading"></div>
536
+ <div class="embetter-play-button"></div>
537
+ </a>
538
+ `
539
+ );
540
+ }
541
+ css() {
542
+ return m;
543
+ }
544
+ html() {
545
+ return this.markup;
546
+ }
547
+ render() {
548
+ this.el.innerHTML = `
549
+ ${this.html()}
550
+ <style>${this.css()}</style>
551
+ `, this.getElements();
552
+ }
553
+ static register() {
554
+ customElements.define("embetter-media", c);
555
+ }
556
+ static upgradeLegacyEmbeds(t = document) {
557
+ t.querySelectorAll(".embetter").forEach((e) => {
558
+ for (const a of d) {
559
+ const s = `data-${a.dataAttribute}`;
560
+ if (e.hasAttribute(s)) {
561
+ const r = e.getAttribute(s), o = document.createElement("embetter-media");
562
+ o.setAttribute(a.dataAttribute, r);
563
+ const l = e.querySelector("a");
564
+ l && o.appendChild(l.cloneNode(!0)), e.hasAttribute("data-loops") && o.setAttribute("loops", ""), e.hasAttribute("data-muted") && o.setAttribute("muted", ""), e.replaceWith(o);
565
+ break;
566
+ }
567
+ }
568
+ });
569
+ }
570
+ static componentMarkupFromURL(t, i) {
571
+ for (let e = 0; e < d.length; e++) {
572
+ const a = d[e];
573
+ if (t.match(a.regex) != null) {
574
+ a.buildFromText(t, (s, r, o) => {
575
+ const l = (h) => {
576
+ let u = c.componentHTML(
577
+ a.dataAttribute,
578
+ s,
579
+ h,
580
+ r
581
+ );
582
+ i(u, a);
583
+ };
584
+ if (!o && a.getData && r) {
585
+ a.getData(r).then((h) => {
586
+ const u = typeof h == "string" ? h : h?.thumbnail;
587
+ l(u || o);
588
+ }).catch(() => l(o));
589
+ return;
590
+ }
591
+ l(o);
592
+ });
593
+ break;
594
+ }
595
+ }
596
+ }
597
+ }
598
+ c.register();
599
+ export {
600
+ c as default
601
+ };
@@ -0,0 +1,148 @@
1
+ (function(h,d){typeof exports=="object"&&typeof module<"u"?module.exports=d():typeof define=="function"&&define.amd?define(d):(h=typeof globalThis<"u"?globalThis:h||self,h.EmbetterMedia=d())})(this,(function(){"use strict";let h=`
2
+
3
+ :host {
4
+ --anim-speed: 0.25s;
5
+ transition: background-color var(--anim-speed) linear, max-width var(--anim-speed) linear, max-height var(--anim-speed) linear;
6
+ background-color: #000;
7
+ position: relative;
8
+ display: block;
9
+ overflow: hidden;
10
+ padding: 0;
11
+ }
12
+
13
+ :host(:hover) {
14
+ background-color: #000;
15
+ border-color: #ffc;
16
+
17
+ img {
18
+ opacity: 0.9;
19
+ transform: scale(1.02);
20
+ }
21
+ img.gif {
22
+ opacity: 1;
23
+ transform: initial;
24
+ }
25
+ }
26
+
27
+ :host([playing]) {
28
+ img {
29
+ opacity: 0;
30
+ }
31
+ img.gif {
32
+ opacity: 1;
33
+ }
34
+
35
+ .embetter-play-button {
36
+ opacity: 0;
37
+ pointer-events: none;
38
+ }
39
+
40
+ .embetter-loading {
41
+ opacity: 1;
42
+ }
43
+ }
44
+
45
+ :host([youtube-id]) {
46
+ padding-bottom: 56.25%;
47
+ height: 0;
48
+
49
+ img {
50
+ margin: -9.4% 0;
51
+ }
52
+ }
53
+
54
+ :host([soundcloud-id]),
55
+ :host([mixcloud-id]),
56
+ :host([bandcamp-id]) {
57
+ max-width: 660px;
58
+
59
+ .embetter-play-button:before {
60
+ background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2260%22%20height%3D%2260%22%20viewBox%3D%220%200%2060%2060%22%3E%3Ccircle%20cx%3D%2230%22%20cy%3D%2230%22%20r%3D%2228%22%20fill%3D%22%23010101%22/%3E%3Cpath%20fill%3D%22%23fff%22%20d%3D%22M24%2018v24l20-12z%22/%3E%3C/svg%3E');
61
+ background-size: 60px 60px;
62
+ border-radius: 50%;
63
+ }
64
+ }
65
+
66
+ a {
67
+ display: block;
68
+ line-height: 0;
69
+ margin: 0;
70
+ }
71
+
72
+ img {
73
+ transition: opacity var(--anim-speed) linear, transform var(--anim-speed) linear;
74
+ width: 100%;
75
+ margin: 0;
76
+ display: block;
77
+ }
78
+
79
+ iframe,
80
+ video,
81
+ img.gif {
82
+ position: absolute;
83
+ top: 0;
84
+ left: 0;
85
+ width: 100%;
86
+ height: 100%;
87
+ z-index: 5;
88
+ opacity: 1;
89
+ }
90
+
91
+ .embetter-play-button,
92
+ .embetter-loading {
93
+ transition: opacity 0.25s linear;
94
+ position: absolute;
95
+ top: 0;
96
+ left: 0;
97
+ width: 100%;
98
+ height: 100%;
99
+ overflow: hidden;
100
+ cursor: pointer;
101
+ }
102
+
103
+ .embetter-play-button:before {
104
+ background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2286%22%20height%3D%2260%22%20viewBox%3D%220%200%2086%2060%22%3E%3Cpath%20fill%3D%22%23010101%22%20d%3D%22M0%200h86v60h-86z%22/%3E%3Cpath%20fill%3D%22%23fff%22%20d%3D%22M35.422%2017.6v24.8l22.263-12.048z%22/%3E%3C/svg%3E');
105
+ background-repeat: no-repeat;
106
+ background-position: 50% 50%;
107
+ background-size: 33.333% auto;
108
+ width: 100%;
109
+ max-width: 258px;
110
+ height: 100%;
111
+ min-height: 100%;
112
+ content: " ";
113
+ margin: 0 auto;
114
+ display: block;
115
+ }
116
+
117
+ .embetter-loading {
118
+ background-color: #000;
119
+ opacity: 0;
120
+ display: flex;
121
+ align-items: center;
122
+ justify-content: center;
123
+ }
124
+
125
+ .embetter-loading:after {
126
+ content: "";
127
+ width: 40px;
128
+ height: 40px;
129
+ border: 4px solid rgba(255, 255, 255, 0.3);
130
+ border-top-color: #fff;
131
+ border-radius: 50%;
132
+ animation: embetter-spin 0.8s linear infinite;
133
+ }
134
+
135
+ @keyframes embetter-spin {
136
+ to { transform: rotate(360deg); }
137
+ }
138
+
139
+ `;class d{static type="youtube";static dataAttribute="youtube-id";static regex=/(?:.+?)?(?:youtube\.com\/v\/|watch\/|\?v=|\&v=|youtu\.be\/|\/v=|^youtu\.be\/)([a-zA-Z0-9_-]{11})+/;static embed(t){const i=t.autoplay===!0?"&autoplay=1":"",e=t.loops===!0?`&loop=1&playlist=${t.id}`:"";return`<iframe class="video" enablejsapi="1" width="${t.w}" height="${t.h}" src="https://www.youtube.com/embed/${t.id}?rel=0&suggestedQuality=hd720&enablejsapi=1${i}${e}" frameborder="0" scrolling="no" webkitAllowFullScreen mozallowfullscreen allowfullscreen allow=autoplay></iframe>`}static thumbnail(t){return"http://img.youtube.com/vi/"+t+"/0.jpg"}static link(t){return"https://www.youtube.com/watch?v="+t}static buildFromText(t,i){const e=t.match(this.regex)[1];if(e!=null){const a=this.link(e),s=this.thumbnail(e);i(e,a,s)}}}class b{static type="vimeo";static dataAttribute="vimeo-id";static regex=/(?:vimeo\.com\/(?:video\/)?|player\.vimeo\.com\/video\/)(\d+)/i;static embed(t){const i=t.autoplay===!0?"&autoplay=1":"",e=t.loops===!0?"&loop=1":"";return`<iframe id="${t.id}" src="https://player.vimeo.com/video/${t.id}?title=0&byline=0&portrait=0&color=ffffff&api=1&player_id=${t.id}${i}${e}" width="${t.w}" height="${t.h}" frameborder="0" scrolling="no" webkitallowfullscreen mozallowfullscreen allowfullscreen allow=autoplay></iframe>`}static thumbnail(t){return""}static link(t){return"https://vimeo.com/"+t}static getData(t){return new Promise((i,e)=>{const a=`https://vimeo.com/api/v2/video/${t}.json`;fetch(a).then(s=>s.json()).then(s=>i(s[0].thumbnail_large)).catch(()=>i(""))})}static buildFromText(t,i){const e=t.match(this.regex);if(e&&e[1]){const a=e[1],s=this.link(a);this.getData(a).then(r=>{i(a,s,r)})}}}class g{static type="soundcloud";static dataAttribute="soundcloud-id";static regex=/(?:https?:\/\/)?(?:w{3}\.)?(?:soundcloud\.com|snd\.sc)\/([a-zA-Z0-9_-]*(?:\/sets)?(?:\/groups)?\/[a-zA-Z0-9_-]*)/;static embed(t){const i=t.autoplay===!0?"&auto_play=true":"";return`<iframe width="100%" height="600" scrolling="no" frameborder="no" src="https://w.soundcloud.com/player/?url=${encodeURIComponent(`https://soundcloud.com/${t.id}`)}${i}&hide_related=false&color=373737&show_comments=false&show_user=true&show_reposts=false&visual=true" allow="autoplay"></iframe>`}static thumbnail(t){return""}static link(t){return`https://soundcloud.com/${t}`}static getData(t){const i=`/api/soundcloud?url=${encodeURIComponent(t)}&format=json`;return fetch(i).then(e=>e.json()).then(e=>e.thumbnail_url||"").catch(()=>"")}static buildFromText(t,i){const e=t.match(this.regex);if(e&&e[1]){const a=e[1],s=this.link(a);this.getData(s).then(r=>{i(a,s,r)})}}}class A{static type="instagram";static dataAttribute="instagram-id";static regex=/(?:https?:\/\/)?(?:w{3}\.)?(?:instagram\.com|instagr\.am)\/(p|reel|tv)\/([a-zA-Z0-9-_]+)/i;static normalizePath(t){if(!t)return"p/";const i=String(t).replace(/^\/+|\/+$/g,"");return/^(p|reel|tv)\/[a-zA-Z0-9-_]+$/i.test(i)?i:`p/${i}`}static embed(t){const i=this.normalizePath(t.id),e=t.captioned===!1?"":"captioned/";return`<iframe width="100%" height="100%" scrolling="no" frameborder="0" src="https://www.instagram.com/${i}/embed/${e}?cr=1&v=14" allowfullscreen></iframe>`}static thumbnail(t){return""}static link(t){return`https://www.instagram.com/${this.normalizePath(t)}/`}static buildFromText(t,i){const e=t.match(this.regex);if(e&&e[2]){const a=`${e[1]}/${e[2]}`,s=this.link(a);i(a,s,"")}}}class f{static type="dailymotion";static dataAttribute="dailymotion-id";static regex=/(?:https?:\/\/)?(?:w{3}\.)?dailymotion\.com\/video\/([a-zA-Z0-9-_]*)/;static embed(t){const i=t.autoplay===!0?"?autoPlay=1":"";return`<iframe class="video" width="${t.w}" height="${t.h}" src="https://www.dailymotion.com/embed/video/${t.id}${i}" frameborder="0" scrolling="no" webkitAllowFullScreen mozallowfullscreen allowfullscreen allow=autoplay></iframe>`}static thumbnail(t){return`https://www.dailymotion.com/thumbnail/video/${t}`}static link(t){return`https://www.dailymotion.com/video/${t}`}static buildFromText(t,i){t=t.split("_")[0];const e=t.match(this.regex);if(e&&e[1]){const a=e[1],s=this.link(a),r=this.thumbnail(a);i(a,s,r)}}}class y{static type="mixcloud";static dataAttribute="mixcloud-id";static regex=/(?:https?:\/\/)?(?:w{3}\.)?(?:mixcloud\.com)\/(.*\/.*)/;static embed(t){const i=t.autoplay===!0?"&autoplay=true":"";return`<iframe width="660" height="180" src="https://www.mixcloud.com/widget/iframe/?feed=${encodeURIComponent("https://www.mixcloud.com/"+t.id)}&replace=0&hide_cover=1&stylecolor=ffffff&embed_type=widget_standard${i}" frameborder="0" scrolling="no"></iframe>`}static thumbnail(t){return""}static link(t){return`https://www.mixcloud.com/${t}`}static getData(t){const i=`/api/mixcloud/?url=${encodeURIComponent(t)}&format=json`;return fetch(i).then(e=>e.json()).then(e=>e.image||"").catch(()=>"")}static buildFromText(t,i){const e=t.match(this.regex);if(e&&e[1]){const a=e[1],s=this.link(a);this.getData(s).then(r=>{i(a,s,r)})}}}class w{static type="codepen";static dataAttribute="codepen-id";static regex=/(?:https?:\/\/)?(?:w{3}\.)?(?:codepen\.io)\/([a-zA-Z0-9_\-%]*\/[a-zA-Z0-9_\-%]*\/[a-zA-Z0-9_\-%]*)/;static embed(t){const i=t.id.replace("/pen/","/embed/"),e=i.split("/")[0],a=i.split("/")[2];return`<iframe src="https://codepen.io/${i}?height=${t.h}&theme-id=0&slug-hash=${a}&default-tab=result&user=${e}" frameborder="0" scrolling="no" allowtransparency="true" allowfullscreen allow=autoplay></iframe>`}static thumbnail(t){return`https://codepen.io/${t}/image/large.png`}static link(t){return`https://codepen.io/${t.replace("/embed/","/pen/")}`}static buildFromText(t,i){const e=t.match(this.regex);if(e&&e[1]){let a=e[1].replace("/embed/","/pen/");const s=this.link(a),r=this.thumbnail(a);i(a,s,r)}}}class v{static type="bandcamp";static dataAttribute="bandcamp-id";static regex=/(?:https?:\/\/)?(?:w{3}\.)?([a-zA-Z0-9_\-]*\.bandcamp\.com\/(?:album|track)\/[a-zA-Z0-9_\-%]*)/;static embed(t){return`<iframe src="https://bandcamp.com/EmbeddedPlayer/${t.id}/size=large/bgcol=ffffff/linkcol=333333/tracklist=true/artwork=small/transparent=true/" frameborder="0" scrolling="no" allowtransparency="true" allowfullscreen seamless></iframe>`}static thumbnail(t){return""}static link(t){return t.match(/^(album|track)=/)?"":`https://${t}`}static getData(t){return fetch(`/api/bandcamp?url=${encodeURIComponent(t)}`).then(i=>i.json()).catch(()=>({id:null,thumbnail:null}))}static buildFromText(t,i){const e=t.match(this.regex);if(e&&e[1]){const s=`https://${e[1]}`;this.getData(s).then(r=>{r.id&&i(r.id,s,r.thumbnail||"")})}}}class ${static type="giphy";static dataAttribute="giphy-id";static regex=/(?:https?:\/\/)?(?:w{3}\.)?giphy\.com\/gifs\/([a-zA-Z0-9_\-%]*)/;static embed(t){return`<iframe width="${t.w}" height="${t.h}" src="https://giphy.com/embed/${t.id}/twitter/iframe" frameborder="0" webkitAllowFullScreen mozallowfullscreen allowfullscreen allow=autoplay></iframe>`}static thumbnail(t){return`https://media.giphy.com/media/${t}/giphy_s.gif`}static link(t){return`https://giphy.com/gifs/${t}`}static buildFromText(t,i){const e=t.split("/"),a=e[e.length-1],s=a.split("-"),r=s[s.length-1];if(r){const o=this.link(a),c=this.thumbnail(r);i(r,o,c)}}}class k{static type="video";static dataAttribute="video-url";static regex=/(?:https?:\/\/)?(?:w{3}\.)?(.+\.(?:mp4|mov|m4v))(?:\/|$|\s|\?|#)/;static embed(t){const i=t.autoplay===!0?' autoplay="true"':"",e=t.loops===!0?' loop="true"':"",a=t.muted===!0?" muted":"";return`<video src="${t.id}" width="${t.w}" height="${t.h}"${i}${e}${a} controls playsinline webkitallowfullscreen mozallowfullscreen allowfullscreen></video>`}static thumbnail(t){return t.replace(".mp4","-poster.jpg").replace(".mov","-poster.jpg").replace(".m4v","-poster.jpg")}static link(t){return t}static buildFromText(t,i){const e=t.match(this.regex);if(e&&e[1]){const a=e[1],s=this.thumbnail(a);i(a,a,s)}}}class x{static type="gif";static dataAttribute="gif-url";static regex=/(?:https?:\/\/)?(?:w{3}\.)?(.+\.gif)(?:\/|$|\s|\?|#)/;static embed(t){return`<img class="gif" src="${t.id}" width="${t.w}" height="${t.h}">`}static thumbnail(t){return t.replace(".gif","-poster.jpg")}static link(t){return t}static buildFromText(t,i){const e=t.match(this.regex);if(e&&e[1]){const a=e[1],s=this.thumbnail(a);i(a,a,s)}}}const m=[d,b,g,A,f,y,w,v,$,k,x];class l extends HTMLElement{defaultThumbnail="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAArwAAAGcAQMAAAABMOGrAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAADUExURQAAAKd6PdoAAAA6SURBVHja7cGBAAAAAMOg+VPf4ARVAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAN488AAGP4e1mAAAAAElFTkSuQmCC";static EMBETTER_ACTIVATED="embetter-activated";connectedCallback(){this.shadow=this.attachShadow({mode:"open"}),this.el=this.shadow,this.initComponent(),this.render(),this.checkThumbnail(),this.addListeners(),this.setupMobileObserver()}disconnectedCallback(){this.unembedMedia(),this.removeAttribute("ready"),this.playButton&&this.playButton.removeEventListener("click",this.clickListener),document.removeEventListener(l.EMBETTER_ACTIVATED,this.embedListener),this.observer&&(this.observer.disconnect(),this.observer=null)}initComponent(){this.markup="embetter-media component not initialized properly.",this.loops=this.hasAttribute("loops"),this.muted=this.hasAttribute("muted"),this.posterURL=null;const t=this.querySelector("img");t&&t.src&&(this.posterURL=t.src),this.innerHTML="",this.findAndActivateService()}getElements(){this.thumbnail=this.el.querySelector("img")}addListeners(){this.clickListener=this.onClick.bind(this),this.playButton=this.el.querySelector(".embetter-play-button"),this.playButton&&this.playButton.addEventListener("click",this.clickListener),this.embedListener=this.onEmbedActivated.bind(this),document.addEventListener(l.EMBETTER_ACTIVATED,this.embedListener)}onClick(t){t.preventDefault(),this.embedMedia()}onEmbedActivated(t){t.detail!==this&&this.unembedMedia()}findAndActivateService(){for(let t of m){let i=t.dataAttribute;if(this.hasAttribute(i)){this.service=t,this.serviceType=t.type,this.serviceId=this.getAttribute(i);let e=this.posterURL||t.thumbnail(this.serviceId);if(this.markup=this.playerHTML(t.link(this.serviceId),e),t.getData){const a=t.link(this.serviceId);t.getData(a).then(s=>{const r=typeof s=="string"?s:s?.thumbnail;!e&&!this.posterURL&&r&&this.thumbnail&&(this.thumbnail.src=r)})}break}}}checkThumbnail(){this.thumbnail&&(this.setAttribute("loading",""),this.thumbnail.onload=()=>{this.removeAttribute("loading"),this.setAttribute("ready","")},this.thumbnail.onerror=()=>{this.thumbnail.src=this.defaultThumbnail,this.removeAttribute("loading"),this.setAttribute("ready","")},setTimeout(()=>{this.thumbnail.height<50&&(this.thumbnail.src=this.defaultThumbnail),this.removeAttribute("loading"),this.setAttribute("ready","")},4e3))}setupMobileObserver(){this.isMobile()&&(this.observer=new IntersectionObserver(t=>{t.forEach(i=>{i.isIntersecting?this.embedMedia(!1):this.unembedMedia()})},{threshold:.3}),this.observer.observe(this))}embedMedia(t){if(document.dispatchEvent(new CustomEvent(l.EMBETTER_ACTIVATED,{bubbles:!0,composed:!0,detail:this})),this.hasAttribute("playing"))return;t===void 0&&(t=this.hasAttribute("autoplay")?this.getAttribute("autoplay")!=="false":!0);let i=this.service.embed({id:this.serviceId,w:this.thumbnail&&this.thumbnail.width||"100%",h:this.thumbnail&&this.thumbnail.height||"100%",autoplay:t,loops:this.loops,muted:this.muted||t});this.playerEl=l.stringToDomElement(i),this.el.appendChild(this.playerEl),this.setAttribute("playing","")}unembedMedia(){this.playerEl!=null&&this.playerEl.parentNode!=null&&this.playerEl.parentNode.removeChild(this.playerEl),this.removeAttribute("playing")}isMobile(){return/iphone|ipad|ipod|android/i.test(navigator.userAgent)}static stringToDomElement(t){var i=document.createElement("div");return i.innerHTML=t,i.firstChild}static componentHTML(t,i,e=null,a=null){let s="";if(a||e){const r=a||"#",o=e?`<img src="${e}">`:"";s=`<a href="${r}">${o}</a>`}return`<embetter-media ${t}="${i}">${s}</embetter-media>`}playerHTML(t,i){return`
140
+ <a href="${t}">
141
+ <img src="${i}" />
142
+ <div class="embetter-loading"></div>
143
+ <div class="embetter-play-button"></div>
144
+ </a>
145
+ `}css(){return h}html(){return this.markup}render(){this.el.innerHTML=`
146
+ ${this.html()}
147
+ <style>${this.css()}</style>
148
+ `,this.getElements()}static register(){customElements.define("embetter-media",l)}static upgradeLegacyEmbeds(t=document){t.querySelectorAll(".embetter").forEach(e=>{for(const a of m){const s=`data-${a.dataAttribute}`;if(e.hasAttribute(s)){const r=e.getAttribute(s),o=document.createElement("embetter-media");o.setAttribute(a.dataAttribute,r);const c=e.querySelector("a");c&&o.appendChild(c.cloneNode(!0)),e.hasAttribute("data-loops")&&o.setAttribute("loops",""),e.hasAttribute("data-muted")&&o.setAttribute("muted",""),e.replaceWith(o);break}}})}static componentMarkupFromURL(t,i){for(let e=0;e<m.length;e++){const a=m[e];if(t.match(a.regex)!=null){a.buildFromText(t,(s,r,o)=>{const c=u=>{let p=l.componentHTML(a.dataAttribute,s,u,r);i(p,a)};if(!o&&a.getData&&r){a.getData(r).then(u=>{const p=typeof u=="string"?u:u?.thumbnail;c(p||o)}).catch(()=>c(o));return}c(o)});break}}}}return l.register(),l}));
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "embetter",
3
+ "version": "2.0.0",
4
+ "type": "module",
5
+ "description": "Lazy-loading iframe embed web component",
6
+ "main": "dist/embetter-media.umd.js",
7
+ "module": "dist/embetter-media.js",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/embetter-media.js",
11
+ "require": "./dist/embetter-media.umd.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "keywords": [
18
+ "embed",
19
+ "iframe",
20
+ "lazy-load",
21
+ "web-component",
22
+ "youtube",
23
+ "vimeo",
24
+ "soundcloud",
25
+ "bandcamp",
26
+ "video"
27
+ ],
28
+ "scripts": {
29
+ "dev": "vite",
30
+ "build": "vite build",
31
+ "preview": "vite preview",
32
+ "prepublishOnly": "npm run build",
33
+ "update-libs": "npx npm-check-updates -u"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/cacheflowe/embetter.git"
38
+ },
39
+ "author": "cacheflowe",
40
+ "license": "MIT",
41
+ "bugs": {
42
+ "url": "https://github.com/cacheflowe/embetter/issues"
43
+ },
44
+ "homepage": "https://github.com/cacheflowe/embetter#readme",
45
+ "devDependencies": {
46
+ "@vitejs/plugin-basic-ssl": "^2.1.4",
47
+ "vite": "^7.3.1"
48
+ }
49
+ }