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

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 6

      +3

      Подход с posThreshold будет плохо работать на больших слайдерах во всю ширину экрана — пользователю придётся делать длинные размашистые движения мышкой/пальцем, чтобы переключить этот чёртов слайд. Лучше вместо координат считать скорость курсора и переключать слайд в зависимости от вектора направления скорости — тогда будет достаточно сколь угодно малого движения

        0
        Это имеет смысл, спасибо)
        0

        Сейчас простые слайдеры проще реализовывать с помощью scroll-snap-type: mandatory

          0
          не знаю вкурсе вы или нет, но сейчас то что вы делаете, делается в пару строк кода
          почитайте про CSS Scroll Snapping
            0

            не благодарите


            <div class="container">
              <section class="child"></section>
              <section class="child"></section>
              <section class="child"></section>
            </div>

            .container {
              scroll-snap-type: y mandatory;
            }
            
            .child {
              scroll-snap-align: start;
            }

            image

              0
              Вы ошиблись статьей. Статья называется Touch slider на JavaScript. И эта реализация решает не ту же задачу, что описана в статье. Но вещь однозначно интересная и заслуживает внимания)

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое