PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, Embed PDF viewer, YouTube Videos, 3D FlipBook, Social feeds & more / trunk
EmbedPress – PDF Embedder, Embed PDF viewer, YouTube Videos, 3D FlipBook, Social feeds & more vtrunk
4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 trunk 1.0.0 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 2.0.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.1.3 3.2.0 3.2.1 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.6.8 3.7.0 3.7.1 3.7.2 3.7.3 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.9.0 3.9.1 3.9.10 3.9.11 3.9.12 3.9.13 3.9.14 3.9.15 3.9.16 3.9.17 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9 4.0.0 4.0.1 4.0.10 4.0.11 4.0.12 4.0.13 4.0.14 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.10 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 4.3.1 4.4.0 4.4.1 4.4.10 4.4.11 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 4.5.0 4.5.1
embedpress / assets / js / ep-yt-queue.js
embedpress / assets / js Last commit date
chunks 1 week ago vendor 8 months ago admin.build.js 1 week ago admin.js 3 months ago analytics-tracker.js 4 weeks ago analytics.build.js 1 week ago blocks.build.js 1 week ago carousel.js 1 year ago custom-player.build.js 1 week ago documents-viewer-script.js 3 months ago ep-pdf-lightbox.js 3 months ago ep-view-count.js 1 week ago ep-yt-queue.js 4 weeks ago feature-notices.js 7 months ago front.js 4 weeks ago frontend.build.js 3 months ago gallery-justify.js 1 week ago gutneberg-script.js 1 month ago index.html 7 years ago initCarousel.js 2 years ago initplyr.js 1 month ago instafeed.js 4 weeks ago instagram-shortcode-generator.js 1 month ago lazy-load.js 6 months ago license.js 3 months ago meetup-timezone.js 3 months ago onboarding.build.js 1 week ago pdf-gallery-elementor-editor.js 3 months ago pdf-gallery.js 3 months ago preview.js 8 months ago settings.js 3 months ago sponsored.js 9 months ago
ep-yt-queue.js
510 lines
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 })();
510