如何将滑块指示器位置与滑块滚动位置同步

How to sync slider indicator position with slider scroll position

我创建了这个滑块和自定义指示器来显示你在哪张幻灯片上,但我无法将指示器同步到滑块的滚动位置。

在下面的示例中,指标的 left 属性 被设置为主滑块滚动的任何百分比。但是,它没有考虑指示器本身的宽度,因此它超出了包含它的灰色框。

关于如何修改 moveIndicator 函数以正确显示指标的任何想法?同样在此示例中,屏幕上有两张幻灯片,但这需要在 1 或 3 张幻灯片的情况下工作(要测试此更改,请将 css 中的 <li> 元素宽度更改为 100%33.3%)

class SliderComponent extends HTMLElement {
  constructor() {
    super();
    this.slider = this.querySelector('ul');
    this.slides = this.querySelectorAll('li');
    this.sliderTray = this.querySelector('.indicator-tray');
    this.sliderIndicator = this.querySelector('.indicator');
    this.prevButton = this.querySelector('button[name="previous"]');
    this.nextButton = this.querySelector('button[name="next"]');

    const resizeObserver = new ResizeObserver(entries => this.initialise());
    resizeObserver.observe(this.slider);

    this.slider.addEventListener('scroll', this.update.bind(this));
    this.prevButton.addEventListener('click', this.onButtonClick.bind(this));
    this.nextButton.addEventListener('click', this.onButtonClick.bind(this));
  }

  initialise() {
    const slidesToShow = Array.from(this.slides).filter(element => element.clientWidth > 0);

    this.sliderLastItem = slidesToShow[slidesToShow.length - 1];

    if (slidesToShow.length === 0) return;

    this.slidesPerPage = Math.floor(this.slider.clientWidth / slidesToShow[0].clientWidth);
    this.totalPages = slidesToShow.length - this.slidesPerPage + 1;
    this.update();
  }

  moveIndicator() {
    const indicatorWidth = 100 / this.totalPages;
    const scrollPercentage = Math.floor(100 * (this.slider.scrollLeft / (this.slider.scrollWidth - this.slider.clientWidth)));

    this.sliderIndicator.style.width = `${indicatorWidth}%`;
    this.sliderIndicator.style.left = `${scrollPercentage}%`;
  }

  update() {
    this.currentPage = Math.round(this.slider.scrollLeft / this.sliderLastItem.clientWidth) + 1;
    requestAnimationFrame(() => this.moveIndicator());
  }

  onButtonClick(event) {
    event.preventDefault();
    const slideScrollPosition = event.currentTarget.name === 'next' ?
      this.slider.scrollLeft + this.sliderLastItem.clientWidth :
      this.slider.scrollLeft - this.sliderLastItem.clientWidth;

    this.slider.scrollTo({
      left: slideScrollPosition
    });
  }
}

customElements.define('slider-component', SliderComponent);
ul {
  position: relative;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  scroll-padding-left: 1rem;
  -webkit-overflow-scrolling: touch;
  display: flex;
  width: 100%;
  margin: 0;
  padding: 0;
  list-style: none;
}

li {
  width: 50%;
  min-width: auto;
  height: 100px;
  flex-shrink: 0;
  scroll-snap-align: start;
  background-color: lightblue;
  border: 2px dashed white;
}

.indicator-tray {
  position: relative;
  display: flex;
  height: 5px;
  width: 50%;
  background-color: grey;
  margin: 20px auto;
}

.indicator {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  background-color: lightblue;
}
<slider-component>
  <ul>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
  </ul>

  <div class="indicator-tray">
    <span class="indicator"></span>
  </div>

  <div class="arrows">
    <button type="button" name="previous">Prev</button>
    <button type="button" name="next">Next</button>
  </div>
</slider-component>

感谢 @DraganS 的评论 - 为希望构建类似内容的任何人提供的解决方案:

class SliderComponent extends HTMLElement {
  constructor() {
    super();
    this.slider = this.querySelector('ul');
    this.slides = this.querySelectorAll('li');
    this.sliderTray = this.querySelector('.indicator-tray');
    this.sliderIndicator = this.querySelector('.indicator');
    this.prevButton = this.querySelector('button[name="previous"]');
    this.nextButton = this.querySelector('button[name="next"]');

    const resizeObserver = new ResizeObserver(entries => this.initialise());
    resizeObserver.observe(this.slider);

    this.slider.addEventListener('scroll', this.update.bind(this));
    this.prevButton.addEventListener('click', this.onButtonClick.bind(this));
    this.nextButton.addEventListener('click', this.onButtonClick.bind(this));
  }

  initialise() {
    const slidesToShow = Array.from(this.slides).filter(element => element.clientWidth > 0);

    this.sliderLastItem = slidesToShow[slidesToShow.length - 1];

    if (slidesToShow.length === 0) return;

    this.slidesPerPage = Math.floor(this.slider.clientWidth / slidesToShow[0].clientWidth);
    this.totalPages = slidesToShow.length - this.slidesPerPage + 1;
    this.update();
  }

  moveIndicator() {
    const indicatorWidth = 100 / this.totalPages;
    const scrollPercentage = Math.floor(100 * (this.slider.scrollLeft / (this.slider.scrollWidth - this.slider.clientWidth)));
    const left = (scrollPercentage / 100) * (this.sliderTray.clientWidth - this.sliderIndicator.clientWidth);

    this.sliderIndicator.style.width = `${indicatorWidth}%`;
    this.sliderIndicator.style.left = `${left}px`;
  }

  update() {
    this.currentPage = Math.round(this.slider.scrollLeft / this.sliderLastItem.clientWidth) + 1;
    requestAnimationFrame(() => this.moveIndicator());
  }

  onButtonClick(event) {
    event.preventDefault();
    const slideScrollPosition = event.currentTarget.name === 'next' ?
      this.slider.scrollLeft + this.sliderLastItem.clientWidth :
      this.slider.scrollLeft - this.sliderLastItem.clientWidth;

    this.slider.scrollTo({
      left: slideScrollPosition
    });
  }
}

customElements.define('slider-component', SliderComponent);
ul {
  position: relative;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  scroll-padding-left: 1rem;
  -webkit-overflow-scrolling: touch;
  display: flex;
  width: 100%;
  margin: 0;
  padding: 0;
  list-style: none;
}

li {
  width: 50%;
  min-width: auto;
  height: 100px;
  flex-shrink: 0;
  scroll-snap-align: start;
  background-color: lightblue;
}

.indicator-tray {
  position: relative;
  display: flex;
  height: 5px;
  width: 50%;
  background-color: grey;
  margin: 20px auto;
}

.indicator {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  background-color: lightblue;
}
<slider-component>
  <ul>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
  </ul>

  <div class="indicator-tray">
    <span class="indicator"></span>
  </div>

  <div class="arrows">
    <button type="button" name="previous">Prev</button>
    <button type="button" name="next">Next</button>
  </div>
</slider-component>