3

UPDATE: the solution I have posted below is not good enough, because it makes all the bullets except the active one non-responsive to clicks, instead of queueing them, so there is room for improvement.


I am working on a custom image carousel, using jQuery and CSS. My aim is to make it really lightweight but with (just) enough features: "bullets", auto-advance, responsiveness.

It works fine, but I have discovered a bug I was unable to fix: when I click 2 bullets in rapid succession - which means clicking the second before the transition triggered by the first is finished - the transitions overlap in a weird manner I can not describe but is visible below:

var $elm = $('.slider'),
  $slidesContainer = $elm.find('.slider-container'),
  slides = $slidesContainer.children('a'),
  slidesCount = slides.length,
  slideHeight = $(slides[0]).find('img').outerHeight(false),
  animationspeed = 1500,
  animationInterval = 7000;

// Set (initial) z-index for each slide
var setZindex = function() {
  for (var i = 0; i < slidesCount; i++) {
    $(slides[i]).css('z-index', slidesCount - i);
  }
};
setZindex();

var displayImageBeforeClick = null;

var setActiveSlide = function() {
  $(slides).removeClass('active');
  $(slides[activeIdx]).addClass('active');
};

var advanceFunc = function() {
  if ($('.slider-nav li.activeSlide').index() + 1 != $('.slider-nav li').length) {
    $('.slider-nav li.activeSlide').next().find('a').trigger('click');
  } else {
    $('.slider-nav li:first').find('a').trigger('click');
  }
}

var autoAdvance = setInterval(advanceFunc, animationInterval);

//Set slide height
$(slides).css('height', slideHeight);

// Append bullets
if (slidesCount > 1) {
  /* Prepend the slider navigation to the slider
     if there are at least 2 slides */
  $elm.prepend('<ul class="slider-nav"></ul>');
  
  // make a bullet for each slide
  for (var i = 0; i < slidesCount; i++) {
    var bullets = '<li><a href="#">' + i + '</a></li>';
    if (i == 0) {
      // active bullet
      var bullets = '<li class="activeSlide"><a href="#">' + i + '</a></li>';
      // active slide
      $(slides[0]).addClass('active');
    }
    $('.slider-nav').append(bullets);
  }
};

var slideUpDown = function() {
  // set top property for all the slides
  $(slides).not(displayImageBeforeClick).css('top', slideHeight);
  // then animate to the next slide
  $(slides[activeIdx]).animate({
    'top': 0
  }, animationspeed);

  $(displayImageBeforeClick).animate({
    'top': "-100%"
  }, animationspeed);
};

$('.slider-nav a').on('click', function(event) {
  displayImageBeforeClick = $(".slider-container .active");
  activeIdx = $(this).text();
  if ($(slides[activeIdx]).hasClass("active")) {
    return false;
  }
  $('.slider-nav a').closest('li').removeClass('activeSlide');
  $(this).closest('li').addClass('activeSlide');

  // Reset autoadvance if user clicks bullet
  if (event.originalEvent !== undefined) {
    clearInterval(autoAdvance);
    autoAdvance = setInterval(advanceFunc, animationInterval);
  }

  setActiveSlide();
  slideUpDown();
});
body * {
  box-sizing: border-box;
}

.container {
  max-width: 1200px;
  margin: 0 auto;
}

.slider {
  width: 100%;
  height: 300px;
  position: relative;
  overflow: hidden;
}

.slider .slider-nav {
  text-align: center;
  position: absolute;
  padding: 0;
  margin: 0;
  left: 10px;
  right: 10px;
  bottom: 2px;
  z-index: 30;
}

.slider .slider-nav li {
  display: inline-block;
  width: 20px;
  height: 3px;
  margin: 0 1px;
  text-indent: -9999px;
  overflow: hidden;
  background-color: rgba(255, 255, 255, .5);
}

.slider .slider-nav a {
  display: block;
  height: 3px;
  line-height: 3px;
}

.slider .slider-nav li.activeSlide {
  background: #fff;
}

.slider .slider-nav li.activeSlide a {
  display: none;
}

.slider .slider-container {
  width: 100%;
  text-align: center;
}

.slider .slider-container a {
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
}

