Touch slider на JavaScript

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Когда я начинал изучать JavaScript, мне очень хотелось понять как работают и делаются слайдеры, которые можно перелистывать свайпами или мышью, но материалов с хорошим объяснением именно того, что мне надо, я не нашел. Через какое-то время мне удалось сделать нечто подобное. И теперь я хочу написать об этом статью, чтобы другим людям, которые хотят понять как это все работает и сделать touch события для слайдера (и не только), было проще разобраться. Я постараюсь излагать по порядку и подкреплять объяснения наглядными примерами.


Я не считаю себя большим специалистом в JavaScript, всегда есть чему учиться, поэтому если знаете как написать какие-то фрагменты кода лучше/проще/эффективнее — обязательно напишите в комментарии.


Содержание:


  1. Какой функционал будем делать?
  2. Пишем HTML и CSS
  3. Как это будет работать?
  4. Пишем JavaScript
  5. Итого
  6. Полная версия кода

Можно сразу посмотреть пример.



1. Какой функционал будем делать?


Напишем с 0 простенький слайдер, который будет иметь следующие функции:


  • реализуем touch и drag перелистывание слайдов;
  • переключение слайдов с помощью стрелок-переключателей;
  • блокировка стрелок-переключателей ← и → на первом и последнем слайде соответственно.

Звучит совсем просто и я постараюсь писать тоже просто и понятно.
Если кому-нибудь будет интересно, то мы можем добавлять потом какой-то функционал, вроде точек-переключателей, бесконечной прокрутки слайдов, вертикальной ориентации, переключение слайдов с помощью клавиатуры и т.д. В конце концов можно завернуть в компонент. Напишите об этом в комментарии. Если будет хотя бы несколько комментариев за какой-то функционал, то реализуем его.



2. Пишем HTML и CSS


Что должен представлять из себя слайдер с точки зрения HTML и CSS?


Видимая часть слайдера (1 слайд) будет размером 200х200 пикселей. Снизу расположим стрелки-переключатели слайдов.
В итоге у нас будет вот это:



Рассмотрим подробнее изнутри.
Несколько слайдов должны идти по порядку и располагаться горизонтально в одну линию. Для этого нужно поместить их в отдельный блок, класс которого будет .slider-track и зададим ему display: flex. Он должен вмещать в себя все слайды горизонтально, то есть быть достаточной ширины, в этом примере зададим для слайдов flex-shrink: 0, чтобы они не сжимались.



Далее желательно скрыть все слайды кроме текущего. Для этого придется обернуть наш track в еще один блок, назовем его .slider-list. Ширина и высота у него будет равна ширине и высоте одного слайда, также у него будет overflow: hidden. Таким образом виден будет только один слайд, а остальные скрыты.



Теперь нужно создать стрелки-переключатели. Для бо́льшего удобства мы положим их в отдельный блок .slider-arrows и расположим его снизу слайдера. Но за пределами .slider-list (overflow: hidden) их не будет видно, поэтому можно:


  • установить для .slider-list padding-bottom и в образовавшееся пустое место разместить блок со стрелками;
  • обернуть .slider-list в еще один блок, который можно назвать просто .slider и внутри него (или за его пределами) располагать стрелки в удобном месте.

Для бо́льшей простоты все размеры будут установлены через CSS.


HTML будет выглядеть так:


<div class="slider">
  <div class="slider-list">
    <div class="slider-track">
      <div class="slide">1</div>
      <div class="slide">2</div>
      <div class="slide">3</div>
      <div class="slide">4</div>
      <div class="slide">5</div>
    </div>
  </div>
  <div class="slider-arrows">
    <button type="button" class="prev">&larr;</button>
    <button type="button" class="next">&rarr;</button>
  </div>
</div>

&larr; и &rarr; это спецсимволы HTML-разметки, которые представляют собой стрелки, направленные в левую и правую сторону соответственно.


CSS будет таким:


.slider {
  position: relative;
  width: 200px;
  height: 200px;
  margin: 50px auto 0;
  /* Чтобы во время перетаскивания слайда ничего не выделить внутри него */
  user-select: none;
  /* Чтобы запретить скролл страницы, если мы начали двигать слайдер по оси X */
  touch-action: pan-y;
}

