1

I'm trying to make a photo slider by using HTML+CSS+JS. Currently it has navigation arrows, dots, and autoplay function. It resets the timer upon each click on arrows/dots. Everything works fine as long as the cycle goes forward. Problem is, I can't figure out how to reverse the animation when the "prev" button or any previous dot than the current one is clicked. I need the slider to go from left to right in that scenarios. Could you please help?

var slider = document.getElementsByClassName("slider")[0];
  var sliderDots = Array.prototype.slice.call(document.getElementsByClassName("dots")[0].children);
  var sliderContents = Array.prototype.slice.call(document.getElementsByClassName("elements")[0].children);
  var sliderArrowLeft = document.getElementsByClassName("arrow-left")[0];
  var sliderArrowRight = document.getElementsByClassName("arrow-right")[0];
  var sliderSpeed = 4000,
    currentSlide = 0,
    currentActive = 0,
    sliderTimer = 0;

  function playSlide(slide) {
    for (var k = 0; k < sliderDots.length; k++) {
      sliderContents[k].classList.remove("active");
      sliderContents[k].classList.remove("inactive");
      sliderDots[k].classList.remove("active");
    }
    if (slide < 0) {
      slide = currentSlide = sliderContents.length - 1;
    }
    if (slide > sliderContents.length - 1) {
      slide = currentSlide = 0;
    }
    if (currentActive != currentSlide) {
      sliderContents[currentActive].classList.add("inactive");
    }
    sliderContents[slide].classList.add("active");
    sliderDots[slide].classList.add("active");
    currentActive = currentSlide;
    clearTimeout(sliderTimer);
    sliderTimer = setTimeout(function() {
      playSlide(currentSlide += 1);
    }, sliderSpeed)
  }
  sliderArrowLeft.addEventListener("click", function() {
    playSlide(currentSlide -= 1);
  })
  sliderArrowRight.addEventListener("click", function() {
    playSlide(currentSlide += 1);
  })
  for (var l = 0; l < sliderDots.length; l++) {
    sliderDots[l].addEventListener("click", function() {
      playSlide(currentSlide = sliderDots.indexOf(this));
    })
  }
  playSlide(currentSlide);
.slider {
  overflow: hidden;
  position: relative;
}

.contents {
  width: 100%;
  height: 200px;
  position: relative;
  overflow: hidden;
}

.arrow-left,
.arrow-right {
  width: 50px;
  height: 50px;
  bottom: 0;
  cursor: pointer;
  display: block;
  text-align: center;
  line-height: 50px;
  background: rgb(200,200,200);
  position: absolute;
  z-index: 2;
}

.arrow-left {
  left: 0;
}

.arrow-right {
  right: 0;
}

.dots {
  height: 14px;
  bottom: 0;
  left: 50%;
  font-size: 0;
  text-align: right;
  position: absolute;
  z-index: 2;
  transform: translateX(-50%);
}

.dot {
  width: 12px;
  height: 12px;
  margin-left: 5px;
  margin-right: 5px;
  display: inline-block;
  cursor: pointer;
  border-style: solid;
  border-width: 1px;
  border-color: rgb(100, 100, 100);
}

.dot.active {
  background: rgb(200, 200, 200);
}

.elements {
  width: 100%;
  overflow: hidden;
  transform: translate(0px);
}

.image {
  width: 100%;
  height: 100px;
  top: 0;
  position: absolute;
  opacity: 0;
}

.image span {
  width: 100%;
  height: 100px;
  text-align: center;
  line-height: 100px;
  display: block;
  background: rgb(200, 200, 200);
}

.image.active {
  position: relative;
  z-index: 1;
  opacity: 1;
}

.image.inactive {
  z-index: -3;
  opacity: 1;
}

.image.active span {
  animation: show .5s ease-in-out forwards;
}

.image.inactive span {
  animation: hide .5s ease-in-out forwards;
}

@keyframes show {
  0% {
    transform: translateX(+100%);
  }
  100% {
    transform: translateX(0);
  }
}

@keyframes hide {
  0% {
    opacity: 1;
    transform: translateX(0);
  }
  100% {
    opacity: 0;
    transform: translateX(-100%);
  }
}
<div class="slider">
  <div class="contents">
    <div class="arrow-left">prev</div>
    <div class="arrow-right">next</div>
    <div class="dots">
      <li class="dot"></li>
      <li class="dot"></li>
      <li class="dot"></li>
    </div>
    <div class="elements">
      <div class="image"><span>image 1</span></div>
      <div class="image"><span>image 2</span></div>
      <div class="image"><span>image 3</span></div>
    </div>
  </div>
</div>
tez
  • 117
  • 1
  • 8

1 Answers1

1

I've changed your playSlide() function to accept an additional argument describing which direction to slide when the controls are clicked.

