// Touch Interactions Composable for Mobile DMS // Provides swipe gestures, touch controls, and mobile enhancements import { ref, onMounted, onUnmounted, nextTick, readonly } from 'vue'; export const useTouchInteractions = (options = {}) => { // Default configuration const defaultOptions = { threshold: 50, // Minimum distance for swipe velocity: 0.3, // Minimum velocity for swipe timeThreshold: 500, // Maximum time for swipe preventScroll: false, // Prevent scroll during swipe enablePinch: false, // Enable pinch-to-zoom enableRotation: false, // Enable rotation ...options }; // Touch state const touchState = ref({ isSupported: false, isSwiping: false, startX: 0, startY: 0, currentX: 0, currentY: 0, startTime: 0, direction: null, distance: 0, velocity: 0 }); // Multi-touch state const multiTouchState = ref({ isMultiTouch: false, initialDistance: 0, currentDistance: 0, scale: 1, rotation: 0 }); // Gesture detection const gestures = ref({ swipeLeft: false, swipeRight: false, swipeUp: false, swipeDown: false, pinch: false, spread: false, rotate: false }); // Event callbacks const callbacks = ref({ onSwipeStart: null, onSwipeMove: null, onSwipeEnd: null, onSwipeLeft: null, onSwipeRight: null, onSwipeUp: null, onSwipeDown: null, onPinchStart: null, onPinchMove: null, onPinchEnd: null, onRotateStart: null, onRotateMove: null, onRotateEnd: null, onTap: null, onDoubleTap: null, onLongPress: null }); // Utility functions const getDistance = (touches) => { if (touches.length < 2) return 0; const dx = touches[0].clientX - touches[1].clientX; const dy = touches[0].clientY - touches[1].clientY; return Math.sqrt(dx * dx + dy * dy); }; const getAngle = (touches) => { if (touches.length < 2) return 0; const dx = touches[1].clientX - touches[0].clientX; const dy = touches[1].clientY - touches[0].clientY; return Math.atan2(dy, dx) * 180 / Math.PI; }; const getDirection = (startX, startY, endX, endY) => { const dx = endX - startX; const dy = endY - startY; if (Math.abs(dx) > Math.abs(dy)) { return dx > 0 ? 'right' : 'left'; } else { return dy > 0 ? 'down' : 'up'; } }; const getVelocity = (distance, time) => { return time > 0 ? distance / time : 0; }; // Touch event handlers const handleTouchStart = (event) => { const touch = event.touches[0]; const now = Date.now(); touchState.value = { ...touchState.value, isSwiping: true, startX: touch.clientX, startY: touch.clientY, currentX: touch.clientX, currentY: touch.clientY, startTime: now, direction: null, distance: 0, velocity: 0 }; // Multi-touch handling if (event.touches.length > 1 && defaultOptions.enablePinch) { multiTouchState.value = { isMultiTouch: true, initialDistance: getDistance(event.touches), currentDistance: getDistance(event.touches), scale: 1, rotation: getAngle(event.touches) }; callbacks.value.onPinchStart?.(multiTouchState.value); } callbacks.value.onSwipeStart?.(touchState.value); if (defaultOptions.preventScroll) { event.preventDefault(); } }; const handleTouchMove = (event) => { if (!touchState.value.isSwiping) return; const touch = event.touches[0]; const currentX = touch.clientX; const currentY = touch.clientY; const deltaX = currentX - touchState.value.startX; const deltaY = currentY - touchState.value.startY; const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); touchState.value = { ...touchState.value, currentX, currentY, distance, direction: getDirection( touchState.value.startX, touchState.value.startY, currentX, currentY ) }; // Multi-touch handling if (event.touches.length > 1 && multiTouchState.value.isMultiTouch) { const currentDistance = getDistance(event.touches); const currentRotation = getAngle(event.touches); multiTouchState.value = { ...multiTouchState.value, currentDistance, scale: currentDistance / multiTouchState.value.initialDistance, rotation: currentRotation - multiTouchState.value.rotation }; callbacks.value.onPinchMove?.(multiTouchState.value); } callbacks.value.onSwipeMove?.(touchState.value); if (defaultOptions.preventScroll) { event.preventDefault(); } }; const handleTouchEnd = (event) => { if (!touchState.value.isSwiping) return; const endTime = Date.now(); const duration = endTime - touchState.value.startTime; const velocity = getVelocity(touchState.value.distance, duration); touchState.value = { ...touchState.value, isSwiping: false, velocity }; // Determine if it's a valid swipe const isValidSwipe = touchState.value.distance >= defaultOptions.threshold && velocity >= defaultOptions.velocity && duration <= defaultOptions.timeThreshold; if (isValidSwipe) { // Reset gesture state Object.keys(gestures.value).forEach(key => { gestures.value[key] = false; }); // Set current gesture switch (touchState.value.direction) { case 'left': gestures.value.swipeLeft = true; callbacks.value.onSwipeLeft?.(touchState.value); break; case 'right': gestures.value.swipeRight = true; callbacks.value.onSwipeRight?.(touchState.value); break; case 'up': gestures.value.swipeUp = true; callbacks.value.onSwipeUp?.(touchState.value); break; case 'down': gestures.value.swipeDown = true; callbacks.value.onSwipeDown?.(touchState.value); break; } } // Multi-touch end if (multiTouchState.value.isMultiTouch) { callbacks.value.onPinchEnd?.(multiTouchState.value); multiTouchState.value.isMultiTouch = false; } callbacks.value.onSwipeEnd?.(touchState.value); }; // Tap detection let tapTimeout = null; let tapCount = 0; let longPressTimeout = null; const handleTap = (event) => { const touch = event.touches[0] || event.changedTouches[0]; // Long press detection longPressTimeout = setTimeout(() => { callbacks.value.onLongPress?.({ x: touch.clientX, y: touch.clientY, target: event.target }); }, 500); // Double tap detection tapCount++; if (tapCount === 1) { tapTimeout = setTimeout(() => { if (tapCount === 1) { callbacks.value.onTap?.({ x: touch.clientX, y: touch.clientY, target: event.target }); } tapCount = 0; }, 300); } else if (tapCount === 2) { clearTimeout(tapTimeout); clearTimeout(longPressTimeout); callbacks.value.onDoubleTap?.({ x: touch.clientX, y: touch.clientY, target: event.target }); tapCount = 0; } }; const handleTouchCancel = () => { touchState.value.isSwiping = false; multiTouchState.value.isMultiTouch = false; clearTimeout(tapTimeout); clearTimeout(longPressTimeout); }; // Setup touch interactions const setupTouchInteractions = (element) => { if (!element) return; // Check for touch support touchState.value.isSupported = 'ontouchstart' in window; if (touchState.value.isSupported) { element.addEventListener('touchstart', handleTouchStart, { passive: !defaultOptions.preventScroll }); element.addEventListener('touchmove', handleTouchMove, { passive: !defaultOptions.preventScroll }); element.addEventListener('touchend', handleTouchEnd, { passive: true }); element.addEventListener('touchcancel', handleTouchCancel, { passive: true }); // Tap events element.addEventListener('touchstart', handleTap, { passive: true }); } return () => { if (element && touchState.value.isSupported) { element.removeEventListener('touchstart', handleTouchStart); element.removeEventListener('touchmove', handleTouchMove); element.removeEventListener('touchend', handleTouchEnd); element.removeEventListener('touchcancel', handleTouchCancel); element.removeEventListener('touchstart', handleTap); } }; }; // Register callback functions const onSwipeStart = (callback) => { callbacks.value.onSwipeStart = callback; }; const onSwipeMove = (callback) => { callbacks.value.onSwipeMove = callback; }; const onSwipeEnd = (callback) => { callbacks.value.onSwipeEnd = callback; }; const onSwipeLeft = (callback) => { callbacks.value.onSwipeLeft = callback; }; const onSwipeRight = (callback) => { callbacks.value.onSwipeRight = callback; }; const onSwipeUp = (callback) => { callbacks.value.onSwipeUp = callback; }; const onSwipeDown = (callback) => { callbacks.value.onSwipeDown = callback; }; const onPinchStart = (callback) => { callbacks.value.onPinchStart = callback; }; const onPinchMove = (callback) => { callbacks.value.onPinchMove = callback; }; const onPinchEnd = (callback) => { callbacks.value.onPinchEnd = callback; }; const onTap = (callback) => { callbacks.value.onTap = callback; }; const onDoubleTap = (callback) => { callbacks.value.onDoubleTap = callback; }; const onLongPress = (callback) => { callbacks.value.onLongPress = callback; }; // DMS-specific touch interactions const dmsInteractions = { // Swipe to delete setupSwipeToDelete: (element, onDelete) => { const cleanup = setupTouchInteractions(element); onSwipeLeft(() => { element.classList.add('swipe-delete-active'); onDelete?.(); }); return cleanup; }, // Pull to refresh setupPullToRefresh: (element, onRefresh) => { let refreshTriggered = false; const cleanup = setupTouchInteractions(element); onSwipeDown((state) => { if (state.distance > 100 && !refreshTriggered) { refreshTriggered = true; onRefresh?.(); setTimeout(() => { refreshTriggered = false; }, 2000); } }); return cleanup; }, // Pinch to zoom for document preview setupPinchZoom: (element, onZoom) => { const cleanup = setupTouchInteractions(element); onPinchMove((state) => { onZoom?.(state.scale); }); return cleanup; }, // Long press for context menu setupContextMenu: (element, onContext) => { const cleanup = setupTouchInteractions(element); onLongPress((state) => { onContext?.(state); }); return cleanup; } }; // Haptic feedback (if supported) const hapticFeedback = { light: () => { if (navigator.vibrate) { navigator.vibrate(10); } }, medium: () => { if (navigator.vibrate) { navigator.vibrate(20); } }, heavy: () => { if (navigator.vibrate) { navigator.vibrate([30, 10, 30]); } }, success: () => { if (navigator.vibrate) { navigator.vibrate([10, 5, 10]); } }, error: () => { if (navigator.vibrate) { navigator.vibrate([50, 25, 50]); } } }; return { // State touchState: readonly(touchState), multiTouchState: readonly(multiTouchState), gestures: readonly(gestures), // Setup setupTouchInteractions, // Event registration onSwipeStart, onSwipeMove, onSwipeEnd, onSwipeLeft, onSwipeRight, onSwipeUp, onSwipeDown, onPinchStart, onPinchMove, onPinchEnd, onTap, onDoubleTap, onLongPress, // DMS-specific dmsInteractions, // Utilities hapticFeedback, // Configuration options: defaultOptions }; };