import { ref, onMounted, onUnmounted, computed } from 'vue'; import { useRouter, useRoute } from 'vue-router'; import { useHapticFeedback } from './useHapticFeedback.js'; /** * Композабл для обработки жеста Swipe Back * Реализует поведение, аналогичное нативному в Telegram MiniApp * Без визуального индикатора */ export function useSwipeBack() { const router = useRouter(); const route = useRoute(); const haptic = useHapticFeedback(); // Состояние жеста const isActive = ref(false); const deltaX = ref(0); const progress = ref(0); const hasTriggeredHaptic = ref(false); // Конфигурация const EDGE_THRESHOLD = 20; // Расстояние от левого края для активации (px) const ACTIVATION_THRESHOLD = 80; // Пороговое расстояние для активации (px) const MAX_DELTA = 80; // Максимальное расстояние для расчета прогресса (px) let touchStartX = 0; let touchStartY = 0; let touchStartTime = 0; let isTracking = false; /** * Вычисляет прогресс жеста (0-1) */ const calculateProgress = (currentDeltaX) => { return Math.min(currentDeltaX / MAX_DELTA, 1); }; /** * Проверяет, можно ли использовать жест swipe back */ const canUseSwipeBack = computed(() => { return route.name !== 'home'; }); /** * Проверяет, начинается ли жест от левого края экрана */ const isStartingFromLeftEdge = (x) => { return x <= EDGE_THRESHOLD; }; /** * Обработчик начала касания */ const handleTouchStart = (e) => { // Проверяем, можно ли использовать жест (не на главной странице) if (!canUseSwipeBack.value) return; const touch = e.touches[0]; if (!touch) return; // Проверяем, не является ли цель касания интерактивным элементом const target = e.target; if (target.closest('input, textarea, select, button, a, [role="button"], .swiper, .swiper-wrapper')) { return; } touchStartX = touch.clientX; touchStartY = touch.clientY; touchStartTime = Date.now(); // Проверяем, начинается ли жест от левого края if (isStartingFromLeftEdge(touchStartX)) { isTracking = true; isActive.value = true; deltaX.value = 0; progress.value = 0; hasTriggeredHaptic.value = false; } }; /** * Обработчик движения пальца */ const handleTouchMove = (e) => { if (!isTracking) return; const touch = e.touches[0]; if (!touch) return; const currentX = touch.clientX; const currentY = touch.clientY; const currentDeltaX = currentX - touchStartX; const currentDeltaY = Math.abs(currentY - touchStartY); // Проверяем, что движение преимущественно горизонтальное if (Math.abs(currentDeltaX) < Math.abs(currentDeltaY) * 0.5 && Math.abs(currentDeltaX) > 5) { // Вертикальное движение - отменяем жест resetGesture(); return; } // Если движение влево (отрицательное), уменьшаем прогресс if (currentDeltaX < 0) { deltaX.value = Math.max(currentDeltaX, -EDGE_THRESHOLD); // Если вернулись слишком далеко влево, отменяем жест if (currentDeltaX < -10) { resetGesture(); return; } } else { // Прямое обновление deltaX для положительного движения deltaX.value = currentDeltaX; } // Обновляем прогресс только для положительного движения progress.value = calculateProgress(Math.max(0, deltaX.value)); // Проверяем достижение порога активации if (deltaX.value >= ACTIVATION_THRESHOLD && !hasTriggeredHaptic.value) { // Срабатывает тактильная обратная связь один раз if (haptic) { haptic.impactOccurred('light'); } hasTriggeredHaptic.value = true; } // Предотвращаем прокрутку страницы во время жеста if (deltaX.value > 0) { e.preventDefault(); } }; /** * Обработчик окончания касания */ const handleTouchEnd = () => { if (!isTracking) return; // Если жест достиг порога активации - выполняем навигацию назад if (deltaX.value >= ACTIVATION_THRESHOLD) { // Небольшая задержка для плавности setTimeout(() => { router.back(); resetGesture(); }, 50); } else { // Откат жеста без действия resetGesture(); } }; /** * Обработчик отмены касания */ const handleTouchCancel = () => { resetGesture(); }; /** * Сброс состояния жеста */ const resetGesture = () => { isTracking = false; isActive.value = false; deltaX.value = 0; progress.value = 0; hasTriggeredHaptic.value = false; }; /** * Инициализация обработчиков событий */ const init = () => { document.addEventListener('touchstart', handleTouchStart, { passive: true }); document.addEventListener('touchmove', handleTouchMove, { passive: false }); document.addEventListener('touchend', handleTouchEnd, { passive: true }); document.addEventListener('touchcancel', handleTouchCancel, { passive: true }); }; /** * Очистка обработчиков событий */ const cleanup = () => { document.removeEventListener('touchstart', handleTouchStart); document.removeEventListener('touchmove', handleTouchMove); document.removeEventListener('touchend', handleTouchEnd); document.removeEventListener('touchcancel', handleTouchCancel); resetGesture(); }; onMounted(() => { init(); }); onUnmounted(() => { cleanup(); }); return { isTracking, isActive, deltaX, progress, ACTIVATION_THRESHOLD, MAX_DELTA, }; }