flickity icon indicating copy to clipboard operation
flickity copied to clipboard

Disable prev/next buttons for contain

Open dzucconi opened this issue 9 years ago • 18 comments

I'm wondering if you have any suggestions for determining if you've arrived at the boundary of the carousel when the contain option is specified. (So that one could say for instance disable the next or previous buttons).

When not using the indicator dots for position feedback this kind of behavior appears confusing, though I understand the logic here.

Example:

contain-carousel

dzucconi avatar Nov 17 '15 17:11 dzucconi

Yes, this is a tricky issue. I'll repeat my thoughts on why page dots and the prev/next buttons work with contain https://github.com/metafizzy/flickity/issues/177#issuecomment-117680694 and https://github.com/metafizzy/flickity/issues/135#issuecomment-100894043

When multiple cells are visible or with 'contain': true, there can be areas when clicking the previous/next arrows won't move the slider.

While the slider does not change, the selected cell does. This may be useful for accessibility, as it allows users to select a cell with buttons, rather than with mouse.

You can detect when the slider is at the end with contain with this code. See demo http://codepen.io/desandro/pen/KdEPJo?editors=001

flkty.on( 'cellSelect', function() {
  var target = flkty.selectedCell.target;
  if ( target == flkty.cells[0].target ) {
    console.log('is at contained start')
  } else if ( target == flkty.getLastCell().target ) {
    console.log('is at contained end')
  }
});

If you're looking to disable prev/next buttons, you could do it by hacking Flickity.PrevNextButton. See demo http://codepen.io/desandro/pen/LpaPya?editors=001

var PrevNextButton = Flickity.PrevNextButton;
PrevNextButton.prototype.update = function() {
  // index of first or last cell, if previous or next
  var cells = this.parent.cells;
  // enable is wrapAround and at least 2 cells
  if ( this.parent.options.wrapAround && cells.length > 1 ) {
    this.enable();
    return;
  }
  var lastIndex = cells.length ? cells.length - 1 : 0;
  var boundIndex = this.isPrevious ? 0 : lastIndex;
  var isEnabling;
  if ( this.parent.options.contain ) {
    var boundCell = cells[ boundIndex ];
    var selectedCell = cells[ this.parent.selectedIndex ];
    isEnabling = selectedCell.target != boundCell.target;
  } else {
    isEnabling = this.parent.selectedIndex == boundIndex
  }
  var method = isEnabling ? 'enable' : 'disable';
  this[ method ]();
};

I do not recommend disabling buttons like this as selecting cells is useful for accessibility.

desandro avatar Nov 17 '15 20:11 desandro

Ah, I see that makes sense. This issue is indeed trickier than I thought. The examples are a great help, thank you!

dzucconi avatar Nov 18 '15 20:11 dzucconi

Re-opening for other's visibility

desandro avatar Nov 18 '15 22:11 desandro

Having the same issues, thanks for the demos @desandro, really really helpful man.

However, I am still able to replicate the error by using a combination of dragging and clicking on prev/next. You can see it in both of the demos above. Start by dragging the slide to the very end (dragging left) until the last cell is highlighted in yellow. Now, click on the previous button, you'll notice you need to click twice until the slider starts moving again. Not so much of an issue in this demo, as the highlighted cell changes, but in my current use case, it feels like a bug.

Wonder if would be worth having that as an option though, for example: detectContainedEnd, setting to true would disable the next button when the last item is in view.

Thanks a lot

peduarte avatar Dec 14 '15 13:12 peduarte

+1

I'm also still having issues with this when using a carousel as navigation for another carousel.

The fact is that in this case an item in the carousel can be selected by the user. Therefore that item then has a class 'is-selected' and if this selected item is the last item in the carousel, and the carousel is cell-aligned to the center or to the left, a click on the left button doesn't move the carousel anymore.

Even if the fix from above has been applied.

dinealbrecht avatar Jan 27 '16 15:01 dinealbrecht

Any update on this one @desandro?

musdy avatar Feb 02 '16 15:02 musdy

One solution is to use groupCells, so that cells are grouped together. When you are in the last group, multiple cells are still visible, but the next button will be disabled.

desandro avatar Aug 05 '16 15:08 desandro

Also trying to solve this for a project with cell align left.

