Files
interview-demo-code/frontend/spa/src/composables/useSwipeBack.js

210 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
};
}