.slider .slider-container img {
  transform: translateX(-50%);
  margin-left: 50%;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>

<div class="container">
  <div class="slider slider-homepage">
    <div class="slider-container">
      <a href="#">
        <img src="https://picsum.photos/1200/300/?gravity=east" alt="">
      </a>
      <a href="#">
        <img src="https://picsum.photos/1200/300/?gravity=south" alt="">
      </a>
      <a href="#">
        <img src="https://picsum.photos/1200/300/?gravity=west" alt="">
      </a>
    </div>
  </div>
</div>

How could I prevent this phenomenon I would call, for lack of a better term, an event crowding (overlap)?

Razvan Zamfir
  • 4,209
  • 6
  • 38
  • 252
  • 1
    Add a boolean "transisting", and only allow actions on your buttons as long as it is set to false. Set it to false on default, and once an action is clicked, set it to true, once the animation is finished, set it to false again. – Rence Aug 17 '18 at 12:03

4 Answers4

2

Here is a possible fix, consisting of waiting for an animation to finish before starting another:

var $elm = $('.slider'),
    $slidesContainer = $elm.find('.slider-container'),
    slides = $slidesContainer.children('a'),
    slidesCount = slides.length,
    slideHeight = $(slides[0]).find('img').outerHeight(false),
    animationspeed = 1500,
    animationInterval = 7000;

// Set (initial) z-index for each slide
var setZindex = function() {
    for (var i = 0; i < slidesCount; i++) {
        $(slides[i]).css('z-index', slidesCount - i);
    }
};
setZindex();

var displayImageBeforeClick = null;

var setActiveSlide = function() {
    $(slides).removeClass('active');
    $(slides[activeIdx]).addClass('active');
};

var advanceFunc = function() {
    if ($('.slider-nav li.activeSlide').index() + 1 != $('.slider-nav li').length) {
        $('.slider-nav li.activeSlide').next().find('a').trigger('click');
    } else {
        $('.slider-nav li:first').find('a').trigger('click');
    }
}

var autoAdvance = setInterval(advanceFunc, animationInterval);

//Set slide height
$(slides).css('height', slideHeight);

// Append bullets
if (slidesCount > 1) {
  /* Prepend the slider navigation to the slider
     if there are at least 2 slides */
  $elm.prepend('<ul class="slider-nav"></ul>');
  
  // make a bullet for each slide
  for (var i = 0; i < slidesCount; i++) {
    var bullets = '<li><a href="#">' + i + '</a></li>';
    if (i == 0) {
      // active bullet
      var bullets = '<li class="activeSlide"><a href="#">' + i + '</a></li>';
      // active slide
      $(slides[0]).addClass('active');
    }
    $('.slider-nav').append(bullets);
  }
};

var animationStart = false;
var slideUpDown = function() {
    animationStart = true;
    // set top property for all the slides
    $(slides).not(displayImageBeforeClick).css('top', slideHeight);
    // then animate to the next slide
    $(slides[activeIdx]).animate({
        'top': 0
    }, animationspeed, function() {
        animationStart = false;
    });

    $(displayImageBeforeClick).animate({
        'top': "-100%"
    }, animationspeed, function() {
        animationStart = false;
    });
};

$('.slider-nav a').on('click', function(event) {
    if (animationStart) {
        return false;
    }
    displayImageBeforeClick = $(".slider-container .active");
    activeIdx = $(this).text();
    if ($(slides[activeIdx]).hasClass("active")) {
        return false;
    }
    $('.slider-nav a').closest('li').removeClass('activeSlide');
    $(this).closest('li').addClass('activeSlide');

    // Reset autoadvance if user clicks bullet
    if (event.originalEvent !== undefined) {
        clearInterval(autoAdvance);
        autoAdvance = setInterval(advanceFunc, animationInterval);
    }

    setActiveSlide();
    slideUpDown();
});
body * {
  box-sizing: border-box;
}

.container {
  max-width: 1200px;
  margin: 0 auto;
}

.slider {
  width: 100%;
  height: 300px;
  position: relative;
  overflow: hidden;
}

.slider .slider-nav {
  text-align: center;
  position: absolute;
  padding: 0;
  margin: 0;
  left: 10px;
  right: 10px;
  bottom: 2px;
  z-index: 30;
}

.slider .slider-nav li {
  display: inline-block;
  width: 20px;
  height: 3px;
  margin: 0 1px;
  text-indent: -9999px;
  overflow: hidden;
  background-color: rgba(255, 255, 255, .5);
}

.slider .slider-nav a {
  display: block;
  height: 3px;
  line-height: 3px;
}

.slider .slider-nav li.activeSlide {
  background: #fff;
}

.slider .slider-nav li.activeSlide a {
  display: none;
}

.slider .slider-container {
  width: 100%;
  text-align: center;
}

.slider .slider-container a {
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
}

.slider .slider-container img {
  transform: translateX(-50%);
  margin-left: 50%;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>

<div class="container">
  <div class="slider slider-homepage">
    <div class="slider-container">
      <a href="#">
        <img src="https://picsum.photos/1200/300/?gravity=east" alt="">
      </a>
      <a href="#">
        <img src="https://picsum.photos/1200/300/?gravity=south" alt="">
      </a>
      <a href="#">
        <img src="https://picsum.photos/1200/300/?gravity=west" alt="">
      </a>
    </div>
  </div>
</div>
Razvan Zamfir
  • 4,209
  • 6
  • 38
  • 252
2

You can chain your animations using jQuery deferred object and Promise. Here is the class allowing you to do it easily.

var Queue = function() {
    var lastPromise = null;

    this.add = function(callable) {
        var methodDeferred = $.Deferred();
        var queueDeferred = this.setup();

        // execute next queue method
        queueDeferred.done(function() {

            // call actual method and wrap output in deferred
            callable().then(methodDeferred.resolve)
        });
        lastPromise = methodDeferred.promise();
    };

    this.setup = function() {
        var queueDeferred = $.Deferred();

        // when the previous method returns, resolve this one
        $.when(lastPromise).always(function() {
            queueDeferred.resolve();
        });

        return queueDeferred.promise();
    }
};

The fiddle is with the animations queued.

PS: I increase the size of the buttons to click more easily

var $elm = $('.slider'),
  $slidesContainer = $elm.find('.slider-container'),
  slides = $slidesContainer.children('a'),
  slidesCount = slides.length,
  slideHeight = $(slides[0]).find('img').outerHeight(false),
  animationspeed = 1500,
  animationInterval = 7000;

// Set (initial) z-index for each slide
var setZindex = function() {
  for (var i = 0; i < slidesCount; i++) {
    $(slides[i]).css('z-index', slidesCount - i);
  }
};
setZindex();

var setActiveSlide = function() {
  $(slides).removeClass('active');
  $(slides[activeIdx]).addClass('active');
};

var advanceFunc = function() {
  if ($('.slider-nav li.activeSlide').index() + 1 != $('.slider-nav li').length) {
    $('.slider-nav li.activeSlide').next().find('a').trigger('click');
  } else {
    $('.slider-nav li:first').find('a').trigger('click');
  }
}

var autoAdvance = setInterval(advanceFunc, animationInterval);

//Set slide height
$(slides).css('height', slideHeight);

// Append bullets
if (slidesCount > 1) {
  /* Prepend the slider navigation to the slider
     if there are at least 2 slides */
  $elm.prepend('<ul class="slider-nav"></ul>');
  
  // make a bullet for each slide
  for (var i = 0; i < slidesCount; i++) {
    var bullets = '<li><a href="#">' + i + '</a></li>';
    if (i == 0) {
      // active bullet
      var bullets = '<li class="activeSlide"><a href="#">' + i + '</a></li>';
      // active slide
      $(slides[0]).addClass('active');
    }
    $('.slider-nav').append(bullets);
  }
};

var Queue = function() {
    var lastPromise = null;

    this.add = function(callable) {
        var methodDeferred = $.Deferred();
        var queueDeferred = this.setup();
        // execute next queue method
        queueDeferred.done(function() {

            // call actual method and wrap output in deferred
            callable().then(methodDeferred.resolve)
        });
        lastPromise = methodDeferred.promise();
    };

    this.setup = function() {
        var queueDeferred = $.Deferred();
        // when the previous method returns, resolve this one
        $.when(lastPromise).always(function() {
            queueDeferred.resolve();
        });
        return queueDeferred.promise();
    }
};

var queue = new Queue();
var slideUpDown = function(previousIdx, activeIdx) {
  queue.add(function() {
    return new Promise(function(resolve, reject) {
      // set top property for all the slides
      $(slides).not(slides[previousIdx]).css('top', slideHeight);
      // then animate to the next slide
      $(slides[activeIdx]).animate({
        'top': 0
      }, animationspeed);

      $(slides[previousIdx]).animate({
        'top': "-100%"
      }, animationspeed, 'swing', resolve);
    })
  })
};

var previousIdx = '0' // First slide
$('.slider-nav a').on('click', function(event) {
  activeIdx = $(this).text();
  
  // Disable clicling on an active item
  if ($(slides[activeIdx]).hasClass("active")) {
    return false;
  }
  $('.slider-nav a').closest('li').removeClass('activeSlide');
  $(this).closest('li').addClass('activeSlide');

  // Reset autoadvance if user clicks bullet
  if (event.originalEvent !== undefined) {
    clearInterval(autoAdvance);
    autoAdvance = setInterval(advanceFunc, animationInterval);
  }

  setActiveSlide();
  slideUpDown(previousIdx, activeIdx);
 previousIdx = activeIdx
});
body * {
  box-sizing: border-box;
}

.container {
  max-width: 1200px;
  margin: 0 auto;
}

.slider {
  width: 100%;
  height: 300px;
  position: relative;
  overflow: hidden;
}

.slider .slider-nav {
  text-align: center;
  position: absolute;
  padding: 0;
  margin: 0;
  left: 10px;
  right: 10px;
  bottom: 2px;
  z-index: 30;
}

.slider .slider-nav li {
  display: inline-block;
  width: 20px;
  height: 6px;
  margin: 0 1px;
  text-indent: -9999px;
  overflow: hidden;
  background-color: rgba(255, 255, 255, .5);
}

.slider .slider-nav a {
  display: block;
  height: 6px;
  line-height: 3px;
}

.slider .slider-nav li.activeSlide {
  background: #fff;
}

.slider .slider-nav li.activeSlide a {
  display: none;
}

.slider .slider-container {
  width: 100%;
  text-align: center;
}

.slider .slider-container a {
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
}

.slider .slider-container img {
  transform: translateX(-50%);
  margin-left: 50%;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>

<div class="container">
  <div class="slider slider-homepage">
    <div class="slider-container">
      <a href="#">
        <img src="https://picsum.photos/1200/300/?gravity=east" alt="">
      </a>
      <a href="#">
        <img src="https://picsum.photos/1200/300/?gravity=south" alt="">
      </a>
      <a href="#">
        <img src="https://picsum.photos/1200/300/?gravity=west" alt="">
      </a>
    </div>
  </div>
</div>
Razvan Zamfir
  • 4,209
  • 6
  • 38
  • 252
Alex83690
  • 758
  • 9
  • 30
  • Razvan Zamfir I canceled your edit because it was overwriting an edit I had just done. I then edited the size at 6px. But then you are free to change the code as you want on your side. – Alex83690 Aug 31 '18 at 09:14
  • I ave just posred *[How can I change the fixed navbar's classes depending on the background of the page section it hovers?](https://stackoverflow.com/questions/52118919/change-the-fixed-navbars-classes-depending-on-the-background-of-the-page-sectio)*. Please have a look. Do you think it is ambiguous? – Razvan Zamfir Sep 01 '18 at 07:21
  • I will have a look this night. – Alex83690 Sep 01 '18 at 10:20
  • Please try an answer to **[Queuing click events in Bootstrap 3 carousel](https://stackoverflow.com/q/53043332/4512005)**. Thanks! – Razvan Zamfir Oct 29 '18 at 16:39
1

You could use a queue: every time you click a bullet it add to the queue and execute the queue. Something like this:

{
    let
            transitionQueue = [],
            transitioning = false;

    function doTransition() {
            displayImageBeforeClick = $(".slider-container .active");
            $('.slider-nav a').closest('li').removeClass('activeSlide');
            transitionQueue.shift().closest('li').addClass('activeSlide');

            // Reset autoadvance if user clicks bullet
            if (event.originalEvent !== undefined) {
              clearInterval(autoAdvance);
              autoAdvance = setInterval(advanceFunc, animationInterval);
            }

            setActiveSlide();
            slideUpDown();
            if (transitionQueue.length)
                setTimeout(doTransition, animationSpeed)
            else
                transitioning = false;
    }

    function callTransition() {
        if (!transitioning) {
            transitioning = true;
            doTransition();
        }
    }

    $('.slider-nav a').click(function () {
        transitionQueue.push($(this));
        callTransition();
    });
}

I haven't tested this, so...

Lucas Noetzold
  • 1,670
  • 1
  • 13
  • 29
0

While this not strictly answering your question, one way you could solve your actual problem is by handling your slider differently, consisting of moving the slider instead of the slides :

1) Make the slides container absolute, and it's container relative

.slider-homepage {
    position: relative;
}

.slider .slider-container {
    position: absolute;
}

.slider .slider-nav {
    //position: absolute; Remove this
}

2) Instead of positionning the slides on click, move the slider container to the right position

var slideUpDown = function() {
    $('.slider-container').stop().animate(
        {top: activeIdx * slideHeight * -1}, 
        {duration: animationspeed}
    );
};

This way no image will ever overlap.

Here is the fiddle, the code can certainly be refactored but I didn't have too much time to look into it, will try to post a snippet here asap if you're ok with not leaving your original thing of moving slides instead of the slider.

Logar
  • 1,248
  • 9
  • 17