【问题标题】:CSS Scroll Snap Points with navigation (next, previous) buttons带有导航(下一个、上一个)按钮的 CSS 滚动捕捉点
【发布时间】:2019-08-16 02:57:36
【问题描述】:

我正在使用 CSS snap points 构建一个非常简约的轮播。拥有 仅 CSS 选项 对我来说很重要,但我可以使用 javascript 进行一些改进(无框架)。

我正在尝试添加上一个和下一个按钮,以编程方式滚动到下一个或上一个元素。如果 javascript 被禁用,按钮将被隐藏并且轮播仍然有效。

我的问题是关于如何触发滚动到下一个捕捉点

所有项目都有不同的大小,我发现的大多数解决方案需要像素值(如示例中使用的scrollBy)。 scrollBy 40px 适用于第 2 页,但不适用于其他页面,因为它们太大(基于视口的大小)。

function goPrecious() {
  document.getElementById('container').scrollBy({ 
    top: -40,
    behavior: 'smooth' 
  });
}

function goNext() {
  document.getElementById('container').scrollBy({ 
    top: 40,
    behavior: 'smooth' 
  });
}
#container {
  scroll-snap-type: y mandatory;
  overflow-y: scroll;

  border: 2px solid var(--gs0);
  border-radius: 8px;
  height: 60vh;
}

#container div {
  scroll-snap-align: start;

  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 4rem;
}
#container div:nth-child(1) {
  background: hotpink;
  color: white;
  height: 50vh;
}
#container div:nth-child(2) {
  background: azure;
  height: 40vh;
}
#container div:nth-child(3) {
  background: blanchedalmond;
  height: 60vh;
}
#container div:nth-child(4) {
  background: lightcoral;
  color: white;
  height: 40vh;
}
<div id="container">
  <div>1</div>
  <div>2</div>
  <div>3</div>
  <div>4</div>
</div>

<button onClick="goPrecious()">previous</button>
<button onClick="goNext()">next</button>