playSlide() uses the idea of this answer to create a CSS variable --direction which controls the direction of the slider (-1 or 1), and the show and hide keyframes now use the var(--direction) in their calculations to determine whether it should be +100% or -100%.

var timeout;
var timeoutDate;
function setDirection(d) {
  document.querySelector('.slider').style.setProperty('--direction', d);
}
setDirection(1);
var slider = document.getElementsByClassName("slider")[0];
var sliderDots = Array.prototype.slice.call(document.getElementsByClassName("dots")[0].children);
var sliderContents = Array.prototype.slice.call(document.getElementsByClassName("elements")[0].children);
var sliderArrowLeft = document.getElementsByClassName("arrow-left")[0];
var sliderArrowRight = document.getElementsByClassName("arrow-right")[0];
var sliderSpeed = 4000,
  currentSlide = 0,
  currentActive = 0,
  sliderTimer = 0,
  remainingMilliseconds = 0;

let elements = document.querySelector('.elements');
elements.addEventListener('mouseover', function () {
  remainingMilliseconds = sliderSpeed - (new Date().getTime() - timeoutDate.getTime());
  clearTimeout(sliderTimer)
});

elements.addEventListener('mouseout', function () {
  console.log("out")
  setTimeout(
    function () {
      playSlide(++currentSlide);
    },
    remainingMilliseconds
  );

});

function playSlide(slide, d) {
  if (!(d === undefined)) {
    direction = d
    setDirection(d)
    clearTimeout(timeout)
    timeout = setTimeout(function () {
      setDirection(1)
    }, 1000)
  }
  for (var k = 0; k < sliderDots.length; k++) {
    sliderContents[k].classList.remove("active");
    sliderContents[k].classList.remove("inactive");
    sliderDots[k].classList.remove("active");
  }
  if (slide < 0) {
    slide = currentSlide = sliderContents.length - 1;
  }
  if (slide > sliderContents.length - 1) {
    slide = currentSlide = 0;
  }
  if (currentActive != currentSlide) {
    sliderContents[currentActive].classList.add("inactive");
  }
  sliderContents[slide].classList.add("active");
  sliderDots[slide].classList.add("active");
  currentActive = currentSlide;
  clearTimeout(sliderTimer);
  timeoutDate = new Date();
  sliderTimer = setTimeout(function () {
    playSlide(currentSlide += 1);
  }, sliderSpeed)
}
sliderArrowLeft.addEventListener("click", function () {
  playSlide(currentSlide -= 1, -1);
})
sliderArrowRight.addEventListener("click", function () {
  playSlide(currentSlide += 1, 1);
})
for (var l = 0; l < sliderDots.length; l++) {
  sliderDots[l].addEventListener("click", function () {
    let d = sliderDots.indexOf(this) - currentSlide
    console.log('d=' + d)
    playSlide(currentSlide = sliderDots.indexOf(this), Math.sign(d));
  })
}
playSlide(currentSlide);
.slider {
  overflow: hidden;
  position: relative;
}

.contents {
  width: 100%;
  height: 200px;
  position: relative;
  overflow: hidden;
}

.arrow-left,
.arrow-right {
  width: 50px;
  height: 50px;
  bottom: 0;
  cursor: pointer;
  display: block;
  text-align: center;
  line-height: 50px;
  background: rgb(200,200,200);
  position: absolute;
  z-index: 2;
}

.arrow-left {
  left: 0;
}

.arrow-right {
  right: 0;
}

.dots {
  height: 14px;
  bottom: 0;
  left: 50%;
  font-size: 0;
  text-align: right;
  position: absolute;
  z-index: 2;
  transform: translateX(-50%);
}

.dot {
  width: 12px;
  height: 12px;
  margin-left: 5px;
  margin-right: 5px;
  display: inline-block;
  cursor: pointer;
  border-style: solid;
  border-width: 1px;
  border-color: rgb(100, 100, 100);
}

.dot.active {
  background: rgb(200, 200, 200);
}

.elements {
  width: 100%;
  overflow: hidden;
  transform: translate(0px);
}

.image {
  width: 100%;
  height: 100px;
  top: 0;
  position: absolute;
  opacity: 0;
}

.image span {
  width: 100%;
  height: 100px;
  text-align: center;
  line-height: 100px;
  display: block;
  background: rgb(200, 200, 200);
}

.image.active {
  position: relative;
  z-index: 1;
  opacity: 1;
}

.image.inactive {
  z-index: -3;
  opacity: 1;
}

.image.active span {
  animation: show .5s ease-in-out forwards;
}

.image.inactive span {
  animation: hide .5s ease-in-out forwards;
}

@keyframes show {
  0% {
    transform: translateX(calc(var(--direction)*100%));
  }
  100% {
    transform: translateX(0);
  }
}