Got this maybe working; Maybe it is a idea to add var boundLastSlide or something like that. When a slide has the endBound on his target, then is know it is the last slide that is going to move. slide.target = Math.min( slide.target, endBound ); And set that one as last slide. [Example: When you have 6 slides, and number 4 is the last one that slides. Number 4 is the "fake last slide"]

Then u can bound the next button to a end. example test code:

in the flickity create add this.boundLastSlide = null;

in function: proto._containSlides = function() { add after: slide.target = Math.max( slide.target, beginBound ); slide.target = Math.min( slide.target, endBound );

This: if ( slide.target == endBound) { var slideIndex = this.slides.indexOf(slide); if (this.boundLastSlide == null ) { this.boundLastSlide = slideIndex; } else if ( this.boundLastSlide > slideIndex ) { this.boundLastSlide = slideIndex; } }

Then this.boundLastSlide has the last slider.

On PrevNextButton.prototype.updatereplace: var lastIndex = slides.length ? slides.length - 1 : 0; var boundIndex = this.isPrevious ? 0 : lastIndex; var method = this.parent.selectedIndex == boundIndex ? 'disable' : 'enable'; with: if ( this.parent.options.contain || !this.parent.options.wrapAround || this.parent.cells.length || this.parent.boundLastSlide != null ) { var lastIndex = this.parent.boundLastSlide; if ( this.isPrevious ) { var method = this.parent.selectedIndex == 0 ? 'disable' : 'enable'; } else { var method = this.parent.selectedIndex >= lastIndex ? 'disable' : 'enable'; } } else { var lastIndex = slides.length ? slides.length - 1 : 0; var boundIndex = this.isPrevious ? 0 : lastIndex; var method = this.parent.selectedIndex == boundIndex ? 'disable' : 'enable'; }

// Not all edits given.

Only this doesn't solve the page dots, but can be a step to work with contain and the bounds. Sorry if the post is not so clear. But hope the code tells something.

Maybe it can done better only in the post u made in nov 2015. https://github.com/metafizzy/flickity/issues/289#issuecomment-157488885

Demo Video https://www.youtube.com/watch?v=5Oy_nYpB4No

ghost avatar Aug 08 '16 16:08 ghost

I'm having a similar issue with this. Grouping fixes the disable issue but I only want to scroll one <div> per click. I was thinking it would be helpful if there was an "in-viewport" class, similar to the "is-selected" that way you could disable the next button if the last-child was in the viewport.

wydflannery avatar Sep 13 '16 19:09 wydflannery

@wydflannery made this local in v1.2.1. ( because IE8 support needed 👎 ) and with settings:

cellAlign: 'left', contain: true, wrapAround: false, pageDots: false

Side note: I did not make a solution for the page dots. this would be simple with: boundLastSlide towards pageDots. In the function PageDots.prototype.setDots

Here you can find the code. https://github.com/KrielkipNL/flickity/tree/containFix

Compare: https://github.com/KrielkipNL/flickity/compare/v1.2.1...KrielkipNL:containFix

Maybe this feedback helps @desandro [Pin: http://codepen.io/anon/pen/VKaGBy]

For the new version (2.0.x) you need to check all possible options in contain.

ghost avatar Sep 14 '16 14:09 ghost

Hey guys!

If anyone still wondering about this, here is my solution:

var carouselInit = new Flickity( '.carousel-init', {
    cellSelector: '.carousel-cell',
    contain: true,
    cellAlign: 'left',
    pageDots: false,
    wrapAround: false,
    freeScroll: false
});

var lastCell = false;

carouselInit.on('cellSelect', function(){
    var friction = (carouselInit.options['friction'] + 0.1) * 1000;

    setTimeout(function(){
        var fullSize = carouselInit.slideableWidth,
            viewport = carouselInit.size['width'],
            cellWidth = carouselInit.selectedSlide['outerWidth'],
            movedX = Math.round(carouselInit.x);

        fullSize = Math.round(fullSize);
        cellWidth = Math.round(cellWidth);
        movedX = Math.abs(movedX);

        var total = Math.round((viewport + movedX) / 100) * 100,
            fullSize = Math.round(fullSize / 100) * 100;
            
        if(total >= fullSize){
            if(lastCell == true)
                carouselInit.select(0);

            lastCell = true;
        }

        if(movedX < 70)
            lastCell = false

    }, friction);
});

LDokos avatar Sep 05 '17 16:09 LDokos

Another possible solution.

new Flickity('.slider', {
      contain: true,
      pageDots: false,
      cellAlign: "left",
      selected: 0,
      on: {
        ready: containFix,
        settle: containFix,
        change: containFixOnChange,
        resize: containFix
      }
    });

    function containFix() {
      const viewport = this.size.width;
      const movedX = Math.round(Math.abs(this.x));

      const total = viewport + movedX;
      const fullSize = Math.round(this.slideableWidth);

      toggle = toggle.bind(this);
      toggle(total, fullSize, viewport);
    }

    function containFixOnChange() {
      const viewport = this.size.width;
      const cellWidth = this.selectedSlide.outerWidth;

      if (this.selectedIndex > this.options.selected) var total = viewport + cellWidth * this.options.selected;
      else if (this.selectedIndex < this.options.selected) var total = viewport - cellWidth * this.options.selected;
      this.options.selected = this.selectedIndex;

      const fullSize = Math.round(this.slideableWidth - cellWidth);

      toggle = toggle.bind(this);
      toggle(total, fullSize, viewport);
    }

    function toggle(total, fullSize, viewport) {
      const cellWidth = this.selectedSlide.outerWidth;
      const maxIdx = this.cells.length - Math.round(viewport / cellWidth);

      if (fullSize > viewport) {
        this.bindDrag();
        this.element.classList.remove("slider--toggle-both");
      } else if (fullSize <= viewport) {
        this.unbindDrag();
        this.element.classList.add("slider--toggle-both");
      }

      if (total >= fullSize) this.element.classList.add("slider--toggle-next");
      else this.element.classList.remove("slider--toggle-next");

      if (this.selectedIndex > maxIdx) {
        this.select(maxIdx);
        this.element.classList.add("slider--toggle-next");
      }
    }

Marvin1003 avatar Nov 06 '18 16:11 Marvin1003

Another solution

// This works if the cells are the same width and aligned center. 
//
// If you have variants in your cell widths, you could loop through cells (beginning/end)
// to figure out which one will land on the middle of flkty.size.width
// 
// Also you can extract the floor/ceil computations to a resize event if it becomes intensive.
flkty.on( 'change', i => {

    // figure out the last/first slides
    let floor = ~~(flkty.size.width / flkty.cells[0].element.offsetWidth / 2),
        ceil = flkty.cells.length - floor;

    // select the floor/ceil cells if the current index exceeds them
    if(i > ceil) this.carousel.select(ceil);
    else if (i < floor) this.carousel.select(floor);

});

vincentsmuda avatar May 27 '19 21:05 vincentsmuda

Would PR https://github.com/metafizzy/flickity/pull/1032 potentially fix this as we are redefining what counts as selected?

nixondesigndev avatar May 20 '20 12:05 nixondesigndev

Hey. As for me, the easiest solution for this issue is to use the IntersectionObserver API.

Just listen to the last slide to get into the viewport, then disable Next Button

cooltolia avatar Sep 11 '20 17:09 cooltolia

Hey. As for me, the easiest solution for this issue is to use the IntersectionObserver API.

Just listen to the last slide to get into the viewport, then disable Next Button

Hey, could you share an example please?

iriepixel avatar Feb 24 '21 12:02 iriepixel

@iriepixel Hey, sorry for so long response. something like that

const observer = new IntersectionObserver(
    entries => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                controls.disableNext();
            } else {
                controls.enableNext();
            }
        });
    },
    { rootMargin: '0px -24px 0px 0px', root: sliderNode }
);
observer.observe(lastSlide);

where sliderNode is your parent node to init slider

cooltolia avatar Mar 17 '21 17:03 cooltolia

I used the on "scroll" function to check if the slider progress is 100%

scroll: function( progress ) {
              progress = Math.max( 0, Math.min( 1, progress ) );
              
              var next_buttons = document.querySelectorAll('.cross-sell-container-desktop .flickity-button.flickity-prev-next-button.next')
             
              if(progress == 1 && next_buttons ){
                next_buttons.forEach(function(button) {
                  button.disabled = true;
                });
              } else{
                next_buttons.forEach(function(button) {
                  button.disabled = false;
                });
              }
              
            }

OverdoseDigital avatar Mar 15 '24 01:03 OverdoseDigital