1/* Heiser WebTec â WAAPI Viewport Reveal (Elementor-aware) 2 * Beobachtet .hwt-watch / [data-hwt-watch] und animiert per Web Animations API 3 * von blur/translate/scale ins Endstadium (opacity:1, blur(0), transform:perspective(1000px)) 4 * Zusätzlich: Allen Elementor-Elementen wird .hwt-is-not-visible gegeben 5 * und beim Eintritt in den Viewport entfernt (CSS-first-Workflow). 6 */ 7(() => { 8 'use strict'; 9 10 // --- Konfiguration --- 11 const SELECTOR = '.hwt-watch, [data-hwt-watch]'; 12 const ELEMENTOR_SELECTOR = '.elementor-widget, .elementor-column, .elementor-section, .elementor-element'; 13 const NOT_VISIBLE_CLASS = 'hwt-is-not-visible'; 14 const VISIBLE_CLASS = 'hwt-is-visible'; 15 16const VIS_RATIO = window.matchMedia('(max-width: 767px)').matches ? 0.1 : 0.5; 17const ROOT_MARGIN = window.matchMedia('(max-width: 767px)').matches ? '0px 0px 0px 0px' : '0px 0px -10% 0px'; 18 19 const DEFAULTS = { 20 duration: 1000, // ms 21 easing: 'cubic-bezier(.2,.65,.25,1)', // knackig, aber smooth 22 translateY: 24, // px Start-Offset 23 blur: 30, // px Start-Blur 24 scale: 0.92, // leicht kleiner am Start 25 delay: 0 26 }; 27 28 const $$ = (root = document, sel = SELECTOR) => Array.from(root.querySelectorAll(sel)); 29 30 // prefers-reduced-motion respektieren 31 const REDUCED = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; 32 33 // Sichtbarkeitsquote (0..1) 34 const visibleRatio = (el) => { 35 const r = el.getBoundingClientRect(); 36 const vh = window.innerHeight || document.documentElement.clientHeight; 37 const vw = window.innerWidth || document.documentElement.clientWidth; 38 if (r.width <= 0 || r.height <= 0) return 0; 39 const visH = Math.max(0, Math.min(r.bottom, vh) - Math.max(r.top, 0)); 40 const visW = Math.max(0, Math.min(r.right, vw) - Math.max(r.left, 0)); 41 return (visH * visW) / (r.width * r.height); 42 }; 43 44 const getRatio = (el) => { 45 // Optional per data-hwt-threshold überschreibbar 46 const a = parseFloat(el.getAttribute('data-hwt-threshold')); 47 return Number.isFinite(a) ? a : VIS_RATIO; 48 }; 49 50 // Per-Element Overrides via Data-Attribute (Dauer, Delay etc.) 51 const readOpts = (el) => ({ 52 duration: parseInt(el.getAttribute('data-hwt-duration')) || DEFAULTS.duration, 53 easing: el.getAttribute('data-hwt-easing') || DEFAULTS.easing, 54 translateY: parseFloat(el.getAttribute('data-hwt-translate')) || DEFAULTS.translateY, 55 blur: parseFloat(el.getAttribute('data-hwt-blur')) || DEFAULTS.blur, 56 scale: parseFloat(el.getAttribute('data-hwt-scale')) || DEFAULTS.scale, 57 delay: parseInt(el.getAttribute('data-hwt-delay')) || DEFAULTS.delay, 58 }); 59 60 // Initialzustand NUR via JS setzen (kein CSS nötig) â für .hwt-watch 61 const setInitial = (el, opts) => { 62 if (el.__hwtInit) return; 63 el.style.opacity = '0'; 64 el.style.filter = `blur(${opts.blur}px)`; 65 el.style.transform = `perspective(1000px) translateY(${opts.translateY}px) scale(${opts.scale})`; 66 el.style.willChange = 'opacity, filter, transform'; 67 el.__hwtInit = true; 68 }; 69 70 // Animation mit WAAPI â läuft 1x, bleibt im Endzustand stehen 71 const animateIn = (el) => { 72 if (el.__hwtDone || REDUCED) { 73 // Bei reduced-motion: sofortiger Endzustand 74 el.style.opacity = '1'; 75 el.style.filter = 'blur(0)'; 76 el.style.transform = 'perspective(1000px)'; 77 el.style.willChange = ''; 78 el.__hwtDone = true; 79 return; 80 } 81 82 const o = readOpts(el); 83 // Falls WAAPI fehlt, kurzer CSS-Notfall-Transition-Fallback: 84 if (!('animate' in el)) { 85 el.style.transition = `opacity ${o.duration}ms ${o.easing}, filter ${o.duration}ms ${o.easing}, transform ${o.duration}ms ${o.easing}`; 86 requestAnimationFrame(() => { 87 el.style.opacity = '1'; 88 el.style.filter = 'blur(0)'; 89 el.style.transform = 'perspective(1000px)'; 90 }); 91 el.__hwtDone = true; 92 return; 93 } 94 95 const anim = el.animate( 96 [ 97 { 98 opacity: 0, 99 filter: `blur(${o.blur}px)`,
100 transform: `perspective(1000px) translateY(${o.translateY}px) scale(${o.scale})` 101 }, 102 { 103 opacity: 1, 104 filter: 'blur(0px)', 105 transform: 'perspective(1000px)' 106 } 107 ], 108 { 109 duration: o.duration, 110 easing: o.easing, 111 delay: o.delay, 112 fill: 'forwards' // Endzustand beibehalten 113 } 114 ); 115 116 anim.onfinish = () => { 117 el.style.opacity = '1'; 118 el.style.filter = 'blur(0)'; 119 el.style.transform = 'perspective(1000px)'; 120 el.style.willChange = ''; 121 el.__hwtDone = true; 122 }; 123 }; 124 125 // IO Setup 126 let io = null; 127 const buildThresholds = (target) => { 128 const steps = [0, 0.1, 0.25, 0.35, 0.45, target, 0.6, 0.75, 0.9, 1]; 129 return Array.from(new Set(steps)).sort((a, b) => a - b); 130 // dichtere Steps um target sorgen fürs Auslösen nahe der Zielquote 131 }; 132 133 const ensureObserver = () => { 134 if (io) return io; 135 if ('IntersectionObserver' in window) { 136 io = new IntersectionObserver((entries) => { 137 entries.forEach((entry) => { 138 const el = entry.target; 139 const need = getRatio(el); 140 const ratio = entry.intersectionRatio; 141 142 // 1) Elementor: Sichtbarkeitsklasse toggeln 143 if (el.matches(ELEMENTOR_SELECTOR)) { 144 if (ratio >= need) { 145 el.classList.remove(NOT_VISIBLE_CLASS); 146 el.classList.add(VISIBLE_CLASS); 147 } else { 148 el.classList.remove(VISIBLE_CLASS); 149 el.classList.add(NOT_VISIBLE_CLASS); 150 } 151} 152 153 // 2) Spezielle .hwt-watch-Animation 154 if (el.matches(SELECTOR) && ratio >= need) { 155 animateIn(el); 156 io.unobserve(el); // nur einmal animieren 157 } 158 }); 159 }, { 160 root: null, 161 threshold: buildThresholds(VIS_RATIO), 162 rootMargin: ROOT_MARGIN 163 }); 164 } 165 return io; 166 }; 167 168 // Elemente registrieren 169 const register = (root = document) => { 170 const obs = ensureObserver(); 171 172 // a) ALLE Elementor-Elemente: Startzustand -> not-visible + beobachten 173 const allEl = $$(root, ELEMENTOR_SELECTOR); 174 allEl.forEach((el) => { 175 el.classList.add(NOT_VISIBLE_CLASS); 176 if (obs) { 177 obs.observe(el); 178 // Falls beim Laden schon genug sichtbar, sofort entfernen 179 if (visibleRatio(el) >= getRatio(el)) el.classList.remove(NOT_VISIBLE_CLASS); 180 if (visibleRatio(el) >= getRatio(el)) el.classList.add(VISIBLE_CLASS); 181 } else { 182 // Fallback ohne IO 183 if (visibleRatio(el) >= getRatio(el)) el.classList.remove(NOT_VISIBLE_CLASS); 184 if (visibleRatio(el) >= getRatio(el)) el.classList.add(VISIBLE_CLASS); 185 } 186 }); 187 188 // b) Nur .hwt-watch: Initial JS-Styles + beobachten + ggf. sofort animieren 189 const animEls = $$(root, SELECTOR); 190 animEls.forEach((el) => { 191 setInitial(el, readOpts(el)); 192 if (obs) { 193 obs.observe(el); 194 if (visibleRatio(el) >= getRatio(el)) { 195 animateIn(el); 196 obs.unobserve(el); 197 } 198 } else { 199 if (visibleRatio(el) >= getRatio(el)) animateIn(el); 200 } 201 }); 202 }; 203 204 // Elementor-Integration + DOM-Ãnderungen beachten 205 const installMutationObserver = () => { 206 const mo = new MutationObserver((muts) => { 207 for (const m of muts) { 208 if (!m.addedNodes || !m.addedNodes.length) continue; 209 m.addedNodes.forEach((n) => { 210 if (!(n instanceof Element)) return; 211 if (n.matches(ELEMENTOR_SELECTOR) || n.matches(SELECTOR)) register(n); 212 register(n); // auch Kinder scannen 213 }); 214 } 215 }); 216 mo.observe(document.body, { childList: true, subtree: true }); 217 }; 218 219 const start = () => { 220 register(document); 221 installMutationObserver(); 222 }; 223 224 // Elementor-sicher booten 225 const boot = () => { 226 const run = () => { 227 if (window.elementorFrontend && window.elementorFrontend.hooks) { 228 elementorFrontend.hooks.addAction('frontend/element_ready/global', (scope) => { 229 const node = scope[0] || scope; 230 register(node); 231 }); 232 } 233 start(); 234 }; 235 if (window.elementorFrontend && window.elementorFrontend.on) { 236 window.jQuery(window).on('elementor/frontend/init', run); 237 } else if (document.readyState === 'loading') { 238 document.addEventListener('DOMContentLoaded', run); 239 } else { 240 run(); 241 } 242 }; 243 244 boot(); 245})(); 246
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.