<div class="scrollbar">
    <div class="scrollbar__thumb"></div>
</div>
<div {{ html_attributes({
  id: id ?? false,
  class: 'scrollbar',
  'data-scrollbar-sync': sync ?? false,
  'data-scrollbar-item-selector': itemSelector ?? false,
  'data-scrollbar-prev-button': prevButton ?? false,
  'data-scrollbar-next-button': nextButton ?? false,
}, attrs ?? {}) }}>
  <div class="scrollbar__thumb"></div>
</div>
/* No context defined. */
  • Content:
    @property --scrollbar-thumb-x {
      inherits: true;
      initial-value: 0;
      syntax: '<length-percentage>';
    }
    
    .scrollbar {
      --_scrollbar-thumb-size: var(--scrollbar-thumb-size, 0.8rem);
      --_scrollbar-track-size: var(--scrollbar-track-size, 0.2rem);
    
      isolation: isolate;
      position: relative;
    
      &::before {
        background-color: var(--scrollbar-track-color, var(--color-gray-300));
        block-size: var(--_scrollbar-track-size);
        border-radius: calc(var(--_scrollbar-track-size) / 2);
        content: '';
        inset-block-start: 50%;
        inset-inline: 0;
        position: absolute;
        translate: 0 -50%;
        z-index: 1;
      }
    }
    
    .scrollbar__thumb {
      background-color: var(--scrollbar-thumb-color, var(--color-gray-600));
      block-size: var(--_scrollbar-thumb-size);
      border-radius: calc(var(--_scrollbar-thumb-size) / 2);
      inline-size: clamp(4.4rem, var(--scrollbar-progress, 0%), 100%);
      opacity: var(--scrollbar-thumb-opacity, 1);
      position: relative;
      touch-action: pan-x;
      transform: translate3d(var(--scrollbar-thumb-x), 0, 0);
      transform-origin: 0 0;
      transition-property: opacity;
      will-change: inline-size, transform;
      z-index: 2;
    
      &::after {
        block-size: 4.4rem;
        content: '';
        inset-block-start: 50%;
        inset-inline: 0;
        position: absolute;
        translate: 0 -50%;
      }
    }
    
  • URL: /components/raw/scrollbar/scrollbar.scss
  • Filesystem Path: src/components/1-particles/scrollbar/scrollbar.scss
  • Size: 1.3 KB
  • Content:
    import { on } from 'delegated-events';
    import getInputPosition from '../../../javascripts/utils/getInputPosition';
    import getTranslatePosition from '../../../javascripts/utils/getTranslatePosition';
    import ScrollbarEvent from '../../../javascripts/events/ScrollbarEvent';
    import abort from '../../../javascripts/utils/abort';
    import isVisible from '../../../javascripts/utils/isVisible';
    import steps from '../../../javascripts/utils/steps';
    
    const handleThumbChange = (event: (MouseEvent | TouchEvent) & { currentTarget: HTMLElement }) => {
      const { currentTarget: $thumb } = event;
      const { parentElement: $scrollbar } = $thumb;
    
      if (!$scrollbar) {
        return;
      }
    
      event.preventDefault();
    
      const { x: startPosition } = getInputPosition(event);
      const { x: thumbX } = getTranslatePosition($thumb);
      const maxPosition = $scrollbar.scrollWidth - $thumb.scrollWidth;
    
      $scrollbar.dispatchEvent(new ScrollbarEvent('start', thumbX / maxPosition));
    
      const moveHandler = (moveEvent: MouseEvent | TouchEvent) => {
        const { x: currentPosition } = getInputPosition(moveEvent);
        const newPosition = Math.min(Math.max(thumbX + currentPosition - startPosition, 0), maxPosition);
    
        $scrollbar.style.setProperty('--scrollbar-thumb-x', `${newPosition}px`);
        $scrollbar.dispatchEvent(new ScrollbarEvent('move', newPosition / maxPosition));
      };
    
      const endHandler = (moveEventName: 'mousemove' | 'touchmove') => {
        window.removeEventListener(moveEventName, moveHandler);
        document.body.style.pointerEvents = '';
    
        const { x: finalThumbX } = getTranslatePosition($thumb);
    
        $scrollbar.dispatchEvent(new ScrollbarEvent('end', finalThumbX / maxPosition));
      };
    
      document.body.style.pointerEvents = 'none';
    
      if (window.MouseEvent && event instanceof MouseEvent) {
        window.addEventListener('mousemove', moveHandler);
        window.addEventListener('mouseup', () => endHandler('mousemove'), { once: true });
      } else if (window.TouchEvent && event instanceof TouchEvent) {
        window.addEventListener('touchmove', moveHandler);
        window.addEventListener('touchend', () => endHandler('touchmove'), { once: true });
      }
    };
    
    on('touchstart', '.scrollbar__thumb', handleThumbChange);
    on('mousedown', '.scrollbar__thumb', handleThumbChange);
    
    on('click', '.scrollbar', (event) => {
      const { currentTarget: $scrollbar } = event;
      const $thumb = $scrollbar.querySelector<HTMLElement>('.scrollbar__thumb') ?? abort();
      const maxPosition = $scrollbar.scrollWidth - $thumb.scrollWidth;
      const { left: leftOffset } = $scrollbar.getBoundingClientRect();
      const { x: position } = getInputPosition(event);
      const { x: thumbX } = getTranslatePosition($thumb);
      const { offsetWidth } = $thumb;
      const moveDirection = (position - leftOffset) > thumbX ? 1 : -1;
      const newPosition = Math.min(Math.max(thumbX + (offsetWidth * moveDirection), 0), maxPosition);
      const percent = newPosition / maxPosition;
    
      $scrollbar.dispatchEvent(new ScrollbarEvent('start', thumbX / maxPosition));
    
      requestAnimationFrame(() => {
        const animationDuration = 200;
        const updateEveryMs = 5;
        const animationSteps = steps(thumbX / maxPosition, percent, animationDuration / updateEveryMs);
    
        let currentStep = 0;
    
        const animation = $scrollbar.animate(
          [{ '--scrollbar-thumb-x': `${newPosition}px` }],
          { duration: animationDuration, fill: 'forwards' },
        );
    
        const moveDispatcher = setInterval(() => {
          const nextStep = animationSteps[currentStep];
          currentStep += 1;
    
          if (nextStep !== undefined) {
            $scrollbar.dispatchEvent(new ScrollbarEvent('move', nextStep));
          } else {
            clearInterval(moveDispatcher);
    
            animation.cancel();
            $scrollbar.style.setProperty('--scrollbar-thumb-x', `${newPosition}px`);
            $scrollbar.dispatchEvent(new ScrollbarEvent('end', percent));
          }
        }, updateEveryMs);
      });
    });
    
    document.querySelectorAll<HTMLElement>('.scrollbar[data-scrollbar-sync]').forEach(($scrollbar) => {
      const {
        scrollbarSync: targetId,
        scrollbarItemSelector: itemSelector,
        scrollbarPrevButton: prevButtonSelector,
        scrollbarNextButton: nextButtonSelector,
      } = $scrollbar.dataset;
    
      const $thumb = $scrollbar.querySelector<HTMLElement>('.scrollbar__thumb') ?? abort();
      const $target = document.querySelector<HTMLElement>(`#${targetId ?? abort()}`) ?? abort();
      const $prevButton = prevButtonSelector ? document.querySelector<HTMLButtonElement>(`#${prevButtonSelector}`) : null;
      const $nextButton = nextButtonSelector ? document.querySelector<HTMLButtonElement>(`#${nextButtonSelector}`) : null;
    
      let inProgress = false;
    
      const getScrollPaddingInlineStart = ($element: HTMLElement) => {
        const { scrollPaddingInlineStart } = window.getComputedStyle($element);
        return scrollPaddingInlineStart !== 'auto' ? parseInt(scrollPaddingInlineStart, 10) : 0;
      };
    
      const updateSize = () => {
        const scrollPadding = getScrollPaddingInlineStart($target);
        const visible = Math.min(($target.offsetWidth / ($target.scrollWidth - scrollPadding)) * 100, 100);
    
        $scrollbar.style.setProperty('--scrollbar-progress', `${visible}%`);
      };
    
      const updatePosition = () => {
        const scrollPosition = $target.scrollLeft / ($target.scrollWidth - $target.offsetWidth);
    
        if (Number.isNaN(scrollPosition)) {
          $scrollbar.style.setProperty('--scrollbar-thumb-opacity', '0');
          $scrollbar.style.setProperty('--scrollbar-thumb-x', '0px');
        } else {
          const maxPosition = $scrollbar.scrollWidth - $thumb.scrollWidth;
    
          $scrollbar.style.setProperty('--scrollbar-thumb-x', `${scrollPosition * maxPosition}px`);
          $scrollbar.style.removeProperty('--scrollbar-thumb-opacity');
        }
      };
    
      const updateButtons = () => {
        const canPrev = $target.scrollLeft <= 1;
        const canNext = $target.scrollWidth <= $target.scrollLeft + $target.offsetWidth;
    
        $prevButton?.toggleAttribute('disabled', canPrev);
        $nextButton?.toggleAttribute('disabled', canNext);
      };
    
      $scrollbar.addEventListener('scrollbarstart', () => {
        inProgress = true;
    
        $target.style.scrollSnapType = 'none';
      });
    
      $scrollbar.addEventListener('scrollbarmove', (event) => {
        $target.scrollTo({
          left: (event as ScrollbarEvent).percent * ($target.scrollWidth - $target.offsetWidth),
          behavior: 'instant',
        });
    
        updateButtons();
      });
    
      $scrollbar.addEventListener('scrollbarend', () => {
        if (itemSelector) {
          const scrollPadding = getScrollPaddingInlineStart($target);
          const maxScrollLeft = $target.scrollWidth - $target.offsetWidth;
    
          const $firstVisibleItem = [...$target.querySelectorAll<HTMLElement>(itemSelector)]
            .find(($item) => isVisible($item, $target));
    
          if ($firstVisibleItem && $target.scrollLeft !== maxScrollLeft) {
            $target.scrollTo({
              left: $firstVisibleItem.offsetLeft - $target.offsetLeft - scrollPadding,
              behavior: 'smooth',
            });
          }
        }
    
        $target.style.scrollSnapType = '';
    
        inProgress = false;
    
        updatePosition();
        updateButtons();
      });
    
      $target.addEventListener('scroll', () => {
        if (!inProgress) {
          updatePosition();
        }
    
        updateButtons();
      }, { passive: true });
    
      $prevButton?.addEventListener('click', (clickEvent) => {
        clickEvent.preventDefault();
    
        $target.scrollBy({
          left: -$target.offsetWidth,
          behavior: 'smooth',
        });
      });
    
      $nextButton?.addEventListener('click', (clickEvent) => {
        clickEvent.preventDefault();
    
        $target.scrollBy({
          left: $target.offsetWidth,
          behavior: 'smooth',
        });
      });
    
      const resizeObserver = new ResizeObserver(() => {
        updatePosition();
        updateSize();
        updateButtons();
      });
    
      requestAnimationFrame(() => {
        $target.scrollTo({ left: 0, behavior: 'instant' });
    
        updatePosition();
        updateSize();
        updateButtons();
      });
    
      resizeObserver.observe($target);
    });
    
  • URL: /components/raw/scrollbar/scrollbar.ts
  • Filesystem Path: src/components/1-particles/scrollbar/scrollbar.ts
  • Size: 7.9 KB

No notes defined.