@keyframes hide {
  0% {
    opacity: 1;
    transform: translateX(0);
  }
  100% {
    opacity: 0;
    transform: translateX(calc(-1*var(--direction)*100%));
  }
}
<div class="slider">
  <div class="contents">
    <div class="arrow-left">prev</div>
    <div class="arrow-right">next</div>
    <div class="dots">
      <li class="dot"></li>
      <li class="dot"></li>
      <li class="dot"></li>
    </div>
    <div class="elements">
      <div class="image"><span>image 1</span></div>
      <div class="image"><span>image 2</span></div>
      <div class="image"><span>image 3</span></div>
    </div>
  </div>
</div>
kmoser
  • 8,780
  • 3
  • 24
  • 40
  • Thank you for the suggestion. Arrows work accurate, but dots are still bugged. When an earlier dot is clicked, animation moves still forward. Can you fix it too? – tez Aug 13 '23 at 15:45
  • @tez I have updated my code so the dots work in the corresponding direction: if you click a dot for a slide that is before the current slide it animates left-to-right, and if you click a dot that is after the current slide it animates right-to-left. – kmoser Aug 13 '23 at 21:19
  • Thank you, but dots are still bugged. Ie. when the loop is on the first dot, and when I click on the third dot, sliding animation adds an empty white space between all images. And the loop continues with this white space... Isn't there an alternative for the `--direction` method? Tbh I don't want to add `--direction: 1;` value in the `body` css. It may cause issues with my other functions on the same page – tez Aug 13 '23 at 22:55
  • @tez I've refactored my code so the dots behave as intended, with only one image being scrolled into view at a time which corresponds to the dot being clicked. I've also gotten rid of the Javascript global `direction` variable, as well as the explicit CSS `--direction` variable (although the JS still sets that variable internally so it can be accessed by the animation keyframes). – kmoser Aug 14 '23 at 02:30
  • Thank you, now more decent. However, after the prev button or an earlier dot is clicked once, animation continues backwards always. It should show the reversed animation, and then go back to the normal behaviour automatically. Image cycle goes back to normal, but the sliding animation stays at the "from right to left" behaviour... Btw, just wondering, is it maybe possible to assign `--direction` to any other div (i.e. slider main div) rather than `body` please? – tez Aug 14 '23 at 07:50
  • @tez I've made those changes. The reversing is a bit of a hack because I used `setTimeout()` to force the direction to forwards after the animation has ended. There may be a simpler way to do that. I've also moved the `--direction` variable to the `.slider` element. – kmoser Aug 14 '23 at 21:42
  • When you click "next" button and then "prev" button immediately, the animation seems bugged. Same happens when clicking previous/next dots as well. I'm afraid `setTimeout()` is not good idea – tez Aug 14 '23 at 22:03
  • @tez It should be fixed now. I just had to clear the timeout with `clearTimeout()`. – kmoser Aug 15 '23 at 02:53
  • Now it works exactly as it should be, thank you very much! I'm convinced to click the accepted answer button, but may I ask a small addition since you're already familiar with my codes, please? Do you know how can I pause the autoplay timer when the mouse hovers `slider` div? (not reset, but pause) – tez Aug 15 '23 at 09:35
  • @tez I've updated the code so it pauses when you hover over `.elements`. You don't want to pause when hovering over `.slider` because that would include things like the dots and the arrows. – kmoser Aug 15 '23 at 14:44
  • Oh thank you! But `clearTimeout` resets the timer. Do you know how to "pause" the timer, so it can continue the countdown after `mouseout`, rather than starting over? – tez Aug 15 '23 at 15:00
  • @tez What would be the expected behavior when the user mouses over for a duration that is not an exact multiple of `sliderSpeed`: would you expect the next slide to start immediately, or only when the clock has hit the next even multiple of `sliderSpeed`? I think this is getting into unnecessary minutiae. If you just want the next slide to play immediately on mouseout, in the `mouseout` handler you could change `playSlide(currentSlide)` to `playSlide(++currentSlide)`. – kmoser Aug 15 '23 at 15:39
  • Sorry if I wasn't clear. I mean, slider speed is set to 4000ms. For example, when mouse hovers over `.elements` at 3000ms, I need to pause the timer. And the moment mouse leaves `.elements`, I need the timer to count down the remaining 1000ms, and then move to the next slide... If it's a difficult function, that's ok. I'm thankful for all your helps, and ready to click the accept button anyway – tez Aug 15 '23 at 16:11
  • @tez Not so hard, I updated it to do that. – kmoser Aug 15 '23 at 18:46
  • Thanks but it gets bugged easily when I start clicking the arrows/dots... Anyways, your solution answers my first post already. I'm thankful for all your assistance once again! – tez Aug 15 '23 at 19:28