gentian
gentian

Reputation: 178

Nested scroll inside vertical swiper slide when mousewheel: true

I have a vertical slider and I'm using swiper to navigate through the slides.

Every swiper-slide container height is 100vh.

I have a slide which content is greater than the view height and when scrolling with the mousewheel, I want to first scroll it's content and when the end or top is reached, according to the scroll direction, move to the next or previous slide.

I went through swiper documentation, SO and other pages but didn't find a solution.

Here is the jsfiddle: https://jsfiddle.net/gentian28/6wdsep1v/13/

HTML

<div class="swiper-container">
    <main class="main swiper-wrapper">

        <!-- landing -->
        <section id="home" class="swiper-slide">
            <div id="particles-js"></div>
            <div id="typeIt" class="d-flex align-center"></div>
        </section>

        <!-- about -->
        <section id="about" class="swiper-slide">
            <span class="animation">About</span>
        </section>

        <!-- portfolio -->
        <section id="portfolio" class="swiper-slide d-flex flex-wrap col-3">
            <div class="card">
                card 1
            </div>
            <div class="card">
                card 2
            </div>
            <div class="card">
                card 3
            </div>
            <div class="card">
                card 4
            </div>
            <div class="card">
                card 1
            </div>
            <div class="card">
                card 1
            </div>
            <div class="card">
                card 1
            </div>
            <div class="card">
                card 1
            </div>
            <div class="card">
                card 1
            </div>
            <div class="card">
                card 1
            </div>
            <div class="card">
                card 1
            </div>
        </section>

        <!-- technologies -->
        <section id="skills" class="swiper-slide">
            Skills
        </section>

        <!-- contact -->
        <section id="contact" class="swiper-slide">
            Contact
        </section>

    </main>

</div>

CSS

body {
    margin: 0;
    padding: 0;
}
.d-flex {
    display: flex;
}
.align-center {
    align-items: center;
}
.justify-center {
    justify-content: center;
}
.justify-between {
    justify-content: space-between;
}
.flex-column {
    flex-flow: column;
}
.column-reverse {
    flex-flow: column-reverse;
}
.flex-wrap {
    flex-wrap: wrap;
}
.col-2 > * {
    width: calc(100% / 2 - 7.5px);
    margin-right: 15px;
    margin-bottom: 15px;
}
.col-2 > *:nth-child(2n) {
    margin-right: 0;
}

.col-3 > * {
    width: calc(100% / 3 - 10px);
    margin-right: 15px;
}

.col-3 > *:nth-child(3n) {
    margin-right: 0;
}

.col-4 > * {
    width: calc(100% / 4 - 10.5px);
    margin-right: 14px;
}

.col-4 > *:nth-child(4n) {
    margin-right: 0;
}
.card {
    height: 300px;
}

.swiper-container {
    width: 100% - 120px;
    height: 100vh;
    margin-left: auto;
    margin-right: auto;
}
.swiper-slide {
    text-align: center;
    font-size: 18px;
    background: #fff;
    /* Center slide text vertically */
    display: -webkit-box;
    display: -ms-flexbox;
    display: -webkit-flex;
    display: flex;
    -webkit-box-pack: center;
    -ms-flex-pack: center;
    -webkit-justify-content: center;
    justify-content: center;
    -webkit-box-align: center;
    -ms-flex-align: center;
    -webkit-align-items: center;
    align-items: center;
    overflow-y: auto;
}
.swiper-pagination {
    display: flex;
    flex-flow: column;
}
.swiper-pagination-bullet-active {
    opacity: 0;
}
.swiper-pagination-bullet {
    width: 120px;
    height: 96px;
    border-radius: 0;
    opacity: 0;
}

JS

const swiperConf = {
    direction: 'vertical',
    slidesPerView: 1,
    spaceBetween: -1,
    mousewheel: true,
    keyboard: true,
    pagination: {
        el: '.swiper-pagination',
        clickable: true,
    }
}

var swiper = new Swiper('.swiper-container', swiperConf);

Upvotes: 5

Views: 7218

Answers (3)

chris
chris

Reputation: 607

Modified from @LeTsoy's answer,

  1. Main change is to trigger this logic when the slide with the scrollbar is the first slide. Where the slideChangeTransitionEnd event will not trigger, especially when HashNavigation is enabled.
  2. disable mousewheel & allowTouchMove as the first thing, to allow inside scrolling to function properly
  3. Removing the scroll event listener to avoid event listener accumulation
  4. added {passive:true} to improve performance (optional)

    const handleScrollInside = (swiper) => {
            swiper.on("slideChangeTransitionEnd", () => {
                swiper.mousewheel.disable();
                swiper.allowTouchMove = false;
                
              const activeSlide = document.querySelector('.swiper-slide-active');
              
              const hasVerticalScrollbar = activeSlide.scrollHeight > activeSlide.clientHeight;
          
              if (hasVerticalScrollbar) {
                const scrollDifferenceTop = activeSlide.scrollHeight - activeSlide.swiperSlideSize;
          
                if (activeSlide.scrollTop === 0) activeSlide.scrollTop += 1;
                if (activeSlide.scrollTop === scrollDifferenceTop) activeSlide.scrollTop -= 2;
                swiper.mousewheel.disable();
                swiper.allowTouchMove = false;
                const scrollHandler = () => {
                    if (activeSlide.scrollTop <= 0 || scrollDifferenceTop - activeSlide.scrollTop <= 1 ) {
                      swiper.mousewheel.enable();
                      swiper.allowTouchMove = true;
                      activeSlide.removeEventListener('scroll',scrollHandler);
                    }
                };
                activeSlide.addEventListener('scroll', scrollHandler, { passive: true });
              }  else {
                swiper.mousewheel.enable();
                swiper.allowTouchMove = true;
              }
            })
            const activeSlide = document.querySelector('.swiper-slide-active');
            const hasVerticalScrollbar = activeSlide.scrollHeight > activeSlide.clientHeight;
            if (hasVerticalScrollbar) {
                swiper.emit('slideChangeTransitionEnd'); 
              }
          } 

