// Enhanced Mobile Gesture Support (function() { 'use strict'; class MobileGestures { constructor() { this.touchStartX = 0; this.touchStartY = 0; this.touchEndX = 0; this.touchEndY = 0; this.longPressTimer = null; this.init(); } init() { // Only initialize on mobile if (!this.isMobile()) return; // Add swipe to navigate back this.initSwipeBack(); // Add long press for context menus this.initLongPress(); // Add swipe actions for list items this.initSwipeActions(); // Add pinch to zoom for images/charts this.initPinchZoom(); } isMobile() { return window.innerWidth <= 768 || 'ontouchstart' in window; } // Swipe from left edge to go back initSwipeBack() { let startX = 0; let startY = 0; let startTime = 0; document.addEventListener('touchstart', (e) => { const touch = e.touches[0]; startX = touch.clientX; startY = touch.clientY; startTime = Date.now(); // Only track if starting from left edge if (startX > 30) return; // Add visual indicator this.showSwipeIndicator(); }, { passive: true }); document.addEventListener('touchmove', (e) => { if (startX > 30) return; const touch = e.touches[0]; const diffX = touch.clientX - startX; const diffY = Math.abs(touch.clientY - startY); // Horizontal swipe detection if (diffX > 50 && diffY < 50) { this.updateSwipeIndicator(diffX); } }, { passive: true }); document.addEventListener('touchend', (e) => { if (startX > 30) return; const endTime = Date.now(); const timeDiff = endTime - startTime; const touch = e.changedTouches[0]; const diffX = touch.clientX - startX; this.hideSwipeIndicator(); // Quick swipe from edge if (timeDiff < 300 && diffX > 100) { this.navigateBack(); } }); } // Long press for context actions initLongPress() { const longPressElements = document.querySelectorAll('[data-long-press]'); longPressElements.forEach(element => { element.addEventListener('touchstart', (e) => { this.longPressTimer = setTimeout(() => { this.showContextMenu(element, e); // Haptic feedback if (navigator.vibrate) { navigator.vibrate(50); } }, 500); }, { passive: true }); element.addEventListener('touchend', () => { clearTimeout(this.longPressTimer); }); element.addEventListener('touchmove', () => { clearTimeout(this.longPressTimer); }); }); } // Swipe actions on list items initSwipeActions() { const swipeElements = document.querySelectorAll('[data-swipe-actions]'); swipeElements.forEach(element => { let startX = 0; let currentX = 0; let startTime = 0; element.addEventListener('touchstart', (e) => { startX = e.touches[0].clientX; startTime = Date.now(); element.style.transition = 'none'; }, { passive: true }); element.addEventListener('touchmove', (e) => { currentX = e.touches[0].clientX; const diffX = currentX - startX; // Limit swipe distance const maxSwipe = 100; const swipeX = Math.max(-maxSwipe, Math.min(maxSwipe, diffX)); element.style.transform = `translateX(${swipeX}px)`; // Show action hints if (swipeX < -50) { element.classList.add('swipe-left'); element.classList.remove('swipe-right'); } else if (swipeX > 50) { element.classList.add('swipe-right'); element.classList.remove('swipe-left'); } else { element.classList.remove('swipe-left', 'swipe-right'); } }, { passive: true }); element.addEventListener('touchend', (e) => { const endTime = Date.now(); const timeDiff = endTime - startTime; const diffX = currentX - startX; element.style.transition = 'transform 0.3s ease'; element.style.transform = ''; // Quick swipe actions if (timeDiff < 300) { if (diffX < -80) { this.triggerSwipeAction(element, 'left'); } else if (diffX > 80) { this.triggerSwipeAction(element, 'right'); } } element.classList.remove('swipe-left', 'swipe-right'); }); }); } // Pinch to zoom for images initPinchZoom() { const zoomElements = document.querySelectorAll('[data-zoomable]'); zoomElements.forEach(element => { let initialDistance = 0; let currentScale = 1; element.addEventListener('touchstart', (e) => { if (e.touches.length === 2) { initialDistance = this.getDistance(e.touches[0], e.touches[1]); } }, { passive: true }); element.addEventListener('touchmove', (e) => { if (e.touches.length === 2) { e.preventDefault(); const currentDistance = this.getDistance(e.touches[0], e.touches[1]); currentScale = currentDistance / initialDistance; currentScale = Math.max(0.5, Math.min(3, currentScale)); element.style.transform = `scale(${currentScale})`; } }, { passive: false }); element.addEventListener('touchend', () => { if (currentScale < 0.8 || currentScale > 2.5) { element.style.transition = 'transform 0.3s ease'; element.style.transform = 'scale(1)'; currentScale = 1; } }); }); } // Helper methods getDistance(touch1, touch2) { const dx = touch1.clientX - touch2.clientX; const dy = touch1.clientY - touch2.clientY; return Math.sqrt(dx * dx + dy * dy); } showSwipeIndicator() { if (!this.swipeIndicator) { this.swipeIndicator = document.createElement('div'); this.swipeIndicator.className = 'swipe-back-indicator'; this.swipeIndicator.innerHTML = ''; document.body.appendChild(this.swipeIndicator); } this.swipeIndicator.classList.add('visible'); } updateSwipeIndicator(progress) { if (this.swipeIndicator) { const opacity = Math.min(1, progress / 100); const scale = 0.8 + (0.2 * opacity); this.swipeIndicator.style.opacity = opacity; this.swipeIndicator.style.transform = `translateX(${progress * 0.3}px) scale(${scale})`; } } hideSwipeIndicator() { if (this.swipeIndicator) { this.swipeIndicator.classList.remove('visible'); } } navigateBack() { // Check if there's a back button const backBtn = document.querySelector('.btn-back, [href*="javascript:history.back"]'); if (backBtn) { backBtn.click(); } else { history.back(); } } showContextMenu(element, event) { const actions = element.dataset.longPress.split(','); // Create context menu const menu = document.createElement('div'); menu.className = 'mobile-context-menu'; menu.style.top = `${event.touches[0].clientY}px`; menu.style.left = `${event.touches[0].clientX}px`; actions.forEach(action => { const [label, handler] = action.split(':'); const item = document.createElement('button'); item.textContent = label; item.onclick = () => { if (window[handler]) { window[handler](element); } menu.remove(); }; menu.appendChild(item); }); document.body.appendChild(menu); // Remove on outside click setTimeout(() => { document.addEventListener('touchstart', () => menu.remove(), { once: true }); }, 100); } triggerSwipeAction(element, direction) { const action = element.dataset[`swipe${direction.charAt(0).toUpperCase() + direction.slice(1)}`]; if (action && window[action]) { window[action](element); } } } // Add CSS for gestures function addGestureStyles() { if (document.getElementById('gesture-styles')) return; const styles = ` `; document.head.insertAdjacentHTML('beforeend', styles); } // Initialize document.addEventListener('DOMContentLoaded', function() { addGestureStyles(); new MobileGestures(); }); window.MobileGestures = MobileGestures; })();