/* Если где-то внутри слайдера будут изображения,
то нужно задать им pointer-events: none,
чтобы они не перетаскивались мышью */

.slider img {
  poiner-events: none;
}

.slider-list {
  width: 200px;
  height: 200px;
  overflow: hidden;
}

.slider-list.grab {
  cursor: grab;
}

.slider-list.grabbing{
  cursor: grabbing;
}

.slider-track {
  display: flex;
}

.slide {
  width: 200px;
  height: 200px;
  /* Чтобы слайды не сжимались */
  flex-shrink: 0;
  /* Увеличиваем и центрируем цифру внутри слайда */
  font-size: 50px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid #000;
}

.slider-arrows {
  margin-top: 15px;
  text-align: center;
}

.next,
.prev {
  background: none;
  border: none;
  margin: 0 10px;
  font-size: 30px;
  cursor: pointer;
}

.next.disabled,
.prev.disabled {
  opacity: .25;
  pointer-events: none;
}

И выглядеть это будет так:



Без overflow: hidden:



С этим просто, теперь перейдем к обсуждению логики работы слайдера.



3. Как это будет работать?


Двигать слайдер проще и лучше всего через transform: translate3d(x, y, z), чтобы не вызывать лишние перерисовки в браузере. Также можно использовать transform: translateX(x) в комбинации с will-change: transform. Считывать style мы будем при помощи встроенного метода строк match().
Первым делом нужно отслеживать номер текущего слайда в переменной slideIndex. Мы уже условились, что все слайды одной ширины (200px). Перелистываться слайды будут очень просто — в свойство transform: translate3d(x, y, z) в позицию x устанавливается следующее выражение: номер слайда * ширина слайда. Так и будет происходить переключение слайдов и напрямую это будет работать на стрелках-переключателях.


Со свайпами посложнее, разберем подробнее.


Мы будем использовать три основных события в браузере. При касании пальцем срабатывает событие touchstart (при зажатии мыши mousedown), при движении пальцем по экрану — touchmove (mousemove), при отпускании пальца — touchend (mouseup).


Нам нужно будет работать с координатами курсора event.clientX (место касания экрана) следующим образом:


  1. Создадим 3 основных функции для работы со свайпами — swipeStart, swipeAction и swipeEnd.


  2. При первом касании записываем координаты касания (курсора) по оси X (переменные posX1 и posInit, где posX1 в дальнейшем будет меняться, а posInit статичная).


  3. При движении курсора, вычитаем текущие координаты из posX1, записывая результат в переменную posX2, которая потом будет изменять style.transform, чтобы двигать слайды. И перезаписываем posX1 текущими координатами.
    Разберем по порядку подробнее:


    • posX1 и posInit — координаты, полученные при первом касании (текущие координаты курсора event.clientX). Это первое касание экрана в swipeStart, например, если ширина нашего экрана 320px, то касание по центру установит posInit и posX1 в 160. В swipeAction переменная posX1 будет перезаписываться.


    • posX2 — разность posX1 и event.clientX. Будет считаться каждый раз при движении по экрану в swipeAction, например, если мы сдвинули палец чуть-чуть вправо, то "текущие координаты" = 161, значит 160 — 161 = -1, будет смещение на -1px. Нагляднее можно увидеть ниже:


    Таким образом, при движении пальцем по экрану, мы каждый раз считаем смещение курсора по оси X, относительно его предыдущего положения и переменная posX2 всегда будет содержать количество пикселей, на которое мы сдвинули палец по экрану. Обычно это число от 0.5 до 10, в зависимости от размаха (если двинуть палец очень резко, то будут бо́льшие числа, а если палец двигать медленно, то меньшие).


  4. При прекращении свайпа, мы вычитаем из начальной позиции курсора текущую и сравниваем полученное значение (posFinal) со значением "порога" сдвига слайда (posThreshold), который мы определим заранее.
    Разберем подробнее с примерами. Некоторые действия мы пока опустим. Они не столь важны для этого объяснения, рассмотрим только самые основные:


    • posFinal = posInitposX1 (так мы получим количество пикселей, на которое провели пальцем в swipeAction, например, если ширина слайдера 200px, то если мы проведем пальцем от середины слайдера до его края и отпустим, posFinal будет равен 100).


    • posThreshold = ширина слайда (200px) * 0,3 = 60. С этим числом мы будем сравнивать posFinal, например, если posFinal > 60, то переключаем слайд, иначе возвращаем в начальное положение. Само условие:
      if (posFinal >= posThreshold) nextSlide() или prevSlide(); else currentSlide();

    Если со вторым условием else более-менее понятно, то с первым нужно разобраться подробнее.


    Допустим мы превысили порог posThreshold и будем переключать слайд, но теперь нужно понять в какую сторону мы двинули слайд, чтобы вызвать prev() или next() действие. Для этого мы будем сравнивать posInit и posX1. Пусть ширина нашего экрана 320px, чи́сла ширины идут слева направо, то есть 0px-320px (в самой левой точке экрана 0, в самой правой точке экрана 320). Представим, что слайдер расположен по центру, прикладываем палец в самый центр и получаем posInit 160. Теперь, если мы будем вести палец влево (допустим до края экрана), то posX1 будет уменьшаться с 160 до 0. Если будем вести палец вправо до края экрана, то posX1 будет увеличиваться c 160 до 320. Итак, мы приложили палец в центр и провели немного влево, при окончании свайпа posInit 160, а posX1 100, значит мы прошли порог posThreshold и нам нужно показывать следующий слайд. Получается группа условий:


    if (posInit > posX1) nextSlide(); else if (posInit < posX1) prevSlide();

    Еще возможен вариант, когда мы прислонили палец и сразу убрали, вроде события click. В таком случае posInit === posX1. Этот вариант мы тоже должны предусмотреть, но об этом позже.