if you have trouble with the inside scrolling, try bringing the active slide to the top for it to receive the scroll event. (i'm at swiper version: 11.x) it looks like all the slides in the swiper are stacked one above the other starting from 1th slide and the opacity is set to 0. only the active slide's opacity is set to 1. so the last (nth) slide will be at the top, preventing the active slide to receive the scrolling event.

.swiper-slide-active{
  position: relative;
  z-index: 10;
}

Upvotes: 0

LeTsoy
LeTsoy

Reputation: 21

ahh guys! thank you a lot for the input, i had the same issue. but your solutions didnt work for me on mobile devices. so i tried sth own with your input and hacked a little bit. so here is mine: (my first post on stack overflow yee)

const handleScrollInside = (swiper) => {
  swiper.on("slideChangeTransitionEnd", () => {
    const activeSlide = document.querySelector('.swiper-slide-active');
    const hasVerticalScrollbar = activeSlide.scrollHeight > activeSlide.clientHeight;

    if (hasVerticalScrollbar) {
      const scrollDifferenceTop = activeSlide.scrollHeight - activeSlide.swiperSlideSize;

      if (activeSlide.scrollTop === 0) activeSlide.scrollTop += 1;
      if (activeSlide.scrollTop === scrollDifferenceTop) activeSlide.scrollTop -= 2;
      swiper.mousewheel.disable();
      swiper.allowTouchMove = false;

      activeSlide.addEventListener("scroll", () => {
        if (activeSlide.scrollTop <= 0 || scrollDifferenceTop - activeSlide.scrollTop <= 1 ) {
          swiper.mousewheel.enable();
          swiper.allowTouchMove = true;
        }
      });
    }
  })
}

Upvotes: 2

Lane
Lane

Reputation: 1

Also ran into this issue, and the fiddle from @Daryll was very helpful, but with typescript there were some issues getting the 'swiperSlideSize' from an HTMLElement (also using reactjs there are some differences). This worked for me as the event handler for 'onSlideChangeTransitionEnd':

  const allowScroll = (swiper: SwiperEvent) => {
    var activeIndex = swiper.activeIndex;
    var activeSlide = swiper.slides[activeIndex];
    var { scrollHeight, clientHeight } = activeSlide;
    const diff = scrollHeight - clientHeight;
    if (diff > 0) {
      const findScroll = (e) => {
        const scrollUp = e.deltaY < 0;
        if (scrollUp && activeSlide.scrollTop === 0) {
          swiper.mousewheel.enable();
          activeSlide.removeEventListener("wheel", findScroll);
        } else if (!scrollUp && activeSlide.scrollTop === diff) {
          swiper.mousewheel.enable();
          activeSlide.scrollTop = 0;
          activeSlide.removeEventListener("wheel", findScroll);
        }
      };
      activeSlide.addEventListener("wheel", findScroll);
      swiper.mousewheel.disable();
    }
  };

edit: the "SwiperEvent" type is an alias I'm using with import { Swiper as SwiperEvent } from "swiper"; to avoid namespace conflict with import { Swiper } from "swiper/react";

edit 2: for mobile usage, you have to consider 'touchmove' events, which don't (always?) register the 'wheel' event. By disabling 'allowTouchMove' on the swiper, you achieve the same effect on mobile as with mousewheel.disable() on desktop. Here's some code for that case:

  const allowScroll = (swiper: SwiperEvent) => {
    var activeIndex = swiper.activeIndex;
    var activeSlide = swiper.slides[activeIndex];
    var { scrollHeight, clientHeight } = activeSlide;
    const diff = scrollHeight - clientHeight;
    if (activeSlide.scrollTop === 0) activeSlide.scrollTop = 1;
    else if (activeSlide.scrollTop === diff) activeSlide.scrollTop = diff - 1;
    if (diff > 0) {
      const findScroll = (e) => {
        const scrollUp = e.deltaY < 0;
        if (
          (scrollUp || e.type === "touchmove") &&
          activeSlide.scrollTop <= 0
        ) {
          swiper.mousewheel.enable();
          swiper.allowTouchMove = true;
          activeSlide.removeEventListener("wheel", findScroll);
          activeSlide.removeEventListener("touchmove", findScroll);
        } else if (
          (!scrollUp || e.type === "touchmove") &&
          activeSlide.scrollTop >= diff
        ) {
          swiper.mousewheel.enable();
          swiper.allowTouchMove = true;
          activeSlide.removeEventListener("wheel", findScroll);
          activeSlide.removeEventListener("touchmove", findScroll);
        }
      };
      activeSlide.addEventListener("wheel", findScroll);
      activeSlide.addEventListener("touchmove", findScroll);
      swiper.mousewheel.disable();
      swiper.allowTouchMove = false;
    }
  };

Basically, by setting the scrollTop to 1px from either the top or bottom of the range, you prevent the mousewheel.enable() call from triggering immediately. In the original version, the slide would always start at the top of the scroll height when activated, while this version starts at the "top" (technically 1px down) if you're swiping down to it and the "bottom" if you're swiping up to it.

Upvotes: 0

Related Questions