PageSourceSearch

https://m4bl.org/wp-content/plugins/embedpress/assets/js/ep-yt-queue.js?ver=1789487342

js m4bl.org collected 2026-09-24 08:42:37 UTC 21,668 bytes, 509 lines download raw bytes

1/**
2 * EmbedPress — YouTube Playlist (Queue + Theatre)
3 *
4 * Wires up the .ep-yt-playlist shells emitted by
5 * EmbedPress\Providers\TemplateLayouts\YoutubeLayout (queue + theatre).
6 * Per-layout selectors differ (queue uses .ep-yt-queue__*, theatre uses
7 * .ep-yt-theatre__*) but the interaction model is the same:
8 *   - click-to-swap: clicking an item/card swaps the iframe src to that vid,
9 *     updates the active highlight and the "N / M" position counter
10 *   - infinite scroll: when scrolled near the end of the list/strip,
11 *     fetches the next page of items via WP REST and appends them
12 *   - loop: when ended (postMessage from YT iframe), advance to next or
13 *     restart from 1 if loop is on
14 *   - shuffle: randomize playback order; toggling off restores natural order
15 *   - theatre extras: prev/next arrow buttons scroll the card strip
16 *
17 * No framework dependency — vanilla DOM. Safe to enqueue multiple times;
18 * the per-instance `init` guard prevents double-binding.
19 */
20(function () {
21    'use strict';
22
23    var ROOT_SELECTOR      = '.ep-yt-playlist';
24    var INFINITE_SCROLL_PX = 120;
25    var INIT_FLAG          = 'epYtPlaylistInit';
26
27    // Per-layout selector tables — keep markup contracts in one place.
28    // New Pro layouts plug in here without any other JS change.
29    var SELECTORS = {
30        queue: {
31            iframe:     '.ep-yt-queue__iframe',
32            list:       '.ep-yt-queue__items',
33            item:       '.ep-yt-queue__item',
34            activeItem: '.ep-yt-queue__item.is-active',
35            loader:     '.ep-yt-queue__loader',
36            loopBtn:    '.ep-yt-queue__loop',
37            shuffleBtn: '.ep-yt-queue__shuffle',
38            posCurrent: '.ep-yt-queue__position-current',
39            scrollAxis: 'y'
40        },
41        theatre: {
42            iframe:     '.ep-yt-theatre__iframe',
43            list:       '.ep-yt-theatre__strip',
44            item:       '.ep-yt-theatre__card',
45            activeItem: '.ep-yt-theatre__card.is-active',
46            loader:     '.ep-yt-theatre__loader',
47            loopBtn:    '.ep-yt-theatre__loop',
48            shuffleBtn: '.ep-yt-theatre__shuffle',
49            posCurrent: '.ep-yt-theatre__position-current',
50            navPrev:    '.ep-yt-theatre__nav--prev',
51            navNext:    '.ep-yt-theatre__nav--next',
52            scrollAxis: 'x'
53        },
54        // Library: no inline player. Clicking a card opens the modal.
55        library: {
56            iframe:     '.ep-yt-library__modal-iframe',
57            list:       '.ep-yt-library__grid',
58            item:       '.ep-yt-library__card',
59            activeItem: '.ep-yt-library__card.is-active',
60            loader:     '.ep-yt-library__loader',
61            modal:      '.ep-yt-library__modal',
62            modalClose: '.ep-yt-library__modal-close',
63            posCurrent: '.ep-yt-library__position-current',
64            scrollAxis: 'y'
65        },
66        spotlight: {
67            iframe:     '.ep-yt-spotlight__iframe',
68            list:       '.ep-yt-spotlight__rail',
69            item:       '.ep-yt-spotlight__rail-card',
70            activeItem: '.ep-yt-spotlight__rail-card.is-active',
71            loader:     '.ep-yt-spotlight__loader',
72            loopBtn:    '.ep-yt-spotlight__loop',
73            shuffleBtn: '.ep-yt-spotlight__shuffle',
74            posCurrent: '.ep-yt-spotlight__position-current',
75            heroTitle:  '.ep-yt-spotlight__hero-title',
76            heroDesc:   '.ep-yt-spotlight__hero-desc',
77            scrollAxis: 'y'
78        },
79        cinema: {
80            iframe:     '.ep-yt-cinema__iframe',
81            list:       '.ep-yt-cinema__items',
82            item:       '.ep-yt-cinema__item',
83            activeItem: '.ep-yt-cinema__item.is-active',
84            loader:     '.ep-yt-cinema__loader',
85            loopBtn:    '.ep-yt-cinema__loop',
86            shuffleBtn: '.ep-yt-cinema__shuffle',
87            posCurrent: '.ep-yt-cinema__position-current',
88            playPrev:     '.ep-yt-cinema__prev',
89            playNext:     '.ep-yt-cinema__next',
90            overlayOpen:  '.ep-yt-cinema__open-queue',
91            overlayClose: '.ep-yt-cinema__close-queue',
92            overlay:      '.ep-yt-cinema__overlay',
93            scrollAxis: 'y'
94        },
95        magazine: {
96            iframe:     '.ep-yt-magazine__iframe',
97            list:       '.ep-yt-magazine__rail',
98            item:       '.ep-yt-magazine__rail-item',
99            activeItem: '.ep-yt-magazine__rail-item.is-active',
100            loader:     '.ep-yt-magazine__loader',
101            loopBtn:    '.ep-yt-magazine__loop',
102            shuffleBtn: '.ep-yt-magazine__shuffle',
103            posCurrent: '.ep-yt-magazine__position-current',
104            heroTitle:  '.ep-yt-magazine__hero-title',
105            heroDesc:   '.ep-yt-magazine__hero-desc',
106            readMore:   '.ep-yt-magazine__readmore',
107            scrollAxis: 'y'
108        }
109    };
110
111    function ready(fn) {
112        if (document.readyState !== 'loading') fn();
113        else document.addEventListener('DOMContentLoaded', fn);
114    }
115    function $$(root, sel) { return Array.prototype.slice.call(root.querySelectorAll(sel)); }
116    function $(root, sel) { return root.querySelector(sel); }
117
118    function getLayout(root) {
119        var l = root.getAttribute('data-layout') || 'queue';
120        return SELECTORS[l] ? l : 'queue';
121    }
122
123    function swapVideoId(iframe, vid) {
124        if (!iframe || !vid) return;
125        try {
126            var src = iframe.src;
127            var next = src.replace(/(\/embed\/)([^?&"'>]+)(.*)?/, '$1' + vid + '$3');
128            if (/[?&]autoplay=0/.test(next)) {
129                next = next.replace(/([?&])autoplay=0/, '$1autoplay=1');
130            } else if (next.indexOf('autoplay=') === -1) {
131                next += (next.indexOf('?') === -1 ? '?' : '&') + 'autoplay=1';
132            }
133            iframe.src = next;
134        } catch (err) { /* ignore */ }
135    }
136
137    function setActive(root, sel, item) {
138        $$(root, sel.activeItem).forEach(function (el) { el.classList.remove('is-active'); });
139        if (item) {
140            item.classList.add('is-active');
141            var list = $(root, sel.list);
142            if (list && typeof item.scrollIntoView === 'function') {
143                var listRect = list.getBoundingClientRect();
144                var itemRect = item.getBoundingClientRect();
145                var off = sel.scrollAxis === 'x'
146                    ? (itemRect.left < listRect.left || itemRect.right > listRect.right)
147                    : (itemRect.top < listRect.top || itemRect.bottom > listRect.bottom);
148                if (off) {
149                    item.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'smooth' });
150                }
151            }
152        }
153    }
154
155    function updateCounter(root, sel) {
156        var current = $(root, sel.posCurrent);
157        var active  = $(root, sel.activeItem);
158        if (current && active) {
159            current.textContent = active.getAttribute('data-index') || '';
160        }
161    }
162
163    function playByIndex(root, sel, index, state) {
164        var items = $$(root, sel.item);
165        if (!items.length) return;
166        if (index >= items.length) {
167            if (state.loop) index = 0;
168            else return;
169        }
170        if (index < 0) index = 0;
171        var item   = items[index];
172        var iframe = $(root, sel.iframe);
173        var vid    = item.getAttribute('data-vid');
174        if (!vid || !iframe) return;
175        swapVideoId(iframe, vid);
176        setActive(root, sel, item);
177        updateCounter(root, sel);
178    }
179
180    function visualIndex(root, sel, item) {
181        var items = $$(root, sel.item);
182        return items.indexOf(item);
183    }
184
185    function attachClickToSwap(root, sel, state) {
186        var list = $(root, sel.list);
187        if (!list) return;
188        list.addEventListener('click', function (event) {
189            var item = event.target.closest(sel.item);
190            if (!item || !list.contains(item)) return;
191            event.preventDefault();
192            state.cursor = visualIndex(root, sel, item);
193            playByIndex(root, sel, state.cursor, state);
194            // Layouts that have a "hero" area (Spotlight, Magazine) promote
195            // the clicked item's title + description into that area.
196            if (sel.heroTitle || sel.heroDesc) {
197                var titleEl = sel.heroTitle ? $(root, sel.heroTitle) : null;
198                var descEl  = sel.heroDesc  ? $(root, sel.heroDesc)  : null;
199                if (titleEl) {
200                    var t = item.getAttribute('data-title') || '';
201                    if (t) titleEl.textContent = t;
202                }
203                if (descEl) {
204                    var d = item.getAttribute('data-description') || '';
205                    if (d !== '') descEl.textContent = d;
206                }
207            }
208            // Library: open the modal player on click.
209            if (sel.modal && state.layout === 'library') {
210                var modal = $(root, sel.modal);
211                if (modal) modal.hidden = false;
212            }
213        });
214    }
215
216    // Cinema overlay queue + Library modal close wiring.
217    function attachLayoutExtras(root, sel, state) {
218        if (state.layout === 'cinema') {
219            var openBtn  = $(root, sel.overlayOpen);
220            var closeBtn = $(root, sel.overlayClose);
221            var overlay  = $(root, sel.overlay);
222            if (openBtn && overlay) openBtn.addEventListener('click', function () { overlay.classList.add('is-open'); });
223            if (closeBtn && overlay) closeBtn.addEventListener('click', function () { overlay.classList.remove('is-open'); });
224        }
225        if (state.layout === 'library') {
226            var modal = $(root, sel.modal);
227            var close = $(root, sel.modalClose);
228            if (close && modal) close.addEventListener('click', function () { modal.hidden = true; });
229            if (modal) modal.addEventListener('click', function (event) {
230                if (event.target === modal) modal.hidden = true; // backdrop click
231            });
232        }
233        if (state.layout === 'magazine') {
234            var more = $(root, sel.readMore);
235            var desc = $(root, sel.heroDesc);
236            if (more && desc) more.addEventListener('click', function () {
237                desc.classList.toggle('is-expanded');
238                more.textContent = desc.classList.contains('is-expanded')
239                    ? (more.getAttribute('data-collapse-label') || 'Show less')
240                    : (more.getAttribute('data-expand-label')   || 'Read more');
241            });
242        }
243    }
244
245    function attachLoopShuffle(root, sel, state) {
246        var loopBtn    = $(root, sel.loopBtn);
247        var shuffleBtn = $(root, sel.shuffleBtn);
248        if (loopBtn) {
249            loopBtn.addEventListener('click', function () {
250                state.loop = !state.loop;
251                loopBtn.setAttribute('aria-pressed', String(state.loop));
252            });
253        }
254        if (shuffleBtn) {
255            shuffleBtn.addEventListener('click', function () {
256                state.shuffle = !state.shuffle;
257                shuffleBtn.setAttribute('aria-pressed', String(state.shuffle));
258                if (state.shuffle) shuffleOrder(root, sel, state);
259                else restoreOrder(root, sel, state);
260            });
261        }
262    }
263
264    function shuffleOrder(root, sel, state) {
265        var list = $(root, sel.list);
266        if (!list) return;
267        var items = $$(root, sel.item);
268        if (!state.originalOrder) {
269            state.originalOrder = items.map(function (i) { return i.getAttribute('data-vid'); });
270        }
271        var shuffled = items.slice();
272        for (var i = shuffled.length - 1; i > 0; i--) {
273            var j = Math.floor(Math.random() * (i + 1));
274            var tmp = shuffled[i]; shuffled[i] = shuffled[j]; shuffled[j] = tmp;
275        }
276        shuffled.forEach(function (el) { list.appendChild(el); });
277        var active = $(root, sel.activeItem);
278        state.cursor = visualIndex(root, sel, active);
279    }
280
281    function restoreOrder(root, sel, state) {
282        if (!state.originalOrder) return;
283        var list = $(root, sel.list);
284        if (!list) return;
285        var byVid = {};
286        $$(root, sel.item).forEach(function (el) {
287            byVid[el.getAttribute('data-vid')] = el;
288        });
289        state.originalOrder.forEach(function (vid) {
290            if (byVid[vid]) list.appendChild(byVid[vid]);
291        });
292        var active = $(root, sel.activeItem);
293        state.cursor = visualIndex(root, sel, active);
294    }
295
296    function attachInfiniteScroll(root, sel, state) {
297        var list = $(root, sel.list);
298        if (!list) return;
299        list.addEventListener('scroll', function () {
300            if (state.loadingNext || !state.nextPageToken) return;
301            var remaining = sel.scrollAxis === 'x'
302                ? list.scrollWidth - list.scrollLeft - list.clientWidth
303                : list.scrollHeight - list.scrollTop - list.clientHeight;
304            if (remaining < INFINITE_SCROLL_PX) {
305                fetchNextPage(root, sel, state);
306            }
307        });
308
309        // Hard cap on initial-fill rounds — protects against pathological
310        // cases where the container grows along with the strip and the
311        // "is it scrollable yet?" predicate never flips to false.
312        var fillRounds = 0;
313        function fillUntilScrollable() {
314            if (!state.nextPageToken || state.loadingNext) return;
315            if (fillRounds >= 3) return;
316            var room = sel.scrollAxis === 'x'
317                ? list.scrollWidth <= list.clientWidth + 8
318                : list.scrollHeight <= list.clientHeight + 8;
319            if (room) {
320                fillRounds++;
321                fetchNextPage(root, sel, state).then(function () {
322                    setTimeout(fillUntilScrollable, 50);
323                });
324            }
325        }
326        setTimeout(fillUntilScrollable, 50);
327    }
328
329    // Playback prev/next buttons (cinema): step the active video backward /
330    // forward through the playlist, reusing the same cursor model as auto-loop.
331    // Distinct from theatre's nav buttons, which scroll the card strip.
332    function attachPlaybackNavButtons(root, sel, state) {
333        var prev = sel.playPrev ? $(root, sel.playPrev) : null;
334        var next = sel.playNext ? $(root, sel.playNext) : null;
335        function step(delta) {
336            var items = $$(root, sel.item);
337            if (!items.length) return;
338            var active = $(root, sel.activeItem);
339            state.cursor = active ? visualIndex(root, sel, active) : state.cursor;
340            var target = state.cursor + delta;
341            if (target >= items.length) target = state.loop ? 0 : items.length - 1;
342            if (target < 0) target = state.loop ? items.length - 1 : 0;
343            state.cursor = target;
344            playByIndex(root, sel, target, state);
345        }
346        if (prev) prev.addEventListener('click', function () { step(-1); });
347        if (next) next.addEventListener('click', function () { step(1); });
348    }
349
350    function attachTheatreNavButtons(root, sel) {
351        var prev = $(root, sel.navPrev);
352        var next = $(root, sel.navNext);
353        var list = $(root, sel.list);
354        if (!list) return;
355        function pageBy(dir) {
356            list.scrollBy({ left: dir * list.clientWidth * 0.8, behavior: 'smooth' });
357        }
358        if (prev) prev.addEventListener('click', function () { pageBy(-1); });
359        if (next) next.addEventListener('click', function () { pageBy(1); });
360    }
361
362    function fetchNextPage(root, sel, state) {
363        if (state.loadingNext || !state.nextPageToken) return Promise.resolve();
364        state.loadingNext = true;
365        var loader = $(root, sel.loader);
366        if (loader) loader.hidden = false;
367
368        var data = window.embedpressFrontendData || {};
369        var restUrl = (data.rest_url || '/wp-json/') + 'embedpress/v1/youtube-playlist-items';
370
371        var params = new URLSearchParams({
372            playlist_id: state.playlistId,
373            page_token:  state.nextPageToken,
374            page_size:   String(state.pageSize),
375            offset:      String($$(root, sel.item).length),
376            layout:      state.layout
377        });
378
379        var headers = { 'Accept': 'application/json' };
380        if (data.rest_nonce) headers['X-WP-Nonce'] = data.rest_nonce;
381
382        return fetch(restUrl + '?' + params.toString(), { headers: headers, credentials: 'same-origin' })
383            .then(function (r) { return r.ok ? r.json() : Promise.reject(r); })
384            .then(function (payload) {
385                if (payload && payload.html) {
386                    var list = $(root, sel.list);
387                    list.insertAdjacentHTML('beforeend', payload.html);
388                }
389                state.nextPageToken = payload && payload.next_page_token ? payload.next_page_token : '';
390                root.setAttribute('data-next-page-token', state.nextPageToken);
391                if (state.shuffle) {
392                    shuffleOrder(root, sel, state);
393                } else {
394                    state.originalOrder = $$(root, sel.item).map(function (i) {
395                        return i.getAttribute('data-vid');
396                    });
397                }
398            })
399            .catch(function () { state.nextPageToken = ''; })
400            .finally(function () {
401                state.loadingNext = false;
402                if (loader) loader.hidden = true;
403            });
404    }
405
406    function attachEndedListener(root, sel, state) {
407        var iframe = $(root, sel.iframe);
408        if (!iframe) return;
409        try {
410            iframe.contentWindow.postMessage(JSON.stringify({
411                event: 'listening',
412                id:    state.id
413            }), '*');
414        } catch (e) { /* non-fatal */ }
415    }
416
417    function handleEnded(state) {
418        var root = state.root;
419        var sel  = SELECTORS[state.layout];
420        if (state.shuffle) {
421            var items = $$(root, sel.item);
422            if (items.length <= 1) return;
423            var next = state.cursor;
424            for (var attempt = 0; attempt < 5; attempt++) {
425                next = Math.floor(Math.random() * items.length);
426                if (next !== state.cursor) break;
427            }
428            state.cursor = next;
429            playByIndex(root, sel, next, state);
430        } else {
431            state.cursor++;
432            playByIndex(root, sel, state.cursor, state);
433        }
434    }
435
436    function bindGlobalMessageRouter() {
437        if (window[INIT_FLAG + 'MsgBound']) return;
438        window[INIT_FLAG + 'MsgBound'] = true;
439        window.addEventListener('message', function (event) {
440            if (typeof event.data !== 'string') return;
441            if (event.origin.indexOf('youtube.com') === -1 && event.origin.indexOf('youtube-nocookie.com') === -1) return;
442            var data;
443            try { data = JSON.parse(event.data); } catch (e) { return; }
444            if (data && data.event === 'onStateChange' && data.info === 0) {
445                $$(document, ROOT_SELECTOR).forEach(function (root) {
446                    var sel = SELECTORS[getLayout(root)];
447                    var iframe = $(root, sel.iframe);
448                    if (iframe && iframe.contentWindow === event.source) {
449                        var state = root[INIT_FLAG + 'State'];
450                        if (state) handleEnded(state);
451                    }
452                });
453            }
454        });
455    }
456
457    function initOne(root) {
458        if (root[INIT_FLAG]) return;
459        root[INIT_FLAG] = true;
460
461        var layout = getLayout(root);
462        var sel    = SELECTORS[layout];
463        var active = $(root, sel.activeItem) || $(root, sel.item);
464        var state  = {
465            root:           root,
466            layout:         layout,
467            id:             'epyp-' + Math.random().toString(36).slice(2, 9),
468            playlistId:     root.getAttribute('data-playlist-id') || '',
469            pageSize:       parseInt(root.getAttribute('data-pagesize'), 10) || 6,
470            nextPageToken:  root.getAttribute('data-next-page-token') || '',
471            cursor:         active ? visualIndex(root, sel, active) : 0,
472            loop:           false,
473            shuffle:        false,
474            loadingNext:    false,
475            originalOrder:  null
476        };
477        root[INIT_FLAG + 'State'] = state;
478
479        attachClickToSwap(root, sel, state);
480        attachLoopShuffle(root, sel, state);
481        attachInfiniteScroll(root, sel, state);
482        attachEndedListener(root, sel, state);
483        attachLayoutExtras(root, sel, state);
484        attachPlaybackNavButtons(root, sel, state);
485        if (layout === 'theatre') attachTheatreNavButtons(root, sel);
486    }
487
488    function init() {
489        bindGlobalMessageRouter();
490        $$(document, ROOT_SELECTOR).forEach(initOne);
491
492        if (typeof window.MutationObserver === 'function') {
493            var obs = new MutationObserver(function (mutations) {
494                mutations.forEach(function (m) {
495                    Array.prototype.forEach.call(m.addedNodes || [], function (node) {
496                        if (node.nodeType !== 1) return;
497                        if (node.matches && node.matches(ROOT_SELECTOR)) initOne(node);
498                        if (node.querySelectorAll) {
499                            Array.prototype.forEach.call(node.querySelectorAll(ROOT_SELECTOR), initOne);
500                        }
501                    });
502                });
503            });
504            obs.observe(document.body, { childList: true, subtree: true });
505        }
506    }
507
508    ready(init);
509})();

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.