【问题讨论】:

    标签: javascript css scroll frontend


    【解决方案1】:

    好问题!我将此视为挑战。
    所以,我增加了JavaScript 让它动态工作。按照我的详细解决方案(最后是完整代码):

    首先,将position: relative 添加到.container,因为它需要在.container 中作为scrollheight 检查的引用。

    那么,我们创建3个全局辅助variables

    1) 将项目scroll positions(顶部和底部)作为arrays 获取到array。示例:[[0, 125], [125, 280], [280, 360]](在本例中为 3 项)。
    3) 存储 .container height 一半的一个(稍后会有用)。
    2) 另一个存储item indexscroll 位置

    var carouselPositions;
    var halfContainer;
    var currentItem;
    

    现在,一个名为getCarouselPositionsfunction 创建了带有项目位置的array(存储在carouselPositions)并计算了.container 的一半(存储在halfContainer):

    function getCarouselPositions() {
      carouselPositions = [];
      document.querySelectorAll('#container div').forEach(function(div) {
        carouselPositions.push([div.offsetTop, div.offsetTop + div.offsetHeight]); // add to array the positions information
      })
      halfContainer = document.querySelector('#container').offsetHeight/2;
    }
    
    getCarouselPositions(); // call it once
    

    让我们替换buttons 上的functions。现在,当您单击它们时,将调用相同的 function,但带有 "next""previous" 参数:

    <button onClick="goCarousel('previous')">previous</button>
    <button onClick="goCarousel('next')">next</button>
    

    这里是关于goCarousel function 本身:

    首先,它创建 2 个variables 存储top scroll 位置和bottom scroll 位置的轮播。

    然后,有 2 个条件来查看当前轮播位置是在大多数 top 还是大多数 bottom
    如果它在top 上并单击"next" button,它将转到第二个项目位置。如果它在bottom 上并单击"previous" button,它将转到最后一项之前的上一项。

    如果两个条件都失败,则表示当前项目不是第一个或最后一个。因此,它会检查current position 是什么,使用容器的一半与array of positions 循环计算以查看item 显示的内容。然后,它结合"previous""next" 检查为currentItem 变量设置正确的下一个位置。

    最后,它通过scrollTo使用currentItem新值到达正确的位置。

    下面是完整的代码:

    var carouselPositions;
    var halfContainer;
    var currentItem;
    
    function getCarouselPositions() {
      carouselPositions = [];
      document.querySelectorAll('#container div').forEach(function(div) {
        carouselPositions.push([div.offsetTop, div.offsetTop + div.offsetHeight]); // add to array the positions information
      })
      halfContainer = document.querySelector('#container').offsetHeight/2;
    }
    
    getCarouselPositions(); // call it once
    
    function goCarousel(direction) {
      
      var currentScrollTop = document.querySelector('#container').scrollTop;
      var currentScrollBottom = currentScrollTop + document.querySelector('#container').offsetHeight;
      
      if (currentScrollTop === 0 && direction === 'next') {
          currentItem = 1;
      } else if (currentScrollBottom === document.querySelector('#container').scrollHeight && direction === 'previous') {
          console.log('here')
          currentItem = carouselPositions.length - 2;
      } else {
          var currentMiddlePosition = currentScrollTop + halfContainer;
          for (var i = 0; i < carouselPositions.length; i++) {
            if (currentMiddlePosition > carouselPositions[i][0] && currentMiddlePosition < carouselPositions[i][1]) {
              currentItem = i;
              if (direction === 'next') {
                  currentItem++;
              } else if (direction === 'previous') {
                  currentItem--    
              }
            }
          }
      } 
      
      document.getElementById('container').scrollTo({
        top: carouselPositions[currentItem][0],
        behavior: 'smooth' 
      });
      
    }
    window.addEventListener('resize', getCarouselPositions);
    #container {
      scroll-snap-type: y mandatory;
      overflow-y: scroll;
      border: 2px solid var(--gs0);
      border-radius: 8px;
      height: 60vh;
      position: relative;
    }
    
    #container div {
      scroll-snap-align: start;
    
      display: flex;
      justify-content: center;
      align-items: center;
      font-size: 4rem;
    }
    #container div:nth-child(1) {
      background: hotpink;
      color: white;
      height: 50vh;
    }
    #container div:nth-child(2) {
      background: azure;
      height: 40vh;
    }
    #container div:nth-child(3) {
      background: blanchedalmond;
      height: 60vh;
    }
    #container div:nth-child(4) {
      background: lightcoral;
      color: white;
      height: 40vh;
    }
    <div id="container">
      <div>1</div>
      <div>2</div>
      <div>3</div>
      <div>4</div>
    </div>
    
    <button onClick="goCarousel('previous')">previous</button>
    <button onClick="goCarousel('next')">next</button>

    另一个需要添加的好细节是,如果窗口调整大小,则再次调用getCarouselPositions 函数:

    window.addEventListener('resize', getCarouselPositions);
    

    就是这样。
    这样做很酷。我希望它能以某种方式提供帮助。

    【讨论】:

    • 我从来都不是在 js 上存储像素值的忠实粉丝,因为它很容易不同步,但我不得不说你的解决方案效果很好,接缝也很牢固,喜欢它?。感谢您花一些时间。
    • 优秀的解决方案谢谢。将其转换为 Angular 项目,并且运行良好。
    • 这是一个很好的解决方案,但这里只有一个问题:在 ios 上通过单击幻灯片切换没有平滑动画
    • safari 不支持 behavior: 'smooth' 但这里现有的 polyfill stackoverflow.com/questions/51229742/…
    【解决方案2】:

    我最近做了类似的事情。这个想法是使用IntersectionObserver 来跟踪当前正在查看的项目,然后将上一个/下一个按钮连接到调用Element.scrollIntoView() 的事件处理程序。

    无论如何,Safari 目前不支持滚动行为选项。因此,您可能希望使用 polyfill.app 服务按需填充它。

    let activeIndex = 0;
    const container = document.querySelector("#container");
    const elements = [...document.querySelectorAll("#container div")];
    
    function handleIntersect(entries){
      const entry = entries.find(e => e.isIntersecting);
      if (entry) {
        const index = elements.findIndex(
          e => e === entry.target
        );
        activeIndex = index;
      }
    }
    
    const observer = new IntersectionObserver(handleIntersect, {
      root: container,
      rootMargin: "0px",
      threshold: 0.75
    });
    
    elements.forEach(el => {
      observer.observe(el);
    });
    
    function goPrevious() {
      if(activeIndex > 0) {
        elements[activeIndex - 1].scrollIntoView({
          behavior: 'smooth'
        })
      }
    }
    
    function goNext() {
      if(activeIndex < elements.length - 1) {
        elements[activeIndex + 1].scrollIntoView({
          behavior: 'smooth'
        })
      }
    }
    #container {
      scroll-snap-type: y mandatory;
      overflow-y: scroll;
    
      border: 2px solid var(--gs0);
      border-radius: 8px;
      height: 60vh;
    }
    
    #container div {
      scroll-snap-align: start;
    
      display: flex;
      justify-content: center;
      align-items: center;
      font-size: 4rem;
    }
    #container div:nth-child(1) {
      background: hotpink;
      color: white;
      height: 50vh;
    }
    #container div:nth-child(2) {
      background: azure;
      height: 40vh;
    }
    #container div:nth-child(3) {
      background: blanchedalmond;
      height: 60vh;
    }
    #container div:nth-child(4) {
      background: lightcoral;
      color: white;
      height: 40vh;
    }
    <div id="container">
      <div>1</div>
      <div>2</div>
      <div>3</div>
      <div>4</div>
    </div>
    
    <button onClick="goPrevious()">previous</button>
    <button onClick="goNext()">next</button>

    【讨论】:

    • 非常优雅的解决方案,但是如果您在 DOM 中有多个滑块,这将不起作用(因为索引不与每个滑块相关)
    • 仅当容器的视口仅显示 1 个项目时才有效。对于更多项目,变量activeIndex 实际上将保存最后显示的项目的索引。现在,假设您正在向下滚动(通过单击“下一步”或滚动/滑动或其他方式);然后activeIndex 将设置为显示的最后一项;现在单击“上一个”,没有任何反应,因为activeIndex - 1 已经可见,因此不需要scrollIntoView。对于超过 1 个可见的项目,也许我们需要实际跟踪每个项目的可见性;在goPrevious 上,我们必须scrollIntoView 第一个不可见的项目。
    【解决方案3】:

    使用 react 完成的更简单的方法。

    export const AppCarousel = props => {
    
      const containerRef = useRef(null);
      const carouselRef = useRef(null);
    
    
      const [state, setState] = useState({
        scroller: null,
        itemWidth: 0,
        isPrevHidden: true,
        isNextHidden: false
    
      })
    
      const next = () => {
        state.scroller.scrollBy({left: state.itemWidth * 3, top: 0, behavior: 'smooth'});
    
        // Hide if is the last item
        setState({...state, isNextHidden: true, isPrevHidden: false});
      }
    
    
       const prev = () => {
        state.scroller.scrollBy({left: -state.itemWidth * 3, top: 0, behavior: 'smooth'});
        setState({...state, isNextHidden: false, isPrevHidden: true});
        // Hide if is the last item
        // Show remaining
       }
    
      useEffect(() => {
    
          const items = containerRef.current.childNodes;
          const scroller = containerRef.current;
          const itemWidth = containerRef.current.firstElementChild?.clientWidth;
    
          setState({...state, scroller, itemWidth});
    
        return () => {
    
        }
      },[props.items])
    
    
      return (<div className="app-carousel" ref={carouselRef}>
    
          <div className="carousel-items shop-products products-swiper" ref={containerRef}>
              {props.children}
          </div>
          <div className="app-carousel--navigation">
            <button className="btn prev" onClick={e => prev()} hidden={state.isPrevHidden}>&lt;</button>
            <button className="btn next" onClick={e => next()} hidden={state.isNextHidden}>&gt;</button>
          </div>
    
      </div>)
    }
    
    

    【讨论】:

      猜你喜欢
      • 2018-12-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-10-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多