Надеюсь у меня получилось объяснить правильно и понятно. Теперь приступим к написанию JavaScript.



4. Пишем JavaScript


Ниже я опишу кратко алгоритм еще раз, но помимо основного алгоритма работы слайдера, будет еще не мало разных условий для "фиксов" не нужного нам поведения, которое будет приводить к разным странностям, либо будет ломать слайдер. Сначала мы сделаем просто переключение слайдов свайпами, а фиксить будем уже потом. Я постараюсь делать все объяснения и приводить примеры, сохраняя всю последовательность.


Кратко об основном еще раз:


  • .slider-track будет двигаться при помощи transfrom: translate3d(Xpx, 0px, 0px);
  • изначально зададим ему этот стиль transfrom: translate3d(0px, 0px, 0px) в объект style, чтобы можно было его считывать;
  • мы будем считывать текущую трансформацию из style.transform с помощью встроенного метода строк match() и изменять ее в зависимости от движения курсора;
  • в самом начале назначим функцию swipeStart на .slider-list с помощью слушателя событий touchstart и mousedown;
  • в функции swipeStart мы будем назначать уже на document функции swipeAction и swipeEnd с помощью слушателя событий touchmove (mousemove) и touchend (mouseup) соответственно, позже удалять;
  • во время свайпа при проходе определенного числового порога posThreshold, мы будем увеличивать или уменьшать slideIndex и вызывать функцию переключения слайда;
  • если порог posThreshold пройден не был, то вернем слайдер в начальное положение;
  • при клике на стрелки-переключатели, слайды будут переключаться.

Полная версия кода без разрывов на пояснения будет снизу.
Полная версия кода с фиксами нежелательного поведния еще ниже


Первым делом получим наши элементы со страницы. При такой очевидной HTML-разметке хорошим вариантом было бы получить элементы через один querySelector и свойства DOM, но для простоты возьмем все через querySelector. Также объявим все нужные для работы переменные и основную функцию, которая будет переключать слайды. Пояснение я дам ниже.


