Events: touchstart, touchmove, touchend
Glossary overview

Events: touchstart, touchmove, touchend

Что такое touchstart, touchmove, touchend?

Эти события нужны для обработки касаний на мобильных устройствах (пальцами).

СобытиеКогда срабатывает
touchstartКогда палец касается экрана
touchmoveКогда палец двигается по экрану
touchendКогда палец убирается с экрана

Они похожи на mousedown, mousemove, mouseup, но для пальцев.

Простой пример

<div id="box" style="width: 200px; height: 200px; background: lightblue;">
  Коснись меня
</div>

<script>
const box = document.getElementById('box');

box.addEventListener('touchstart', (event) => {
  box.style.background = 'lightgreen';
  console.log('Палец коснулся!', event.touches);
});

box.addEventListener('touchmove', (event) => {
  box.style.background = 'yellow';
  console.log('Палец двигается!', event.touches);
});

box.addEventListener('touchend', (event) => {
  box.style.background = 'lightblue';
  console.log('Палец убрали!', event.touches);
});
</script>

Что происходит:

  • Когда ты касаешься div → фон меняется на зелёный.
  • Двигаешь пальцем → фон жёлтый.
  • Убираешь палец → фон снова голубой.

Где это часто используют:

  1. Swipe (свайпы)
    Чтобы определить, например, что пользователь провёл влево или вправо (например, в слайдерах или галереях).
  2. Drag & Drop на мобильных
    Когда хочешь сделать перетаскивание элементов пальцем, а не мышкой.
  3. Кастомные жесты
    Например, зажатие двумя пальцами для масштабирования (pinch-zoom).
  4. Игры
    Чтобы обрабатывать управление движением героя или объектов прикосновением.
  5. Переходы между страницами
    Например, свайп влево → перейти на следующую страницу, свайп вправо → вернуться назад.

Маленький пример свайпа вправо/влево

<script>
let startX = 0;
let endX = 0;

document.addEventListener('touchstart', (e) => {
  startX = e.touches[0].clientX;
});

document.addEventListener('touchend', (e) => {
  endX = e.changedTouches[0].clientX;
  handleSwipe();
});

function handleSwipe() {
  if (endX - startX > 50) {
    console.log('Свайп вправо!');
  } else if (startX - endX > 50) {
    console.log('Свайп влево!');
  } else {
    console.log('Свайп слишком короткий');
  }
}
</script>

Важно:

  • event.touches — это список всех пальцев, которые в данный момент касаются экрана.
  • event.changedTouches — это те пальцы, которые изменили состояние (например, убрали палец).

Drag & Drop пальцем на мобильных устройствах

Пример: перетаскиваем квадрат пальцем

<div id="dragMe" style="
  width: 100px;
  height: 100px;
  background: tomato;
  position: absolute;
  top: 100px;
  left: 100px;
  touch-action: none; /* важно! чтобы отключить зум/скролл */
">
  Тяни меня
</div>

<script>
const box = document.getElementById('dragMe');
let offsetX = 0;
let offsetY = 0;

box.addEventListener('touchstart', (e) => {
  const touch = e.touches[0];
  const rect = box.getBoundingClientRect();
  
  offsetX = touch.clientX - rect.left;
  offsetY = touch.clientY - rect.top;
  
  console.log('Начал тянуть');
});

box.addEventListener('touchmove', (e) => {
  e.preventDefault(); // отменяем скролл страницы
  
  const touch = e.touches[0];
  
  box.style.left = (touch.clientX - offsetX) + 'px';
  box.style.top = (touch.clientY - offsetY) + 'px';
  
  console.log('Тяну...', touch.clientX, touch.clientY);
});

box.addEventListener('touchend', (e) => {
  console.log('Отпустил');
});
</script>

Как это работает:

  • Когда палец касается (touchstart) — запоминаем, где именно на элементе было касание.
  • Когда двигаем палец (touchmove) — перемещаем элемент туда, где сейчас палец.
  • Когда отпускаем палец (touchend) — можем, например, проверить, куда элемент дотащили.

На что обратить внимание:

  • touch-action: none; в CSS нужен, чтобы браузер не пытался скроллить или зумить страницу во время касания.
  • e.preventDefault() внутри touchmove, чтобы отключить стандартное поведение (например, скроллинг).
  • Работает только на устройствах с тачскрином (но можно допилить поддержку мышки — через mousedown, mousemove, mouseup).

Пример на проекте Gstaadguy.

	function enableDragToClose() {
		const nav       = document.querySelector(CLASSNAMES.nav);
		const mapMobile = document.querySelector(CLASSNAMES.mapMobile);
		const $btn     = document.querySelector(CLASSNAMES.showMapBtn);
		const body      = document.body;
		if (!nav || !mapMobile) return;

		let startY = 0, dragging = false;

		nav.style.touchAction = 'pan-x';
		nav.style.userSelect  = 'none';

		nav.addEventListener('touchstart', e => {
			startY   = e.touches[0].clientY;
			dragging = true;
			nav.style.transition   = 'none';
			nav.style.willChange   = 'transform, opacity';
		}, { passive: true });

		nav.addEventListener('touchmove', e => {
			if (!dragging) return;
			const diffY = e.touches[0].clientY - startY;
			if (diffY < 0) nav.style.transform = `translateY(${diffY}px)`;
		}, { passive: true });

		nav.addEventListener('touchend', () => {
			dragging = false;
			nav.style.transition = 'transform 1.3s ease 0s, opacity 1.3s ease 0.7s';
			const matrix = window.getComputedStyle(nav).transform;
			const diffY  = matrix !== 'none' ? parseFloat(matrix.split(',')[5]) : 0;

			if (diffY < -120) {
				nav.style.transform = 'translateY(-100vh)';
				nav.style.opacity   = '0';
				body.style.overflow  = '';
				body.style.height    = '';
				$btn.style.display = '';

				setTimeout(() => {
					nav.classList.remove('active');
					nav.style.cssText = ''; // reset inline styles
				}, 2000);
				setTimeout(() => mapMobile.classList.remove('map-active-mobile'), 800);
			} else {
				nav.style.transform = 'translateY(0)';
			}
		});
	}

