| 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 |
|