let slider = document.querySelector('.slider'),
  sliderList = slider.querySelector('.slider-list'),
  sliderTrack = slider.querySelector('.slider-track'),
  slides = slider.querySelectorAll('.slide'),
  arrows = slider.querySelector('.slider-arrows'),
  prev = arrows.children[0],
  next = arrows.children[1],
  slideWidth = slides[0].offsetWidth,
  slideIndex = 0,
  posInit = 0,
  posX1 = 0,
  posX2 = 0,
  posFinal = 0,
  posThreshold = slideWidth * .35,
  trfRegExp = /[-0-9.]+(?=px)/,
  slide = function() {
    sliderTrack.style.transition = 'transform .5s';
    sliderTrack.style.transform = `translate3d(-${slideIndex * slideWidth}px, 0px, 0px)`;

    // делаем стрелку prev недоступной на первом слайде
    // и доступной в остальных случаях
    // делаем стрелку next недоступной на последнем слайде
    // и доступной в остальных случаях
    prev.classList.toggle('disabled', slideIndex === 0);
    next.classList.toggle('disabled', slideIndex === --slides.length);
  } ...

trfRegExp — переменная с инициализацией регулярного выражения, которое мы будем использовать для считывания свойства transform у нашего .slider-track.


Если для событий мыши нужные нам координаты курсора лежат в event.clientX, то для тач событий они лежат в массиве touches — event.touches[0].clientX. Значит обращаться к свойствам event нужно через условие и т.к. нам нужно получать координаты в двух местах, то можно обернуть условие в функцию getEvent. Мы будем проверять event.type на содержание подстроки touch и в зависимости от результата возвращать первый элемент массива touch или просто event. И сразу же пишем функции дальше.


getEvent = function() {
  return event.type.search('touch') !== -1 ? event.touches[0] : event;
  // p.s. event - аргумент по умолчанию в функции
},
// или es6
getEvent = () => event.type.search('touch') !== -1 ? event.touches[0] : event,

swipeStart = function() {
  let evt = getEvent();

  // берем начальную позицию курсора по оси Х
  posInit = posX1 = evt.clientX;

  // убираем плавный переход, чтобы track двигался за курсором без задержки
  // т.к. он будет включается в функции slide()
  sliderTrack.style.transition = '';

  // и сразу начинаем отслеживать другие события на документе
  document.addEventListener('touchmove', swipeAction);
  document.addEventListener('touchend', swipeEnd);
  document.addEventListener('mousemove', swipeAction);
  document.addEventListener('mouseup', swipeEnd);
},
swipeAction = function() {
  let evt = getEvent(),
    // для более красивой записи возьмем в переменную текущее свойство transform
    style = sliderTrack.style.transform,
    // считываем трансформацию с помощью регулярного выражения и сразу превращаем в число
    transform = +style.match(trfRegExp)[0];

  posX2 = posX1 - evt.clientX;
  posX1 = evt.clientX;

  sliderTrack.style.transform = `translate3d(${transform - posX2}px, 0px, 0px)`;
  // можно было бы использовать метод строк .replace():
  // sliderTrack.style.transform = style.replace(trfRegExp, match => match - posX2);
  // но в дальнейшем нам нужна будет текущая трансформация в переменной
} ...

Пояснение к функции swipeAction:
В этой функции мы изменяем свойство transform. Разберем подробнее:
С помощью регулярного выражения ищем в строке translate3d(0px, 0px, 0px) первое вхождение подстроки "ЧИСЛОpx". Изменяем его и устанавливаем полученное число обратно в свойство transform. Оно отвечает за сдвиг по оси Х.
Регулярное выражение выглядит так: [-0-9.]+(?=px), разберем его подробнее:


  • [-0-9.] — эта группа говорит, что мы ищем или "тире" или "цифру от 0 до 9" или "точку";
  • + — после предыдущей группы говорит, что любой из этих символов может быть 1 или более раз, это позволит нам найти различные сочетания, например: 5, 101.10, -19, -12.5 и т.д.;
  • (?=px) — гворит, что мы ищем предыдущую группу цифр, только если за ними следует "px".

И на этом моменте наш .slider-track уже можно двигать, но не забываем предварительно задать ему sliderTrack.style.transform = 'translate3d(0px, 0px, 0px)', чтобы в функции swipeAction было что менять. Результат:



Но когда мы завершаем свайп (отпускаем мышь или убираем палец от экрана) — функция swipeAction продолжает выполняться, .slider-track двигается за мышью по оси Х. Мы уже привязали функцию swipeEnd к нужным событиями, но до объявления функции не дошли, объявим функцию и сделаем пояснение:


