PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.14
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.14
2.0.14 2.0.13 2.0.12 2.0.11 2.0.10 2.0.9 trunk 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8
blockenberg / assets / js / template-components-front.js

template-components-front.js in Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor 2.0.14, at assets/js/template-components-front.js

392 lines 30.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Native converted-template enhancement. Enqueue once on the frontend.
3 * Contract: [data-bkbg-component="family"][data-bkbg-behavior='JSON object'].
4 * initialOpen is zero based (-1 closes all); comparePosition is 0..100.
5 * Normalized milliseconds take precedence over source settings. Source classes
6 * retain their suffixes with elementor => bkbg-tpl, king-addons => bkbg-catalog.
7 * No source scripts, IDs, runtime globals or external libraries are required.
8 */
9 (function () {
10 'use strict';
11 const selector = '[data-bkbg-component]';
12 const ready = new WeakSet();
13 const motion = window.matchMedia('(prefers-reduced-motion: reduce)');
14 let serial = 0;
15 const id = () => 'bkbg-component-' + (++serial);
16 const parse = value => { try { const v = JSON.parse(value || '{}'); return v && typeof v === 'object' && !Array.isArray(v) ? v : {}; } catch (_) { return {}; } };
17 const number = (value, fallback) => value !== '' && value != null && Number.isFinite(Number(value)) ? Number(value) : fallback;
18 const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
19 const bool = (v, fallback) => v == null ? fallback : v === true || v === 'true' || v === 'yes' || v === 1 || v === '1';
20 const all = (root, query) => Array.from(root.querySelectorAll(query)).filter(el => el.closest(selector) === root);
21 const one = (root, query) => all(root, query)[0];
22 function button(el, label, activate) {
23 if (!el) return;
24 if (!el.matches('button,input,a[href],summary')) { el.setAttribute('role', 'button'); el.tabIndex = 0; }
25 if (el.matches('button')) el.type = 'button';
26 if (label && !el.hasAttribute('aria-label')) el.setAttribute('aria-label', label);
27 el.addEventListener('click', e => {
28 if (e.target.closest('a[href]') && e.target.closest('a[href]') !== el) return;
29 if (el.matches('a[href]') && !/^#?$/.test(el.getAttribute('href'))) return;
30 e.preventDefault(); activate(e);
31 });
32 if (!el.matches('button,input,summary')) el.addEventListener('keydown', e => {
33 if (e.target === el && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); activate(e); }
34 });
35 }
36 function accordion(root, opts, family) {
37 const container = one(root, '.bkbg-catalog-advanced-accordion');
38 const buttons = all(root, '.bkbg-catalog-acc-button,.bkbg-tpl-tab-title');
39 const multiple = bool(opts.accordionMultiple, family.includes('toggle') || container?.dataset.accordionType === 'toggle');
40 const records = buttons.map(b => {
41 const p = b.nextElementSibling;
42 if (!p || !p.matches('.bkbg-catalog-acc-panel,.bkbg-tpl-tab-content')) return null;
43 const king = b.classList.contains('bkbg-catalog-acc-button');
44 b.id = id(); p.id = id(); b.setAttribute('aria-controls', p.id);
45 p.setAttribute('role', 'region'); p.setAttribute('aria-labelledby', b.id);
46 const record = { b, p, king, open: b.classList.contains(king ? 'bkbg-catalog-acc-active' : 'bkbg-tpl-active') || p.classList.contains(king ? 'bkbg-catalog-acc-panel-active' : 'bkbg-tpl-active') || b.getAttribute('aria-expanded') === 'true' };
47 return record;
48 }).filter(Boolean);
49 function set(r, open) {
50 r.open = open; r.b.setAttribute('aria-expanded', String(open)); r.p.hidden = !open;
51 r.b.classList.toggle(r.king ? 'bkbg-catalog-acc-active' : 'bkbg-tpl-active', open);
52 r.p.classList.toggle(r.king ? 'bkbg-catalog-acc-panel-active' : 'bkbg-tpl-active', open);
53 // Legacy Elementor panels use display:none; King panels use grid rows.
54 r.p.style.display = open ? (r.king ? 'grid' : 'block') : 'none';
55 }
56 function activate(r, force) { const next = force || !r.open; if (next && !multiple) records.forEach(other => { if (other !== r) set(other, false); }); set(r, next); }
57 const explicit = number(opts.initialOpen, null);
58 const sourceIndex = container ? number(container.dataset.activeIndex, 0) - 1 : (family.includes('toggle') ? -1 : 0);
59 const hasActive = records.some(r => r.open);
60 records.forEach((r, i) => {
61 set(r, explicit !== null ? i === explicit : hasActive ? r.open : i === sourceIndex);
62 button(r.b, null, () => activate(r));
63 if (container?.dataset.accordionTrigger === 'hover') r.b.addEventListener('pointerenter', () => activate(r, true));
64 r.b.addEventListener('keydown', e => {
65 if (e.target !== r.b) return;
66 let next = i;
67 if (e.key === 'ArrowDown') next = (i + 1) % records.length;
68 else if (e.key === 'ArrowUp') next = (i + records.length - 1) % records.length;
69 else if (e.key === 'Home') next = 0;
70 else if (e.key === 'End') next = records.length - 1;
71 else return;
72 e.preventDefault(); records[next].b.focus();
73 });
74 });
75 const toggle = one(root, '.bkbg-catalog-content-toggle');
76 if (toggle) {
77 let secondary = toggle.classList.contains('bkbg-catalog-content-toggle--secondary-active') || toggle.dataset.defaultState === 'secondary';
78 const panes = all(root, '.bkbg-catalog-content-toggle__pane');
79 const labels = all(root, '.bkbg-catalog-content-toggle__label');
80 const sw = one(root, '.bkbg-catalog-content-toggle__switch');
81 function update() {
82 toggle.classList.toggle('bkbg-catalog-content-toggle--secondary-active', secondary);
83 toggle.classList.toggle('bkbg-catalog-content-toggle--primary-active', !secondary);
84 panes.forEach(p => { const active = p.classList.contains('bkbg-catalog-content-toggle__pane--secondary') === secondary; p.hidden = !active; p.classList.toggle('is-active', active); p.setAttribute('aria-hidden', String(!active)); });
85 labels.forEach(l => { const active = (l.dataset.target === 'secondary') === secondary; l.classList.toggle('is-active', active); l.setAttribute('aria-pressed', String(active)); });
86 if (sw) { sw.setAttribute('role', 'switch'); sw.setAttribute('aria-checked', String(secondary)); }
87 }
88 labels.forEach(l => button(l, null, () => { secondary = l.dataset.target === 'secondary'; update(); }));
89 button(sw, 'Toggle content', () => { secondary = !secondary; update(); }); update();
90 }
91 }
92 function carousel(root, input) { window.bkbgTemplateCarousel(root, input); }
93 function videoURL(raw) {
94 try {
95 const url = new URL(raw, location.href); if (!['http:', 'https:'].includes(url.protocol)) return null;
96 const host = url.hostname.toLowerCase().replace(/^www\./, '');
97 let match;
98 if (host === 'youtu.be' || ['youtube.com', 'm.youtube.com', 'youtube-nocookie.com'].includes(host)) {
99 const key = host === 'youtu.be' ? url.pathname.split('/')[1] : url.searchParams.get('v') || url.pathname.match(/^\/(?:embed|shorts|live)\/([^/]+)/)?.[1];
100 if (!/^[\w-]{11}$/.test(key || '')) return null;
101 return { src: 'https://www.youtube-nocookie.com/embed/' + key + '?autoplay=1', iframe: true };
102 }
103 if (host === 'vimeo.com' || host === 'player.vimeo.com') {
104 match = url.pathname.match(/\/(\d+)(?:\/|$)/); if (!match) return null;
105 const hash = url.searchParams.get('h') || url.pathname.match(/\/\d+\/([\w]+)$/)?.[1];
106 return { src: 'https://player.vimeo.com/video/' + match[1] + '?autoplay=1' + (hash ? '&h=' + encodeURIComponent(hash) : ''), iframe: true };
107 }
108 return /\.(mp4|webm|ogg)(?:$)/i.test(url.pathname) ? { src: url.href, iframe: false } : null;
109 } catch (_) { return null; }
110 }
111 function video(root, opts) {
112 const retainedMedia = all(root, '.bkbg-catalog-video-popup iframe,.bkbg-catalog-video-popup video');
113 const retainedURL = retainedMedia[0]?.getAttribute('src');
114 retainedMedia.forEach(el => { if (el.matches('video')) { el.pause(); el.removeAttribute('autoplay'); } el.removeAttribute('src'); });
115 const triggers = all(root, '.bkbg-catalog-video-popup-button,.bkbg-tpl-custom-embed-play,.bkbg-tpl-video-play-button,[data-video-url]');
116 let dialog;
117 triggers.forEach(trigger => {
118 const existing = one(root, '.bkbg-catalog-video-popup-video');
119 const settings = parse(trigger.getAttribute('data-settings'));
120 const kind = root.dataset.bkbgKind || 'video';
121 const source = opts.sourceSettings || {};
122 let sourceURL = source.kng_video_popup_hosted_url?.url || source.kng_video_popup_external_url?.url;
123 if (!sourceURL && source.kng_video_popup_youtube_url) sourceURL = /^[\w-]{11}$/.test(source.kng_video_popup_youtube_url) ? 'https://youtu.be/' + source.kng_video_popup_youtube_url : source.kng_video_popup_youtube_url;
124 if (!sourceURL && source.kng_video_popup_vimeo_url) sourceURL = /^\d+$/.test(source.kng_video_popup_vimeo_url) ? 'https://vimeo.com/' + source.kng_video_popup_vimeo_url : source.kng_video_popup_vimeo_url;
125 const raw = (videoURL(opts.videoUrl) ? opts.videoUrl : sourceURL) || retainedURL || trigger.dataset.videoUrl || settings.videoUrl || existing?.getAttribute('src') || trigger.getAttribute('href');
126 const media = videoURL(raw); if (!media) return;
127 button(trigger, 'Play video', () => {
128 if (dialog) return;
129 const previous = document.activeElement;
130 dialog = document.createElement('dialog'); dialog.className = 'bkbg-template-video-dialog'; dialog.setAttribute('aria-label', 'Video player');
131 dialog.style.cssText = 'position:fixed;inset:0;margin:auto;padding:56px 16px 16px;border:0;border-radius:12px;background:#111;color:white;box-sizing:border-box;width:min(932px,calc(100vw - 32px));max-width:calc(100vw - 32px);max-height:calc(100dvh - 32px);z-index:2147483000;box-shadow:0 0 0 100vmax rgba(0,0,0,.75)';
132 const close = document.createElement('button'); close.type = 'button'; close.className = 'bkbg-template-video-close'; close.setAttribute('aria-label', 'Close video');
133 close.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>';
134 const player = document.createElement(media.iframe ? 'iframe' : 'video');
135 player.style.cssText = 'display:block;width:100%;aspect-ratio:16/9;border:0';
136 if (media.iframe) { player.title = 'Video'; player.allow = 'autoplay; fullscreen; picture-in-picture'; player.setAttribute('allowfullscreen', ''); player.referrerPolicy = 'strict-origin-when-cross-origin'; }
137 else { player.controls = true; player.autoplay = true; player.playsInline = true; }
138 player.src = media.src; dialog.append(close, player); root.append(dialog);
139 function dispose() { if (!dialog) return; const own = dialog; dialog = null; own.remove(); if (previous?.isConnected) previous.focus(); }
140 close.addEventListener('click', dispose); dialog.addEventListener('cancel', e => { e.preventDefault(); dispose(); });
141 dialog.addEventListener('click', e => { if (e.target === dialog) { const box = dialog.getBoundingClientRect(); if (e.clientX < box.left || e.clientX > box.right || e.clientY < box.top || e.clientY > box.bottom) dispose(); } });
142 dialog.addEventListener('keydown', e => { if (e.key === 'Escape') { e.preventDefault(); dispose(); } });
143 if (dialog.showModal) dialog.showModal(); else { dialog.setAttribute('open', ''); dialog.setAttribute('role', 'dialog'); dialog.setAttribute('aria-modal', 'true'); }
144 close.focus();
145 });
146 });
147 }
148 function counters(root, opts) {
149 all(root, '.bkbg-tpl-counter-number,.bkbg-catalog-smooth-counter-number,[data-to],[data-end]').forEach(el => {
150 const settings = Object.assign({}, opts.sourceSettings, parse(root.getAttribute('data-settings')), parse(el.getAttribute('data-settings')), parse(el.getAttribute('kng-data-settings')));
151 const target = number(opts.target, number(el.dataset.to ?? el.dataset.toValue, number(el.dataset.end, number(settings.king_addons_smooth_counter_finish, number(settings.ending_number, null)))));
152 if (target === null) return;
153 const duration = clamp(number(opts.duration, number(el.dataset.duration, number(settings.king_addons_smooth_counter_anim_duration, 1500))), 0, 60000);
154 const start = number(opts.start, number(el.dataset.from ?? el.dataset.fromValue, 0));
155 const decimals = Math.min(6, number(el.dataset.decimals, (String(target).split('.')[1] || '').length));
156 const delimiter = el.dataset.delimiter || '';
157 const format = value => { const parts=value.toFixed(decimals).split('.');if(delimiter) parts[0]=parts[0].replace(/\B(?=(\d{3})+(?!\d))/g,delimiter);return parts.join('.'); };
158 const separatePrefix = one(root, '.bkbg-tpl-counter-number-prefix,.bkbg-catalog-smooth-counter-before-text');
159 const separateSuffix = one(root, '.bkbg-tpl-counter-number-suffix,.bkbg-catalog-smooth-counter-after-text');
160 if (opts.prefix != null && separatePrefix) separatePrefix.textContent = String(opts.prefix);
161 if (opts.suffix != null && separateSuffix) separateSuffix.textContent = String(opts.suffix);
162 const digitSlots = root.dataset.bkbgKind === 'king-addons-smooth-counter';
163 const measure = digitSlots ? document.createElement('canvas').getContext('2d') : null;
164 let lastText;
165 function sizeDigits() {
166 if (!measure) return;
167 const style = getComputedStyle(el);
168 measure.font = style.fontStyle + ' ' + style.fontWeight + ' ' + style.fontSize + ' ' + style.fontFamily;
169 const spacing = parseFloat(style.letterSpacing) || 0;
170 el.style.setProperty('--bkbg-digit-width', (measure.measureText('8').width + spacing) + 'px');
171 }
172 const draw = value => {
173 const text = (separatePrefix ? '' : String(opts.prefix || '')) + format(value) + (separateSuffix ? '' : String(opts.suffix || ''));
174 if (text === lastText) return;
175 lastText = text;
176 if (!digitSlots) { el.textContent = text; return; }
177 // Match Odometer's digit slots without retaining its runtime.
178 el.replaceChildren(...Array.from(text, character => {
179 if (!/\d/.test(character)) return document.createTextNode(character);
180 const digit = document.createElement('span');
181 digit.className = 'bkbg-counter-digit'; digit.textContent = character;
182 return digit;
183 }));
184 };
185 sizeDigits();
186 if (digitSlots) { document.fonts?.ready.then(sizeDigits); window.addEventListener('resize', sizeDigits); }
187 draw(target); // The final value remains readable until observation starts.
188 function animate() {
189 if (motion.matches || !duration) { draw(target); return; }
190 const began = performance.now();
191 function frame(now) { if (!el.isConnected) return; const progress = motion.matches ? 1 : Math.min(1, (now - began) / duration); draw(start + (target - start) * (1 - Math.pow(1 - progress, 3))); if (progress < 1) requestAnimationFrame(frame); }
192 requestAnimationFrame(frame);
193 }
194 if ('IntersectionObserver' in window) { const observer = new IntersectionObserver(entries => { if (entries.some(e => e.isIntersecting)) { observer.disconnect(); animate(); } }); observer.observe(el); }
195 else animate();
196 });
197 }
198 function hotspots(root) {
199 all(root, '.bkbg-catalog-hotspot-item').forEach(item => {
200 const tip = item.querySelector('.bkbg-catalog-hotspot-tooltip-content');
201 const control = item.querySelector('.bkbg-catalog-hotspot-content') || item.firstElementChild;
202 if (!tip || !control) return;
203 const permanent = item.classList.contains('bkbg-catalog-hotspot-tooltip-always-show');
204 let pinned = false;
205 tip.id = id(); tip.setAttribute('role', 'tooltip'); control.setAttribute('aria-describedby', tip.id);
206 function show(on) { const visible = permanent || on; item.classList.toggle('bkbg-catalog-hotspot-tooltip-always-show', visible); control.setAttribute('aria-expanded', String(visible)); tip.setAttribute('aria-hidden', String(!visible)); }
207 button(control, 'Show hotspot details', () => { pinned = !pinned; show(pinned); });
208 item.addEventListener('pointerenter', () => show(true)); item.addEventListener('pointerleave', () => show(pinned || item.contains(document.activeElement)));
209 item.addEventListener('focusin', () => show(true)); item.addEventListener('focusout', e => { if (!item.contains(e.relatedTarget)) show(pinned); });
210 item.addEventListener('keydown', e => { if (e.key === 'Escape') { pinned = false; show(false); } }); show(permanent);
211 });
212 }
213 function masonry(root) {
214 all(root, '.bkbg-catalog-image-grid').forEach(box => {
215 const items = Array.from(box.children).filter(e => e.classList.contains('bkbg-catalog-image-grid-item'));
216 let pending = 0;
217 function layout() {
218 pending = 0;
219 if (!items.length || !box.clientWidth) return;
220 const width = items[0].getBoundingClientRect().width;
221 const columns = Math.max(1, Math.round(box.clientWidth / width));
222 const heights = Array(columns).fill(0);
223 box.style.position = 'relative';
224 const measurements = items.map(item => item.getBoundingClientRect().height);
225 items.forEach((item, i) => {
226 const col = heights.indexOf(Math.min(...heights));
227 item.style.position = 'absolute'; item.style.left = (col * width) + 'px'; item.style.top = heights[col] + 'px';
228 heights[col] += measurements[i];
229 });
230 box.style.height = Math.max(...heights) + 'px';
231 }
232 const queue = () => { if (!pending) pending = requestAnimationFrame(layout); };
233 box.querySelectorAll('img').forEach(img => { if (!img.complete) img.addEventListener('load', queue, { once: true }); });
234 window.addEventListener('resize', queue); layout();
235 });
236 }
237 function comparison(root, opts) {
238 all(root, '.bkbg-catalog-image-comparison').forEach(box => {
239 let range = box.querySelector('input[type="range"]');
240 if (!range) { range = document.createElement('input'); range.type = 'range'; range.className = 'bkbg-catalog-image-comparison-slider'; box.append(range); }
241 range.min = '0'; range.max = '100'; range.step = '0.1'; range.setAttribute('aria-label', 'Image comparison position');
242 range.value = String(clamp(number(opts.comparePosition, number(range.getAttribute('value'), 50)), 0, 100));
243 function update() { box.style.setProperty('--position', range.value + '%'); range.setAttribute('aria-valuetext', Math.round(Number(range.value)) + '% before image'); }
244 range.addEventListener('input', update); update();
245 });
246 }
247 function imageAccordion(root) {
248 const items = all(root, '.bkbg-catalog-image-accordion-item');
249 const initial = parse(items[0]?.querySelector('.bkbg-catalog-img-accordion-media-hover')?.getAttribute('data-settings')).activeItem || {};
250 const wrap = one(root, '.bkbg-catalog-image-accordion-wrap');
251 if (wrap) wrap.style.opacity = '1';
252 function select(item) {
253 items.forEach(other => {
254 const active = item === other;
255 const media = other.querySelector('.bkbg-catalog-img-accordion-media-hover');
256 // The scoped source CSS supplies desktop/mobile expansion ratios.
257 // An inline ratio would override the mobile breakpoint.
258 other.style.removeProperty('flex-grow');
259 other.classList.toggle('bkbg-catalog-image-accordion-item-active', active);
260 other.classList.toggle('bkbg-catalog-image-accordion-item-grow', active);
261 if (media) { media.classList.add('bkbg-catalog-animation-wrap'); media.classList.toggle('bkbg-catalog-animation-wrap-active', active); }
262 if (motion.matches) other.style.transition = 'none';
263 });
264 }
265 items.forEach(item => { if (!item.hasAttribute('tabindex') && !item.matches('a,button')) item.tabIndex = 0; item.addEventListener('focusin', () => select(item)); if (initial.interaction !== 'click') item.addEventListener('pointerenter', () => select(item)); item.addEventListener('click', () => select(item)); item.addEventListener('keydown', e => { if (e.target === item && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); select(item); } }); });
266 const index = Math.trunc(number(initial.defaultActive, number((parse(root.dataset.bkbgBehavior).sourceSettings || {}).default_active, 0))) - 1;
267 if (index >= 0 && index < items.length) select(items[index]);
268 }
269 const lottieInstances = new WeakMap();
270 function lottieDecorations(root, opts) {
271 // PHP enqueues the trusted local vendor before this file. Never fetch code.
272 if (!window.lottie || typeof window.lottie.loadAnimation !== 'function') return;
273 all(root, '.bkbg-catalog-lottie-animations[data-json-url]').forEach(node => {
274 if (lottieInstances.has(node)) return;
275 const settings = Object.assign({}, opts.sourceSettings, parse(node.dataset.settings));
276 let sourceJSONPath;
277 try {
278 sourceJSONPath = new URL(node.dataset.jsonUrl, document.baseURI);
279 if (!['http:', 'https:'].includes(sourceJSONPath.protocol)) return;
280 } catch (_) { return; }
281 const trigger = String(settings.trigger || 'none').toLowerCase();
282 // Lottie's path retains the JSON directory for relative image assets (u/p).
283 // Do not override assetsPath: doing so would discard source image folders.
284 const config = {
285 container: node,
286 path: sourceJSONPath.href,
287 renderer: ['svg', 'canvas', 'html'].includes(settings.lottie_renderer) ? settings.lottie_renderer : 'svg',
288 loop: bool(settings.loop, bool(opts.loop, false)),
289 autoplay: false
290 };
291 let animation;
292 try { animation = window.lottie.loadAnimation(config); } catch (_) { return; }
293 lottieInstances.set(node, animation);
294 animation.setSpeed(clamp(number(settings.speed, 1), 0.01, 100));
295 animation.setDirection(bool(settings.reverse, false) ? -1 : 1);
296 let loaded = false, hovered = false, focused = false, clicked = false, pending = 0, disposed = false;
297 const autoplay = bool(settings.autoplay, bool(opts.autoplay, false));
298 const events = new AbortController();
299 function dispose() {
300 if (disposed) return;
301 disposed = true; events.abort(); cancelAnimationFrame(pending);
302 animation.destroy(); lottieInstances.delete(node);
303 }
304 function update() {
305 pending = 0;
306 if (!node.isConnected) { dispose(); return; }
307 if (!loaded) return;
308 if (motion.matches) { animation.goToAndStop(0, true); return; }
309 if (document.hidden) { animation.pause(); return; }
310 if (trigger === 'scroll' || trigger === 'viewport') {
311 const box = node.getBoundingClientRect();
312 const height = document.documentElement.clientHeight || window.innerHeight;
313 const start = clamp(number(settings.scroll_start?.size ?? settings.scroll_start, 0), 0, 100);
314 const end = clamp(number(settings.scroll_end?.size ?? settings.scroll_end, 100), 0, 100);
315 const inView = box.bottom / height * 100 > start && box.top / height * 100 < end;
316 if (trigger === 'viewport') { if (inView) animation.play(); else animation.pause(); }
317 else {
318 animation.pause();
319 if (inView) {
320 const distance = Math.max(1, document.documentElement.scrollHeight - height);
321 const progress = clamp(window.scrollY / distance, 0, 1);
322 animation.goToAndStop(progress * Math.max(0, animation.totalFrames - 1), true);
323 }
324 }
325 return;
326 }
327 const playing = trigger === 'hover' ? hovered || focused : trigger === 'click' ? clicked : autoplay;
328 if (playing) animation.play(); else animation.pause();
329 }
330 function queue() { if (!pending && !disposed) pending = requestAnimationFrame(update); }
331 function listen(target, name, handler, extra) { target.addEventListener(name, handler, Object.assign({ signal: events.signal }, extra)); }
332 if (trigger === 'hover' || trigger === 'click') {
333 if (!node.hasAttribute('tabindex') && !node.matches('a[href],button')) node.tabIndex = 0;
334 if (trigger === 'hover') {
335 listen(node, 'pointerenter', () => { hovered = true; update(); });
336 listen(node, 'pointerleave', () => { hovered = false; update(); });
337 listen(node, 'focusin', () => { focused = true; update(); });
338 listen(node, 'focusout', e => { focused = node.contains(e.relatedTarget); update(); });
339 } else {
340 node.setAttribute('role', 'button');
341 if (!node.hasAttribute('aria-label')) node.setAttribute('aria-label', 'Play or pause animation');
342 node.setAttribute('aria-pressed', 'false');
343 const toggle = () => { clicked = !clicked; node.setAttribute('aria-pressed', String(clicked)); update(); };
344 listen(node, 'click', e => { if (!e.target.closest('a[href]')) toggle(); });
345 listen(node, 'keydown', e => { if (e.target === node && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); toggle(); } });
346 }
347 }
348 if (trigger === 'scroll' || trigger === 'viewport') {
349 listen(window, 'scroll', queue, { passive: true }); listen(window, 'resize', queue);
350 }
351 listen(motion, 'change', update); listen(document, 'visibilitychange', update);
352 animation.addEventListener('DOMLoaded', () => { loaded = true; update(); });
353 animation.addEventListener('data_failed', dispose);
354 if (animation.isLoaded) { loaded = true; update(); }
355 });
356 }
357 function boot(scope) {
358 const roots = scope.matches?.(selector) ? [scope, ...scope.querySelectorAll(selector)] : Array.from(scope.querySelectorAll(selector));
359 roots.forEach(root => {
360 const family = (root.dataset.bkbgComponent || '').toLowerCase().replace(/_/g, '-');
361 const opts = parse(root.dataset.bkbgBehavior);
362 const kind = root.dataset.bkbgKind || family;
363 const source = opts.sourceSettings && typeof opts.sourceSettings === 'object' ? opts.sourceSettings : {};
364 const aliases = { autoplay: ['autoplay'], delay: ['autoplay_speed', 'autoplay_delay'], loop: ['infinite', 'loop'], speed: ['speed', 'transition_speed'], carouselSlides: ['slides_to_show', 'slides_per_view'], carouselSlidesTablet: ['slides_to_show_tablet', 'slides_per_view_tablet'], carouselSlidesMobile: ['slides_to_show_mobile', 'slides_per_view_mobile'], videoUrl: ['youtube_url', 'vimeo_url', 'video_url', 'kng_video_popup_youtube_url', 'kng_video_popup_vimeo_url'], target: ['ending_number', 'king_addons_smooth_counter_finish'], start: ['starting_number', 'king_addons_smooth_counter_start'], duration: ['duration', 'king_addons_smooth_counter_anim_duration'] };
365 Object.keys(aliases).forEach(key => { if (opts[key] == null) { const found = aliases[key].find(name => source[name] != null && source[name] !== ''); if (found) opts[key] = source[found]; } });
366 // Also retry missing vendor initialization on an explicit refresh.
367 if (bool(opts.lottie, false) || /lottie/.test(family) || one(root, '.bkbg-catalog-lottie-animations[data-json-url]')) lottieDecorations(root, opts);
368 if (ready.has(root)) return;
369 ready.add(root);
370 if (/accordion|toggle/.test(kind) && !/image|img/.test(kind)) accordion(root, opts, kind);
371 if (one(root, '.swiper-wrapper') && !/comparison/.test(kind)) carousel(root, opts);
372 if (/video/.test(family)) video(root, opts);
373 if(family==='timeline'){const outer=one(root,'.bkbg-catalog-timeline-outer-container');if(outer)outer.style.opacity='1';}
374 if (/counter/.test(family)) counters(root, opts);
375 if (/hotspot/.test(family)) hotspots(root);
376 if (family === 'grid') masonry(root);
377 if (/comparison|compare/.test(family)) comparison(root, opts);
378 if (/(image|img)-accordion/.test(kind)) imageAccordion(root);
379 if (/horizontal.*stack|stack.*horizontal/.test(family)) {
380 const stack = one(root, '.bkbg-catalog-horizontal-stack,.bkbg-tpl-horizontal-stack') || root;
381 stack.style.display = 'flex'; stack.style.flexWrap = 'nowrap'; stack.style.overflowX = 'auto'; stack.style.scrollSnapType = 'x proximity';
382 Array.from(stack.children).forEach(child => { child.style.flexShrink = '0'; child.style.scrollSnapAlign = 'start'; });
383 }
384 // Flip/hover and static map iframes are intentionally source-CSS driven.
385 });
386 }
387 // Explicit re-entry for locally inserted renderer output, without a global observer.
388 document.addEventListener('bkbg:components:refresh', e => boot(e.target instanceof Element ? e.target : document));
389 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => boot(document), { once: true });
390 else boot(document);
391 })();
392