class Swiper { constructor() { this.isInitialized = false; this.initSubscribers(); this.initHandlers(); this.swipe = { startX: null, endX: null, startY: null, endY: null, minSwipe: 50 }; this.pinch = { active: false, startDistance: 0, lastDistance: 0, scale: 1 }; } /********************************************************************* LISTENERS *********************************************************************/ initHandlers() { this.touchStartHandler = this.handleTouchStart.bind(this); this.touchMoveHandler = this.handleTouchMove.bind(this); this.touchEndHandler = this.handleTouchEnd.bind(this); } initListeners() { if (this.isInitialized) { return; } this.isInitialized = true; document.addEventListener('touchstart', this.touchStartHandler); document.addEventListener('touchmove', this.touchMoveHandler); document.addEventListener('touchend', this.touchEndHandler); } cleanupListeners() { if (this.subscribers.size > 0) return; this.isInitialized = false; document.removeEventListener('touchstart', this.touchStartHandler); document.removeEventListener('touchmove', this.touchMoveHandler); document.removeEventListener('touchend', this.touchEndHandler); } handleTouchStart(e) { if (e.touches.length === 2) { // Two-finger pinch start const dx = e.touches[0].clientX - e.touches[1].clientX; const dy = e.touches[0].clientY - e.touches[1].clientY; const distance = Math.sqrt(dx*dx + dy*dy); this.pinch.active = true; this.pinch.startDistance = this.pinch.lastDistance = distance; this.notify('pinch-start', { distance }); return; // Don't treat this as a swipe } this.swipe.startX = e.touches[0].clientX; this.swipe.startY = e.touches[0].clientY; } handleTouchMove(e) { if (this.pinch.active && e.touches.length === 2) { const dx = e.touches[0].clientX - e.touches[1].clientX; const dy = e.touches[0].clientY - e.touches[1].clientY; const distance = Math.sqrt(dx*dx + dy*dy); const scale = distance / this.pinch.startDistance; this.pinch.lastDistance = distance; this.pinch.scale = scale; this.notify('pinch-move', { e, distance, scale }); // Direction if (distance > this.pinch.startDistance) { this.notify('pinch-out', { scale }); } else { this.notify('pinch-in', { scale }); } return; // Do not fire swipe logic } this.swipe.endX = e.touches[0].clientX; this.swipe.endY = e.touches[0].clientY; } handleTouchEnd(e) { // Finish pinch if (this.pinch.active) { this.notify('pinch-end', { finalScale: this.pinch.scale }); this.pinch.active = false; return; } if ((!this.swipe.startX || !this.swipe.endX) ||(!this.swipe.startY || !this.swipe.endY)) return; const distanceX = this.swipe.startX - this.swipe.endX; const distanceY = this.swipe.startY - this.swipe.endY; if (Math.abs(distanceX) > this.swipe.minSwipe) { let direction = distanceX > 0 ? 'swipe-right' : 'swipe-left'; this.notify(direction); } if (Math.abs(distanceY) > this.swipe.minSwipe) { let direction = distanceY > 0 ? 'swipe-up' : 'swipe-down'; this.notify(direction); } this.swipe.startX = this.swipe.startY = this.swipe.endX = this.swipe.endY = null; } /********************************************************************* SUBSCRIBERS *********************************************************************/ initSubscribers() { this.subscribers = new Set(); } subscribe(callback) { if (!this.isInitialized) { this.initListeners(); } this.subscribers.add(callback); return () => this.subscribers.delete(callback); } unsubscribe(callback) { this.subscribers.delete(callback); if (this.subscribers.size === 0) { this.cleanupListeners(); } } notify(event, data = {}) { this.subscribers.forEach( callback => { try { callback(event, data); } catch (error) { console.error('Subscriber error:', error); } }); } /****************************************************************** CLEANUP ******************************************************************/ destroy() { this.subscribers.clear(); this.cleanupListeners(); } } window.jvbSwiper = new Swiper();