swipeEnd = function() {
  // финальная позиция курсора
  posFinal = posInit - posX1;

  document.removeEventListener('touchmove', swipeAction);
  document.removeEventListener('mousemove', swipeAction);
  document.removeEventListener('touchend', swipeEnd);
  document.removeEventListener('mouseup', swipeEnd);

  // убираем знак минус и сравниваем с порогом сдвига слайда
  if (Math.abs(posFinal) > posThreshold) {
    // если мы тянули вправо, то уменьшаем номер текущего слайда
    if (posInit < posX1) {
      slideIndex--;
    // если мы тянули влево, то увеличиваем номер текущего слайда
    } else if (posInit > posX1) {
      slideIndex++;
    }
  }

  // если курсор двигался, то запускаем функцию переключения слайдов
  if (posInit !== posX1) {
    slide();
  }

};

Теперь touch и drag события для слайдера полностью работают, слайды переключаются. Напоминаю: если posFinal будет больше posThreshold, то переключаем слайд, иначе, он возвращается в изначальное положение. Благодаря функции Math.abs() знак минус опускается.



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


arrows.addEventListener('click', function() {
  let target = event.target;

  if (target === next) {
    slideIndex++;
  } else if (target === prev) {
    slideIndex--;
  } else {
    return;
  }

  slide();
});

sliderTrack.style.transform = 'translate3d(0px, 0px, 0px)';

slider.addEventListener('touchstart', swipeStart);
slider.addEventListener('mousedown', swipeStart);

Теперь слайдер будет переключаться и с помощью стрелок. Обработчики событий снизу привязывают начальные функции на слайдер.



Итого


И вот слайдер полностью работает. Слайды перелистываются tocuh событиями, drag событиями и стрелками-переключателями. Но есть еще некоторое поведение, которое может его сломать, посмотрим на примерах ниже:


  • Когда мы скроллим страницу вверх или вниз с телефона, мы прислоняем палец к экрану и делаем им взмах. В момент взмаха, палец может уходить в сторону и если этот взмах был на слайдере, то наше действие будет сдвигать слайдер в сторону:


В этом случае нужно отслеживать координаты курсора и по оси Y, затем разрешать или запрещать свайп, в зависимости от того какое действие мы совершаем (свайп слайдера или скролл страницы).


  • Мы можем тянуть слайдер в сторону, когда с другой стороны пусто:


В этом случае нужно на первом и последнем слайде отслеживать в какую сторону мы тянем слайд и завершать работу функции, если на первом слайде мы тянем вправо, а на последнем слайде тянем влево.


  • Слайды можно тащить в любую сторону, пока позволяет ширина экрана:


В этом случае также нужно определять в какую сторону мы тянем слайд и в зависимости от стороны, сравнивать текущую трансформацию с трансформацией следующего или предыдущего слайда. Чтобы каждый раз не считать трансформацию следующего и предыдущего слайда в функции swipeAction, нужно в функции swipeStart обновлять их 1 раз.


  • Слайдер можно "схватить", когда слайд еще не закончил перемещение.

Для исправления этого поведения нужно объявить переменную allowSwipe и регулировать ей запрет свайпа.


Описывать это подробно я уже не буду. Просто выложу этот код ниже.
И в примере все эти условия уже будут сделаны. Также для наглядности меняется курсор на слайдере.



