Prevent event overlap in custom jQuery image carou

2020-02-14 07:07发布

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)?

4条回答
Viruses.
2楼-- · 2020-02-14 07:47

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>

查看更多
家丑人穷心不美
3楼-- · 2020-02-14 07:52

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>

查看更多
狗以群分
4楼-- · 2020-02-14 07:58

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...

查看更多
对你真心纯属浪费
5楼-- · 2020-02-14 07:59

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.

查看更多
登录 后发表回答