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.