EDMS/composables/useTouchInteractions.js
2025-06-05 14:57:08 +08:00

449 lines
12 KiB
JavaScript

// 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
};
};