JavaScript touch slider

When I started learning JavaScript, I really wanted to understand how sliders work and are made that can be flipped with swipe or mouse, but I did not find materials with a good explanation of exactly what I need. After some time, I managed to do something similar. And now I want to write an article about this, so that other people who want to understand how it all works and make touch events for the slider (and not only) make it easier to figure it out. I will try to set out in order and support the explanations with illustrative examples.


I do not consider myself a great specialist in JavaScript, there is always something to learn, so if you know how to write some code fragments better / easier / more efficiently, be sure to write in the comments.


Content:


  1. What functionality will we do?
  2. Writing HTML and CSS
  3. How will it work?
  4. Writing javascript
  5. Total
  6. Full version code

You can immediately see an example .



1. What functionality will we do?


Let's write with 0 a simple slider that will have the following functions:


  • we implement touch and drag turning over slides;
  • switch slides using the arrow keys;
  • lock the arrow keys โ† and โ†’ on the first and last slide, respectively.

.
- , - , -, , , .. . . - , .



2. HTML CSS


HTML CSS?


(1 ) 200200 . - .
:



.
. , .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 = posInit โ€“ posX1 ( , 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 .


  • The slider can be "grabbed" when the slide has not yet finished moving.

To fix this behavior, you need to declare a variable allowSwipeand adjust the swipe ban on it.


I will not describe it in detail. Just lay out this code below .
And in the example, all these conditions will already be made. Also, for clarity, the cursor on the slider changes.



Full code (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();
  });


The most complete code
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


All Articles