PageSourceSearch

https://betterbaseball.com/static/version1791372995/frontend/Ecomi…/Magebit_CustomEngraving/js/iframe-embed.js

js betterbaseball.com collected 2026-10-07 19:36:23 UTC 10,033 bytes, 261 lines download raw bytes

1/**
2 * @author Magebit <[email protected]>
3 * @copyright Copyright (c) Magebit, Ltd. (https://magebit.com)
4 * @license https://magebit.com/code-license
5 *
6 * Video embed (YouTube / Vimeo) behind a poster image, driven by the
7 * providers' player APIs instead of raw postMessage.
8 *
9 * Flow (same on every device, no UA sniffing):
10 *  1. The iframe is pre-mounted paused when the widget scrolls near the
11 *     viewport, so the player boots while the user is still reading.
12 *  2. Once the player reports ready, the poster stays painted on top but
13 *     turns click-transparent — the tap lands on the player's own play
14 *     button underneath. That gesture happens inside the iframe, which
15 *     every browser honors (iOS refuses gestures forwarded from the
16 *     parent page, so a programmatic play() there is not reliable).
17 *  3. The player's state events are the single source of truth: PLAYING /
18 *     BUFFERING removes the poster, step changes pause via the API.
19 *
20 * Data-driven from the root element so this file stays static:
21 *   data-embed-src - embed URL (enablejsapi/playsinline added server-side)
22 */
23(() => {
24    let ytApi = null;
25    let vimeoApi = null;
26
27    const loadYouTubeApi = () => {
28        if (window.YT && window.YT.Player) return Promise.resolve(window.YT);
29        if (ytApi) return ytApi;
30
31        ytApi = new Promise((resolve) => {
32            const previous = window.onYouTubeIframeAPIReady;
33            window.onYouTubeIframeAPIReady = () => {
34                if (typeof previous === 'function') previous();
35                resolve(window.YT);
36            };
37
38            const script = document.createElement('script');
39            script.src = 'https://www.youtube.com/iframe_api';
40            script.async = true;
41            document.head.appendChild(script);
42        });
43
44        return ytApi;
45    };
46
47    const loadVimeoApi = () => {
48        if (window.Vimeo && window.Vimeo.Player) return Promise.resolve(window.Vimeo);
49        if (vimeoApi) return vimeoApi;
50
51        vimeoApi = new Promise((resolve, reject) => {
52            const script = document.createElement('script');
53            script.src = 'https://player.vimeo.com/api/player.js';
54            script.async = true;
55            script.onload = () => resolve(window.Vimeo);
56            script.onerror = reject;
57            document.head.appendChild(script);
58        });
59
60        return vimeoApi;
61    };
62
63    /**
64     * Both adapters expose the same surface: {play, pause, destroy}.
65     * handlers.onReady(adapter) fires once the player is interactive,
66     * handlers.onPlaying() whenever playback starts / resumes buffering.
67     */
68    const createYouTubeAdapter = (iframe, handlers) => loadYouTubeApi().then((YT) => {
69        const player = new YT.Player(iframe, {
70            events: {
71                onReady: () => handlers.onReady(adapter),
72                onStateChange: (event) => {
73                    if (event.data === YT.PlayerState.PLAYING
74                        || event.data === YT.PlayerState.BUFFERING
75                    ) {
76                        handlers.onPlaying();
77                    }
78                }
79            }
80        });
81
82        const adapter = {
83            play: () => player.playVideo(),
84            pause: () => player.pauseVideo(),
85            destroy: () => player.destroy()
86        };
87
88        return adapter;
89    });
90
91    const createVimeoAdapter = (iframe, handlers) => loadVimeoApi().then((Vimeo) => {
92        const player = new Vimeo.Player(iframe);
93        const adapter = {
94            play: () => player.play().catch(() => {}),
95            pause: () => player.pause().catch(() => {}),
96            destroy: () => player.destroy().catch(() => {})
97        };
98
99        player.on('play', () => handlers.onPlaying());
100        player.ready().then(() => handlers.onReady(adapter));
101
102        return adapter;
103    });
104
105    function initIframeEmbed() {
106        return {
107            mounted: false,
108            ready: false,
109            playing: false,
110            pendingPlay: false,
111            _adapter: null,
112            _observer: null,
113            _wasVisible: false,
114            // Bumped on teardown so callbacks from a discarded player
115            // can't mutate the state of its replacement.
116            _generation: 0,
117
118            init() {
119                this._observer = new IntersectionObserver((entries) => {
120                    const e = entries[0];
121                    if (!e) return;
122
123                    // Pre-mount the paused player as the widget approaches the
124                    // viewport so it is ready before the user can tap it.
125                    if (e.isIntersecting && !this.mounted) {
126                        this.mounted = true;
127                    }
128
129                    // display:none on an ancestor (wizard step switch, modal
130                    // close) collapses our box to 0x0; scroll-out leaves it
131                    // sized. A ready player is just paused so the user can
132                    // resume on return; one still booting is torn down and
133                    // remounted fresh next time.
134                    const inLayout = e.boundingClientRect.width > 0
135                        && e.boundingClientRect.height > 0;
136
137                    if (this._wasVisible && !inLayout && this.mounted) {
138                        if (this.ready) {
139                            this._adapter && this._adapter.pause();
140                        } else {
141                            this._teardown();
142                        }
143                    }
144
145                    this._wasVisible = inLayout;
146                }, { rootMargin: '200px' });
147                this._observer.observe(this.$root);
148
149                // The observer above only fires when our box collapses to 0x0.
150                // A wizard step switch hides the step via x-show (display:none)
151                // but doesn't reliably trigger it, so pause on an explicit
152                // step-change signal too (modal close still goes via the observer).
153                this._onStepChange = () => {
154                    if (this.ready && this._adapter) {
155                        this._adapter.pause();
156                    }
157                };
158                window.addEventListener('engraving:step-changed', this._onStepChange);
159            },
160
161            destroy() {
162                if (this._observer) this._observer.disconnect();
163                if (this._onStepChange) {
164                    window.removeEventListener('engraving:step-changed', this._onStepChange);
165                }
166                this._teardown();
167            },
168
169            // Only reachable before the player is ready (afterwards the poster
170            // is click-transparent): mount if needed and remember the intent,
171            // the queued play() fires from onReady.
172            posterClick() {
173                this.pendingPlay = true;
174
175                if (!this.mounted) {
176                    this.mounted = true;
177                } else if (this.ready && this._adapter) {
178                    this._adapter.play();
179                }
180            },
181
182            onIframeLoad() {
183                const iframe = this.$root.querySelector('iframe');
184                if (!iframe) return;
185
186                const generation = ++this._generation;
187                const src = iframe.src || '';
188                const handlers = {
189                    onReady: (adapter) => {
190                        if (generation !== this._generation) return;
191                        this._adapter = adapter;
192                        this.ready = true;
193
194                        if (this.pendingPlay) {
195                            adapter.play();
196                        }
197                    },
198                    onPlaying: () => {
199                        if (generation !== this._generation) return;
200                        this.playing = true;
201                        this.pendingPlay = false;
202                    }
203                };
204
205                if (/youtube(-nocookie)?\.com/.test(src)) {
206                    createYouTubeAdapter(iframe, handlers).catch(() => {});
207                } else if (/vimeo\.com/.test(src)) {
208                    createVimeoAdapter(iframe, handlers).catch(() => {});
209                }
210            },
211
212            embedSrc() {
213                // The player is pre-mounted, so a leftover autoplay param
214                // (admin-pasted, or HTML cached before autoplay was dropped
215                // server-side) would fire a blocked play attempt whose
216                // BUFFERING event reads as "playback started" and removes
217                // the poster prematurely.
218                const src = (this.$root.dataset.embedSrc || '')
219                    .replace('?autoplay=1&', '?')
220                    .replace('&autoplay=1', '')
221                    .replace('?autoplay=1', '');
222
223                // The widget API requires origin to match the embedding page,
224                // and it can only be known client-side.
225                if (/youtube(-nocookie)?\.com/.test(src)) {
226                    return src
227                        + (src.includes('?') ? '&' : '?')
228                        + 'origin=' + encodeURIComponent(window.location.origin);
229                }
230
231                return src;
232            },
233
234            _teardown() {
235                this._generation++;
236
237                if (this._adapter) {
238                    try {
239                        this._adapter.destroy();
240                    } catch {
241                        // player already gone with its DOM node
242                    }
243                    this._adapter = null;
244                }
245
246                this.mounted = false;
247                this.ready = false;
248                this.playing = false;
249                this.pendingPlay = false;
250            }
251        };
252    }
253
254    if (window.Alpine) {
255        Alpine.data('initIframeEmbed', initIframeEmbed);
256    } else {
257        window.addEventListener('alpine:init', () => {
258            Alpine.data('initIframeEmbed', initIframeEmbed);
259        }, { once: true });
260    }
261})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.