Полный код (swipe, drag, arrows)
let slider = document.querySelector('.slider'),
  sliderList = slider.querySelector('.slider-list'),
  sliderTrack = slider.querySelector('.slider-track'),
  slides = slider.querySelectorAll('.slide'),
  arrows = slider.querySelector('.slider-arrows'),
  prev = arrows.children[0],
  next = arrows.children[1],
  slideWidth = slides[0].offsetWidth,
  slideIndex = 0,
  posInit = 0,
  posX1 = 0,
  posX2 = 0,
  posFinal = 0,
  posThreshold = slides[0].offsetWidth * 0.35,
  trfRegExp = /([-0-9.]+(?=px))/,
  getEvent = function() {
    return (event.type.search('touch') !== -1) ? event.touches[0] : event;
  },
  slide = function() {
    if (transition) {
      sliderTrack.style.transition = 'transform .5s';
    }
    sliderTrack.style.transform = `translate3d(-${slideIndex * slideWidth}px, 0px, 0px)`;

    prev.classList.toggle('disabled', slideIndex === 0);
    next.classList.toggle('disabled', slideIndex === --slides.length);
  },
  swipeStart = function() {
    let evt = getEvent();

    posInit = posX1 = evt.clientX;

    sliderTrack.style.transition = '';

    document.addEventListener('touchmove', swipeAction);
    document.addEventListener('mousemove', swipeAction);
    document.addEventListener('touchend', swipeEnd);
    document.addEventListener('mouseup', swipeEnd);
  },
  swipeAction = function() {

    let evt = getEvent(),
      style = sliderTrack.style.transform,
      transform = +style.match(trfRegExp)[0];

    posX2 = posX1 - evt.clientX;
    posX1 = evt.clientX;

    sliderTrack.style.transform = `translate3d(${transform - posX2}px, 0px, 0px)`;
  },
  swipeEnd = function() {
    posFinal = posInit - posX1;

    document.removeEventListener('touchmove', swipeAction);
    document.removeEventListener('mousemove', swipeAction);
    document.removeEventListener('touchend', swipeEnd);
    document.removeEventListener('mouseup', swipeEnd);

    if (Math.abs(posFinal) > posThreshold) {
      if (posInit < posX1) {
        slideIndex--;
      } else if (posInit > posX1) {
        slideIndex++;
      }
    }

    if (posInit !== posX1) {
      slide();
    }
  };

  sliderTrack.style.transform = 'translate3d(0px, 0px, 0px)';

  slider.addEventListener('touchstart', swipeStart);
  slider.addEventListener('mousedown', swipeStart);

  arrows.addEventListener('click', function() {
    let target = event.target;

    if (target === next) {
      slideIndex++;
    } else if (target === prev) {
      slideIndex--;
    } else {
      return;
    }

    slide();
  });


