/*! * VE-Slider Vaylens — ultraperformanter Drag- & Infinity-Slider für den nativen Webflow-Slider. * * Drop-in-Nachfolger des Legacy VE-SliderDrag v7 mit identischen Attributen: * Aktivierung: ve-attr="slider-drag" auf dem .w-slider-Element * data-infinite "true" → Endlos-Modus (natives Webflow-Attribut wird wiederverwendet) * data-true-infinite "true" (Default) → nahtloser Endlos-Modus mit Klon-Slides * data-duration Animationsdauer in ms (Default 500) * data-start-slide Start-Index (0-basiert); ohne Angabe gilt der w-active-Dot * data-logging "true" → eine Init-Zeile in der Konsole * data-keyboard "true" auf .w-slider-arrow-left/-right → der Pfeil * reagiert auf die Pfeiltaste (←/→), solange der Slider * im Viewport ist und kein Eingabefeld den Fokus hat * window.veSliderDragConfig: startSlideIndex, trueInfiniteMode, preventClicks, * enableLogging werden respektiert; alle übrigen Legacy-Keys (iOS-Strategien, * cssMode, nested, Debug-Kanäle …) sind in dieser Architektur gegenstandslos. * * Webflow-Koexistenz: Das native Slider-Modul wird gezielt stillgelegt — * data-disable-swipe, Entfernen des Swipe-Bindings, Neutralisieren des * Layout-Rebuilds (verhindert Dot-Duplikate durch die Klon-Slides) und eigene * Click-/Keyboard-Behandlung auf Pfeilen und Dots. Webflow-Autoplay wird beim * Init gestoppt, sollte aber zusätzlich im Designer deaktiviert sein. * Bekannte Einschränkung: mousedown auf Pfeilen/Dots erreicht document-Listener * nicht (Outside-Close-Pattern von Drittcode reagiert dort nicht). * * Architektur: Pointer Events + touch-action statt Mouse/Touch/iOS-Sonderpfaden, * Transform-Writes nur im requestAnimationFrame, transitionend statt Timeouts, * ResizeObserver repositioniert nach Breakpoint-Wechseln (Legacy-Bug behoben). * Bewusster Trade-off: Transforms werden wie im Legacy pro Slide gesetzt (kein * Track-Wrapper), damit Webflow-/Projekt-CSS und das Markup unangetastet bleiben. */ (() => { 'use strict'; const SELECTOR = '.w-slider[ve-attr="slider-drag"]'; let GLOBAL = {}; // wird in initAll() gelesen — Config darf nach dem Script stehen // Verhaltensparameter — identisch zum Legacy v7 kalibriert const DRAG_THRESHOLD = 5; // px Bewegung, ab der ein Drag beginnt const NAV_DISTANCE_RATIO = 0.2; // Anteil der Slide-Breite, ab dem navigiert wird const NAV_VELOCITY = 0.5; // px/ms Mindest-Geschwindigkeit für Navigation const FLICK_TIME = 200; // ms: schneller Wisch … const FLICK_VELOCITY = 0.8; // px/ms … bewegt genau 1 Slide const WHEEL_NAV_DELTA = 15; // px horizontales Wheel-Delta für Navigation const WHEEL_COOLDOWN = 800; // ms Sperre zwischen Wheel-Navigationen const CLICK_SUPPRESS_MS = 300; // ms Klick-Sperre nach einem echten Drag const STYLE_ID = 've-slider-transitions'; const instances = []; // So früh wie möglich, damit Webflows configure() das Attribut beim ersten // Build sieht (Footer-Platzierung: Script-Eval läuft vor Webflow.ready). for (const el of document.querySelectorAll(SELECTOR)) { el.setAttribute('data-disable-swipe', '1'); } function injectStyles() { if (document.getElementById(STYLE_ID)) return; const style = document.createElement('style'); style.id = STYLE_ID; style.textContent = [ '.ve-slide-transitioning { pointer-events: none !important; }', '.ve-slide-transitioning * { pointer-events: none !important; user-select: none !important; }', `${SELECTOR} .w-slider-mask { cursor: grab; }`, `${SELECTOR}.ve-dragging .w-slider-mask { cursor: grabbing; }`, `${SELECTOR}.ve-dragging { user-select: none; -webkit-user-select: none; }`, ].join('\n'); document.head.appendChild(style); } class VaylensSlider { constructor(root, id) { this.root = root; this.id = id; this.mask = root.querySelector('.w-slider-mask'); const originals = this.mask ? this.mask.querySelectorAll('.w-slide') : []; if (!this.mask || !originals.length) { throw new Error(`Slider #${id}: .w-slider-mask oder .w-slide fehlt`); } this.arrowLeft = root.querySelector('.w-slider-arrow-left'); this.arrowRight = root.querySelector('.w-slider-arrow-right'); this.nav = root.querySelector('.w-slider-nav'); this.keyboardLeft = !!this.arrowLeft && this.arrowLeft.getAttribute('data-keyboard') === 'true'; this.keyboardRight = !!this.arrowRight && this.arrowRight.getAttribute('data-keyboard') === 'true'; this.inViewport = true; this.viewObserver = null; this.count = originals.length; this.duration = parseInt(root.getAttribute('data-duration'), 10) || 500; const trueInfinite = this.readBool('data-true-infinite', GLOBAL.trueInfiniteMode, true); this.isInfinite = root.getAttribute('data-infinite') === 'true' && trueInfinite && this.count > 1; this.preventClicks = GLOBAL.preventClicks !== false; this.logging = this.readBool('data-logging', GLOBAL.enableLogging, false); this.autoplayConfigured = ['1', 'true'].includes(root.getAttribute('data-autoplay')); if (this.autoplayConfigured) { console.warn(`[VE-Slider Vaylens] Slider #${id}: data-autoplay ist aktiv — bitte im Designer deaktivieren; Webflows Timer wird neutralisiert.`); } this.cloneSets = 0; if (this.isInfinite) this.createClones(originals); this.slides = this.mask.querySelectorAll('.w-slide'); // Drag-/Animations-State this.index = 0; this.currentX = 0; // zuletzt geschriebener Transform-Offset this.baseOffset = 0; // Offset beim Drag-Start (inkl. Live-Wrap-Korrektur) this.pointerId = null; this.dragging = false; this.animating = false; this.rafId = 0; this.failsafe = 0; this.startX = 0; this.startTime = 0; this.lastX = 0; this.lastMoveTime = 0; this.velocity = 0; this.suppressClicksUntil = 0; this.lastWheelNav = 0; this.transitionsAnimated = null; // Cache: letzter transition-Zustand der Slides this.dragController = null; this.onDragMove = (e) => this.handleMove(e); this.onDragEnd = (e) => this.handleEnd(e); this.tick = () => this.handleTick(); this.controller = new AbortController(); this.bindEvents(); this.neutralizeWebflowSlider(); this.stopWebflowAutoplay(); this.measure(); this.index = this.resolveStartIndex(); this.setTransitions(false); this.applyTransform(-this.index * this.step); this.healWebflowResiduals(); this.updateUI(); // Repositioniert nach Breakpoint-Wechseln und sobald ein zunächst // unsichtbarer Slider Maße bekommt (ersetzt das 300ms-Init-Timeout des Legacy). this.resizeObserver = new ResizeObserver(() => { this.measure(); if (!this.dragging && !this.animating) { this.setTransitions(false); this.applyTransform(-this.index * this.step); } this.healWebflowResiduals(); this.updateUI(); }); this.resizeObserver.observe(this.mask); if (this.logging) { console.info(`[VE-Slider Vaylens] Slider #${id}: ${this.count} Slides, infinite=${this.isInfinite}, duration=${this.duration}ms`); } } readBool(attr, globalValue, fallback) { const v = this.root.getAttribute(attr); if (v !== null) return v === 'true'; if (typeof globalValue === 'boolean') return globalValue; return fallback; } createClones(originals) { // Legacy-Formel: genug Klon-Sets, damit beidseitig immer Slides sichtbar sind this.cloneSets = Math.max(2, Math.ceil(10 / this.count)); const fragFront = document.createDocumentFragment(); const fragBack = document.createDocumentFragment(); for (let set = 0; set < this.cloneSets; set++) { for (const slide of originals) { const front = slide.cloneNode(true); front.classList.add('ve-cloned-slide', 've-clone-front', `ve-clone-set-${set}`); front.setAttribute('aria-hidden', 'true'); fragFront.appendChild(front); const back = slide.cloneNode(true); back.classList.add('ve-cloned-slide', 've-clone-back', `ve-clone-set-${set}`); back.setAttribute('aria-hidden', 'true'); fragBack.appendChild(back); } } this.mask.insertBefore(fragFront, this.mask.firstChild); this.mask.appendChild(fragBack); } resolveStartIndex() { let logical = 0; const attr = this.root.getAttribute('data-start-slide'); const parsed = parseInt(attr, 10); if (attr !== null && Number.isInteger(parsed)) { logical = parsed; } else if (Number.isInteger(GLOBAL.startSlideIndex) && GLOBAL.startSlideIndex !== 0) { logical = GLOBAL.startSlideIndex; } else if (this.nav) { const active = Array.from(this.nav.children).findIndex((d) => d.classList.contains('w-active')); if (active > 0) logical = active; } logical = Math.min(Math.max(logical, 0), this.count - 1); return this.isInfinite ? logical + this.count * this.cloneSets : logical; } measure() { const first = this.slides[0]; this.slideWidth = first.offsetWidth; const cs = getComputedStyle(first); this.step = this.slideWidth + (parseFloat(cs.marginLeft) || 0) + (parseFloat(cs.marginRight) || 0); } logicalIndex(i = this.index) { return ((i % this.count) + this.count) % this.count; } // ── Webflow-Stilllegung ────────────────────────────────────────────────── // Macht Webflows Slider-Modul für dieses Element dauerhaft inert: Das // Swipe-Binding wird entfernt und maskWidth so überschrieben, dass Webflows // maskChanged() nie anschlägt — sonst würde ein Redraw (Navbar-Öffnen, // IX-Animation) die Klon-Slides ingestieren und z. B. 27 Dots erzeugen. neutralizeWebflowSlider() { const wf = window.Webflow; const $ = window.jQuery; if (!wf || !$) return; const root = this.root; const apply = () => { $(root).off('swipe'); const data = $.data(root, '.w-slider'); if (!data || data.veNeutralized || !data.mask) return; data.veNeutralized = true; Object.defineProperty(data, 'maskWidth', { get: () => data.mask.width(), set: () => {}, configurable: true, }); }; apply(); // Falls Webflows Slider-Build noch nicht gelaufen ist oder bei Redraws // neu bindet — apply ist idempotent. this.wfNeutralize = apply; if (typeof wf.push === 'function') wf.push(apply); if (wf.redraw && typeof wf.redraw.on === 'function') wf.redraw.on(apply); } // Trippt Webflows einmaligen Autoplay-Stopper (one('mousedown touchstart')). stopWebflowAutoplay() { if (!this.autoplayConfigured) return; this.root.dispatchEvent(new MouseEvent('mousedown', { bubbles: false })); } // Entfernt Reste von Webflows initialem Build: aria-hidden auf Original- // Slides und tabindex="-1" auf deren fokussierbaren Inhalten. Bewusst nicht // auf Slide-Nachfahren-aria-hidden angewendet (Autoren-Attribute, z. B. // dekorative Icons, dürfen nicht mitgelöscht werden). healWebflowResiduals() { for (const slide of this.mask.querySelectorAll('.w-slide:not(.ve-cloned-slide)')) { slide.removeAttribute('aria-hidden'); for (const el of slide.querySelectorAll('a[href][tabindex="-1"], button[tabindex="-1"], input[tabindex="-1"], select[tabindex="-1"], textarea[tabindex="-1"]')) { el.removeAttribute('tabindex'); } } } // ── Rendering ──────────────────────────────────────────────────────────── setTransitions(animated) { if (this.transitionsAnimated === animated) return; this.transitionsAnimated = animated; const value = animated ? `transform ${this.duration}ms ease` : 'none'; for (const slide of this.slides) slide.style.transition = value; } applyTransform(x) { this.currentX = x; const value = `translate3d(${x}px, 0, 0)`; for (const slide of this.slides) slide.style.transform = value; } // ── Drag ───────────────────────────────────────────────────────────────── bindEvents() { const { signal } = this.controller; const captureSignal = { capture: true, signal }; // Der Browser scrollt vertikal selbst und überlässt uns horizontale // Gesten (bei vertikalem Scroll kommt pointercancel) — ersetzt die // komplette iOS-/Safari-Sonderbehandlung des Legacy. this.mask.style.touchAction = 'pan-y pinch-zoom'; this.mask.style.overscrollBehaviorX = 'none'; this.mask.addEventListener('pointerdown', (e) => this.handleDown(e), { signal }); // Webflows tap-basierte Control-Navigation aussperren. Bewusst NUR auf // den Controls: mousedown/touchstart aus der Mask muss document erreichen, // sonst bricht Webflows validClick-Recorder (Touch-Klicks in Slides). const blockControls = (e) => { if (e.target.closest('.w-slider-arrow-left, .w-slider-arrow-right, .w-slider-dot')) { if (e.type === 'mousedown') e.preventDefault(); e.stopPropagation(); } }; this.root.addEventListener('mousedown', blockControls, captureSignal); this.root.addEventListener('touchstart', blockControls, { capture: true, passive: true, signal }); // Geister-Klicks nach einem Drag unterdrücken this.root.addEventListener('click', (e) => { if (this.preventClicks && performance.now() < this.suppressClicksUntil) { e.preventDefault(); e.stopPropagation(); } }, captureSignal); if (this.arrowLeft) { this.arrowLeft.addEventListener('click', (e) => this.handleArrow(e, -1), captureSignal); } if (this.arrowRight) { this.arrowRight.addEventListener('click', (e) => this.handleArrow(e, 1), captureSignal); } if (this.nav) { this.nav.addEventListener('click', (e) => this.handleDotClick(e), captureSignal); } // Webflows keydown-Navigation auf Pfeilen/Dots ersetzen (würde sonst in // dessen klonlosem Index-Raum animieren und den Slider zerreißen). this.root.addEventListener('keydown', (e) => this.handleKeydown(e), captureSignal); this.mask.addEventListener('wheel', (e) => this.handleWheel(e), { passive: false, signal }); // Pfeiltasten-Navigation (data-keyboard="true" auf einem Arrow). Nur // aktiv, solange der Slider sichtbar ist — sonst würden Slider weiter // unten auf der Seite unsichtbar mitnavigieren. if (this.keyboardLeft || this.keyboardRight) { this.viewObserver = new IntersectionObserver((entries) => { this.inViewport = entries[0].isIntersecting; }); this.viewObserver.observe(this.root); document.addEventListener('keydown', (e) => this.handleGlobalKeydown(e), { signal }); } // Natives Bild-/Link-Dragging unterbinden (inkl. Klone, daher nach createClones) for (const el of this.root.querySelectorAll('img, a')) { el.setAttribute('draggable', 'false'); } this.root.addEventListener('dragstart', (e) => e.preventDefault(), { signal }); } handleDown(e) { this.stopWebflowAutoplay(); if (this.animating || this.pointerId !== null) return; if (e.pointerType === 'mouse' && e.button !== 0) return; if (e.target.closest('input, select, textarea, button, [contenteditable], .w-slider-arrow-left, .w-slider-arrow-right, .w-slider-nav')) return; this.pointerId = e.pointerId; this.dragging = false; this.startX = e.clientX; this.lastX = e.clientX; this.startTime = e.timeStamp; this.lastMoveTime = e.timeStamp; this.velocity = 0; this.baseOffset = -this.index * this.step; // Move/Up auf window: funktioniert vor wie nach Verlassen der Mask, // ohne setPointerCapture (das würde Klicks auf Links in Slides umlenken). this.dragController = new AbortController(); const opts = { passive: true, signal: this.dragController.signal }; window.addEventListener('pointermove', this.onDragMove, opts); window.addEventListener('pointerup', this.onDragEnd, opts); window.addEventListener('pointercancel', this.onDragEnd, opts); } handleMove(e) { if (e.pointerId !== this.pointerId) return; if (this.animating) return; // Parität: laufende Animation nicht kapern const dt = e.timeStamp - this.lastMoveTime; if (dt > 0) this.velocity = (e.clientX - this.lastX) / dt; this.lastX = e.clientX; this.lastMoveTime = e.timeStamp; if (!this.dragging) { if (Math.abs(e.clientX - this.startX) < DRAG_THRESHOLD) return; this.dragging = true; this.root.classList.add('ve-dragging'); this.setTransitions(false); this.rafId = requestAnimationFrame(this.tick); } } handleTick() { if (!this.dragging) { this.rafId = 0; return; } let x = this.baseOffset + (this.lastX - this.startX); if (this.isInfinite) x = this.wrapDuringDrag(x); this.applyTransform(x); this.rafId = requestAnimationFrame(this.tick); } // Verschiebt Offset, Basis und Index um ganze Slide-Zyklen, sobald der Drag // das sichere Klon-Band verlässt — periodisches Muster, daher unsichtbar. wrapDuringDrag(x) { const period = this.count * this.step; if (!period) return x; const maxX = -this.count * this.step; const minX = -this.count * (this.cloneSets + 1) * this.step; while (x > maxX) { x -= period; this.baseOffset -= period; this.index += this.count; } while (x < minX) { x += period; this.baseOffset += period; this.index -= this.count; } return x; } handleEnd(e) { if (e.pointerId !== this.pointerId) return; this.pointerId = null; this.dragController.abort(); this.dragController = null; if (this.rafId) { cancelAnimationFrame(this.rafId); this.rafId = 0; } const wasDragging = this.dragging; this.dragging = false; this.root.classList.remove('ve-dragging'); if (!wasDragging) return; this.suppressClicksUntil = performance.now() + CLICK_SUPPRESS_MS; if (e.type === 'pointercancel') { // Browser hat die Geste übernommen (z. B. vertikales Scrollen) → zurückschnappen this.goTo(this.index); return; } if (!this.slideWidth || !this.step) { this.goTo(this.index); // Maße ungültig (Slider versteckt) → nur zurückschnappen return; } const deltaX = this.lastX - this.startX; const elapsed = e.timeStamp - this.startTime; const absDelta = Math.abs(deltaX); const absVelocity = Math.abs(this.velocity); if (absDelta > NAV_DISTANCE_RATIO * this.slideWidth || absVelocity > NAV_VELOCITY) { const isFlick = elapsed < FLICK_TIME && absVelocity > FLICK_VELOCITY; const jumps = isFlick ? 1 : Math.max(1, Math.floor(absDelta / this.slideWidth)); const direction = deltaX < 0 ? 1 : -1; this.goTo(this.index + direction * jumps); } else { this.goTo(this.index); // Snap-back } } // ── Navigation ─────────────────────────────────────────────────────────── goTo(target) { if (!Number.isFinite(target)) target = this.index; if (!this.isInfinite) target = Math.min(Math.max(target, 0), this.count - 1); const destination = -target * this.step; this.index = target; this.updateUI(); // sofort wie im Legacy — Dots/Arrows/w-active nicht erst nach der Animation if (this.currentX === destination) return; this.animating = true; this.root.classList.add('ve-slide-transitioning'); this.setTransitions(true); this.applyTransform(destination); let done = false; const finish = () => { if (done) return; done = true; clearTimeout(this.failsafe); this.slides[0].removeEventListener('transitionend', onEnd); if (this.controller.signal.aborted) return; // destroy() während der Animation this.afterNavigation(); }; const onEnd = (ev) => { if (ev.target === this.slides[0] && ev.propertyName === 'transform') finish(); }; this.slides[0].addEventListener('transitionend', onEnd, { signal: this.controller.signal }); this.failsafe = setTimeout(finish, this.duration + 100); } afterNavigation() { if (this.isInfinite) { // Zurück ins Referenz-Band springen (Legacy-Band: [count, 2*count)) const lower = this.count; const upper = this.count * (this.cloneSets + 1); if (this.index < lower || this.index >= upper) { this.index = lower + this.logicalIndex(); } } this.setTransitions(false); this.applyTransform(-this.index * this.step); this.animating = false; this.root.classList.remove('ve-slide-transitioning'); this.updateUI(); } handleArrow(e, direction) { e.preventDefault(); e.stopPropagation(); this.navigate(direction); } navigate(direction) { if (this.animating || this.dragging || this.pointerId !== null) return; this.goTo(this.index + direction); } handleGlobalKeydown(e) { if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return; if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return; // Browser-Shortcuts (z. B. Alt+← = Zurück) nicht anfassen if (!this.inViewport) return; const left = e.key === 'ArrowLeft'; if (left ? !this.keyboardLeft : !this.keyboardRight) return; // Eingaben und das Dot-Fokus-Wandern (handleKeydown) nicht stören if (e.target instanceof Element && e.target.closest('input, select, textarea, [contenteditable], video, audio, .w-slider-dot')) return; e.preventDefault(); this.navigate(left ? -1 : 1); } handleDotClick(e) { const dot = e.target.closest('.w-slider-dot'); if (!dot || !this.nav.contains(dot)) return; // Aktuelles webflow.js bindet die Dots per delegiertem click auf der Nav // (nicht mehr nur über das Touch-Modul) — der mousedown-Blocker reicht // daher nicht. stopPropagation in der Capture-Phase feuert vor Webflows // Bubble-Delegation; KEIN preventDefault im Erfolgsfall (Safari-Klick-Synthese). e.stopPropagation(); if (this.dragging || this.animating || this.pointerId !== null) { e.preventDefault(); return; } const dotIndex = Array.from(this.nav.children).indexOf(dot); if (dotIndex < 0) return; if (this.isInfinite) { let offset = dotIndex - this.logicalIndex(); if (Math.abs(offset) > this.count / 2) offset -= Math.sign(offset) * this.count; this.goTo(this.index + offset); } else { this.goTo(dotIndex); } } handleKeydown(e) { const isActivate = e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar'; const onArrowLeft = e.target.closest('.w-slider-arrow-left'); const onArrowRight = e.target.closest('.w-slider-arrow-right'); const dot = e.target.closest('.w-slider-dot'); if (!onArrowLeft && !onArrowRight && !dot) return; if (isActivate) { if (onArrowLeft) { this.handleArrow(e, -1); return; } if (onArrowRight) { this.handleArrow(e, 1); return; } e.preventDefault(); // Space darf nicht scrollen e.stopPropagation(); this.handleDotClick(e); return; } // Fokus-Wandern auf den Dots selbst übernehmen, damit Webflows // data.pages-basierter Fokus-Handler nichts Eigenes tut. if (dot && ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(e.key)) { e.preventDefault(); e.stopPropagation(); const dots = Array.from(this.nav.children); const current = dots.indexOf(dot); const next = e.key === 'Home' ? 0 : e.key === 'End' ? dots.length - 1 : (e.key === 'ArrowLeft' || e.key === 'ArrowUp') ? Math.max(current - 1, 0) : Math.min(current + 1, dots.length - 1); if (dots[next]) dots[next].focus(); } } handleWheel(e) { let dx = e.deltaX; let dy = e.deltaY; if (e.shiftKey && !dx) { dx = dy; dy = 0; } const factor = e.deltaMode === 1 ? 40 : e.deltaMode === 2 ? 800 : 1; dx *= factor; dy *= factor; if (Math.abs(dx) <= 1.5 * Math.abs(dy) || Math.abs(dx) <= 3) return; // vertikal → Seite scrollt e.preventDefault(); // verhindert horizontales Seiten-Panning / Trackpad-Back-Geste e.stopPropagation(); if (this.pointerId !== null || this.animating || Math.abs(dx) <= WHEEL_NAV_DELTA) return; const now = performance.now(); if (now - this.lastWheelNav < WHEEL_COOLDOWN) return; this.lastWheelNav = now; this.goTo(this.index + (dx > 0 ? 1 : -1)); } // ── UI-Synchronisation (Dots, Arrows, w-active) ────────────────────────── updateUI() { const logical = this.logicalIndex(); this.slides.forEach((slide, i) => { slide.classList.toggle('w-active', i % this.count === logical); }); if (this.nav) { Array.from(this.nav.children).forEach((dot, i) => { const active = i === logical; dot.classList.toggle('w-active', active); dot.setAttribute('aria-selected', String(active)); dot.setAttribute('aria-pressed', String(active)); dot.setAttribute('tabindex', active ? '0' : '-1'); }); } if (this.arrowLeft) { this.setArrowState(this.arrowLeft, 'w-slider-arrow-left-inactive', !this.isInfinite && logical === 0); } if (this.arrowRight) { this.setArrowState(this.arrowRight, 'w-slider-arrow-right-inactive', !this.isInfinite && logical === this.count - 1); } } setArrowState(arrow, inactiveClass, disabled) { arrow.classList.toggle(inactiveClass, disabled); arrow.setAttribute('aria-disabled', String(disabled)); arrow.setAttribute('tabindex', disabled ? '-1' : '0'); } destroy() { this.controller.abort(); if (this.dragController) this.dragController.abort(); if (this.rafId) cancelAnimationFrame(this.rafId); clearTimeout(this.failsafe); this.resizeObserver.disconnect(); if (this.viewObserver) this.viewObserver.disconnect(); const wf = window.Webflow; if (this.wfNeutralize && wf && wf.redraw && typeof wf.redraw.off === 'function') { wf.redraw.off(this.wfNeutralize); } for (const clone of this.mask.querySelectorAll('.ve-cloned-slide')) clone.remove(); for (const slide of this.mask.querySelectorAll('.w-slide')) { slide.style.transform = ''; slide.style.transition = ''; } this.root.classList.remove('ve-dragging', 've-slide-transitioning'); } } // ── Bootstrap ────────────────────────────────────────────────────────────── let initialized = false; function initAll() { if (initialized) return; initialized = true; GLOBAL = window.veSliderDragConfig || {}; injectStyles(); document.querySelectorAll(SELECTOR).forEach((root, i) => { try { instances.push(new VaylensSlider(root, i)); } catch (err) { console.error('[VE-Slider Vaylens] Init fehlgeschlagen:', err); } }); } window.veSliderVaylens = { instances, init: initAll, destroyAll() { for (const instance of instances) instance.destroy(); instances.length = 0; initialized = false; }, }; // Primärpfad: Webflow-Queue. webflow.js adoptiert ein vorab existierendes // Array und führt die Queue NACH dem Slider-Build seiner Module aus — // funktioniert daher auch bei Script-Platzierung im . (window.Webflow = window.Webflow || []).push(initAll); // Fallback nur für Seiten ohne webflow.js: dort bleibt window.Webflow ein // gewöhnliches Array und kein webflow.js-Script ist im Dokument. const initWithoutWebflow = () => { if (initialized) return; const hasWebflowScript = !!document.querySelector('script[src*="webflow"]'); if (!hasWebflowScript && Array.isArray(window.Webflow)) initAll(); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initWithoutWebflow, { once: true }); } else { setTimeout(initWithoutWebflow, 0); } // Sicherheitsnetz: Queue wurde bis zum Load-Event nie adoptiert → selbst starten. window.addEventListener('load', () => { if (!initialized && Array.isArray(window.Webflow)) initAll(); }, { once: true }); })();