Что делает функция enableDragToClose?

Функция позволяет пальцем тащить вниз навигационное меню, чтобы закрыть его, если провести достаточно сильно вверх (на самом деле тут свайп “вверх”).

Процесс:

  • Когда ты тянешь меню вверх пальцем — оно двигается вместе с пальцем.
  • Если “потянуть сильно” — меню плавно уходит вверх и исчезает.
  • Иначе — возвращается на место.

Важно:

Подготовка для тач-событий:

    nav.style.touchAction = 'pan-x';
    nav.style.userSelect  = 'none';
    • touchAction: pan-x — разрешает только горизонтальные скроллы (блокирует вертикальные).
    • userSelect: none — запрет выделения текста при касании.

    Обработка touchstart:

    nav.addEventListener('touchstart', e => { ... });

    Когда пользователь касается:

    • Запоминаем, где именно по вертикали палец коснулся (startY).
    • Включаем флаг dragging = true — значит, сейчас тащим.
    • Убираем переходы анимаций (transition: none), чтобы во время перетаскивания не было тормозов.
    • Включаем оптимизацию для браузера (willChange: transform, opacity).

    Обработка touchmove:

    nav.addEventListener('touchmove', e => { ... });

    Пока палец двигается:

    • Если dragging == true, считаем разницу (diffY) — сколько пикселей двинули палец вверх или вниз.
    • Если тянем вверх (потому что diffY < 0), двигаем меню transform: translateY(diffY).

    Обработка touchend:

    nav.addEventListener('touchend', () => { ... });

    Когда отпускаем палец:

    • Выключаем dragging.
    • Ставим обратно плавные анимации на transform и opacity.
    • Читаем реальное текущее смещение через getComputedStyle(nav).transform.

    Проверка: “достаточно ли сильно тянули”:

    if (diffY < -120) { ... }

    Если меню тянули вверх более чем на 120px:

    • Прячем меню (translateY(-100vh), opacity: 0).
    • Восстанавливаем скроллинг страницы (body.style.overflow = '').
    • Показываем кнопку карты ($btn.style.display = '').
    • Через 2 секунды сбрасываем все стили навигации (nav.style.cssText = '') и убираем класс active.
    • Через 0.8 секунд убираем класс активности с карты (mapMobile.classList.remove('map-active-mobile')).

    Иначе:

    • Если тянули слабо — возвращаем меню обратно на место (translateY(0)).

    Кратко: как это выглядит для пользователя

    • Открыто навигационное меню на мобильнике.
    • Ты пальцем тянешь меню вверх.
    • Если тянешь сильно — меню уезжает и закрывается.
    • Если тянешь слабо — меню возвращается назад.

    Тянем.

    Потянули достаточно сильно чтобы меню при отпускании само поехало вверх и закрылось.

    И плавно с opacity пропадет.

    Зачем нужны некоторые штуки:

    • willChange: transform, opacity — браузер будет заранее готовиться к анимации → плавность выше.
    • touchAction: pan-x — чтобы отключить вертикальный скролл на устройстве во время перетаскивания меню.
    • getComputedStyle(nav).transform — чтобы узнать, на сколько именно сдвинули элемент (мы же тащили его пальцем).

    Можно ли было реализовать эту логику по другому?

    Да, конечно, эту логику можно было реализовать по-другому, причём проще и меньше кода.

    Вот несколько альтернативных способов, кратко:

    1. Через Pointer Events

    Вместо touchstart/touchmove/touchend можно использовать один универсальный API:
    pointerdown, pointermove, pointerup.

    Плюсы:

    • Работает и на пальце, и на мышке без дополнительного кода.
    • Проще тестировать на компьютере.

    Минус:

    • Старые браузеры могут не поддерживать (но сейчас почти все норм).

    2. Через requestAnimationFrame

    Вместо того чтобы сразу в touchmove обновлять style.transform,
    можно просто сохранять координаты, а плавное обновление делать через requestAnimationFrame.

    Плюсы:

    • Более плавная анимация.
    • Меньше нагрузка на процессор (особенно на слабых телефонах).

    Минус:

    • Кода чуть больше.

    3. Через готовую библиотеку

    Например, через маленькую библиотеку типа:

    • Hammer.js
    • ZingTouch

    Они умеют сразу определять свайпы, драги и т.п.

    Плюсы:

    • Почти ничего не надо писать самому.
    • Бонус: можно сразу обрабатывать разные жесты (например, pinch, rotate).

    Минус:

    • Подключение сторонней библиотеки ради одного свайпа — может быть излишним.

    4. Через CSS и touch-action: manipulation + overflow: scroll

    Можно было сделать меню с вертикальным скроллом и слушать просто событие scroll:

    • Если скролл ушёл достаточно вверх — закрыть меню.

    Плюсы:

    • Меньше кастомного JavaScript.
    • Всё максимально нативно.

    Минус:

    • Не очень удобно контролировать точную “чувствительность” свайпа.
    • Нужно аккуратно настраивать стили.

    Как бы я сам сделал для реального проекта?

    👉 Я бы выбрал Pointer Events + requestAnimationFrame.
    Это даёт:

    • Поддержку мыши и пальца одновременно.
    • Очень плавную анимацию.
    • Меньше ошибок с производительностью.