Самый полный код
let slider = document.querySelector('.slider'),
  sliderList = slider.querySelector('.slider-list'),
  sliderTrack = slider.querySelector('.slider-track'),
  slides = slider.querySelectorAll('.slide'),
  arrows = slider.querySelector('.slider-arrows'),
  prev = arrows.children[0],
  next = arrows.children[1],
  slideWidth = slides[0].offsetWidth,
  slideIndex = 0,
  posInit = 0,
  posX1 = 0,
  posX2 = 0,
  posY1 = 0,
  posY2 = 0,
  posFinal = 0,
  isSwipe = false,
  isScroll = false,
  allowSwipe = true,
  transition = true,
  nextTrf = 0,
  prevTrf = 0,
  lastTrf = --slides.length * slideWidth,
  posThreshold = slides[0].offsetWidth * 0.35,
  trfRegExp = /([-0-9.]+(?=px))/,
  getEvent = function() {
    return (event.type.search('touch') !== -1) ? event.touches[0] : event;
  },
  slide = function() {
    if (transition) {
      sliderTrack.style.transition = 'transform .5s';
    }
    sliderTrack.style.transform = `translate3d(-${slideIndex * slideWidth}px, 0px, 0px)`;

    prev.classList.toggle('disabled', slideIndex === 0);
    next.classList.toggle('disabled', slideIndex === --slides.length);
  },
  swipeStart = function() {
    let evt = getEvent();

    if (allowSwipe) {

      transition = true;

      nextTrf = (slideIndex + 1) * -slideWidth;
      prevTrf = (slideIndex - 1) * -slideWidth;

      posInit = posX1 = evt.clientX;
      posY1 = evt.clientY;

      sliderTrack.style.transition = '';

      document.addEventListener('touchmove', swipeAction);
      document.addEventListener('mousemove', swipeAction);
      document.addEventListener('touchend', swipeEnd);
      document.addEventListener('mouseup', swipeEnd);

      sliderList.classList.remove('grab');
      sliderList.classList.add('grabbing');
    }
  },
  swipeAction = function() {

    let evt = getEvent(),
      style = sliderTrack.style.transform,
      transform = +style.match(trfRegExp)[0];

    posX2 = posX1 - evt.clientX;
    posX1 = evt.clientX;

    posY2 = posY1 - evt.clientY;
    posY1 = evt.clientY;

    // определение действия свайп или скролл
    if (!isSwipe && !isScroll) {
      let posY = Math.abs(posY2);
      if (posY > 7 || posX2 === 0) {
        isScroll = true;
        allowSwipe = false;
      } else if (posY < 7) {
        isSwipe = true;
      }
    }

    if (isSwipe) {
      // запрет ухода влево на первом слайде
      if (slideIndex === 0) {
        if (posInit < posX1) {
          setTransform(transform, 0);
          return;
        } else {
          allowSwipe = true;
        }
      }

      // запрет ухода вправо на последнем слайде
      if (slideIndex === --slides.length) {
        if (posInit > posX1) {
          setTransform(transform, lastTrf);
          return;
        } else {
          allowSwipe = true;
        }
      }

      // запрет протаскивания дальше одного слайда
      if (posInit > posX1 && transform < nextTrf || posInit < posX1 && transform > prevTrf) {
        reachEdge();
        return;
      }

      // двигаем слайд
      sliderTrack.style.transform = `translate3d(${transform - posX2}px, 0px, 0px)`;
    }

  },
  swipeEnd = function() {
    posFinal = posInit - posX1;

    isScroll = false;
    isSwipe = false;

    document.removeEventListener('touchmove', swipeAction);
    document.removeEventListener('mousemove', swipeAction);
    document.removeEventListener('touchend', swipeEnd);
    document.removeEventListener('mouseup', swipeEnd);

    sliderList.classList.add('grab');
    sliderList.classList.remove('grabbing');

    if (allowSwipe) {
      if (Math.abs(posFinal) > posThreshold) {
        if (posInit < posX1) {
          slideIndex--;
        } else if (posInit > posX1) {
          slideIndex++;
        }
      }

      if (posInit !== posX1) {
        allowSwipe = false;
        slide();
      } else {
        allowSwipe = true;
      }

    } else {
      allowSwipe = true;
    }

  },
  setTransform = function(transform, comapreTransform) {
    if (transform >= comapreTransform) {
      if (transform > comapreTransform) {
        sliderTrack.style.transform = `translate3d(${comapreTransform}px, 0px, 0px)`;
      }
    }
    allowSwipe = false;
  },
  reachEdge = function() {
    transition = false;
    swipeEnd();
    allowSwipe = true;
  };

sliderTrack.style.transform = 'translate3d(0px, 0px, 0px)';
sliderList.classList.add('grab');

sliderTrack.addEventListener('transitionend', () => allowSwipe = true);
slider.addEventListener('touchstart', swipeStart);
slider.addEventListener('mousedown', swipeStart);

arrows.addEventListener('click', function() {
  let target = event.target;

  if (target.classList.contains('next')) {
    slideIndex++;
  } else if (target.classList.contains('prev')) {
    slideIndex--;
  } else {
    return;
  }

  slide();
});

codepen.io

Источник: https://habr.com/ru/post/501258/


Интересные статьи

Интересные статьи

Работа со строками может оказаться непростым делом из-за того, что она подразумевает решение множества разноплановых задач. Например, для простого приведения строки к «верблюжьему» стилю понадоби...
Нет более быстрого способа замедлить сайт (такой вот каламбур), чем использовать на нём кучу JavaScript-кода. При использовании JavaScript приходится расплачиваться за это производительностью про...
Доброго времени суток, друзья! Простые одностраничные приложения, основанные на React, Vue или чистом JavaScript, окружают нас повсюду. Хороший «одностраничник» предполагает соответствующи...
Бизнес-смыслы появились в Битриксе в начале 2016 года, но мало кто понимает, как их правильно использовать для удобной настройки интернет-магазинов.
Перед вами перевод статьи из блога Better Programming на сайте Medium.com. Автор, Indrek Lasn, рассказывает об инструментах для отладки кода, которые предоставляет JavaScript. ...