<div id="card-slider-16337-87180" class="card-slider">
    <div class="card-slider__cards u-hide-scrollbar" id="card-slider-16337-87180-cards">
        <div class="card-slider__card">
            <article class="card has-overlay-link" data-card-type="stacked">
                <div class="card__inner">
                    <div class="card__image">
                        <picture class="image image--cover">
                            <img class="image__img" src="/assets/images/card-placeholder-image.6a8af03ab4.svg" width="800" height="450" alt="" loading="lazy" role="presentation" />
                        </picture>

                    </div>

                    <div class="card__content">

                        <div class="card__head">

                            <div class="card__headline u-line-clamp">
                                <h3 class="headline headline--3"><a href="#" class="headline__link u-overlay-link"><span class="headline__kicker">Optionaler Kicker<span class="u-hidden-visually">: </span></span><span class="headline__text u-underline">Titel mit beinahe beliebig vielen Zeichen, da die Card mitwächst</span></a></h3>
                            </div>
                        </div>

                    </div>
                </div>
            </article>
        </div>
        <div class="card-slider__card">
            <article class="card has-overlay-link" data-card-type="stacked">
                <div class="card__inner">
                    <div class="card__image">
                        <picture class="image image--cover" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAABaADAAQAAAABAAAABQAAAABcYj7LAAAAFUlEQVQIHWP8V8PJgASYkNggJql8AHi/AY1z9PnwAAAAAElFTkSuQmCC)">
                            <img class="image__img" src="https://bildermangel.de/555x555/fe7c09/130f26.webp?text=+++S:+555x555@1x+++" width="555" height="555" alt="Das ist ein Platzhalter-Bild" loading="lazy" />
                        </picture>

                    </div>

                    <div class="card__content">

                        <div class="card__head">

                            <div class="card__headline u-line-clamp">
                                <h3 class="headline headline--3"><a href="#" class="headline__link u-overlay-link"><span class="headline__kicker">Optionaler Kicker<span class="u-hidden-visually">: </span></span><span class="headline__text u-underline">Titel mit beinahe beliebig vielen Zeichen, da die Card mitwächst</span></a></h3>
                            </div>
                        </div>

                    </div>
                </div>
            </article>
        </div>
        <div class="card-slider__card">
            <article class="card has-overlay-link" data-card-type="stacked">
                <div class="card__inner">
                    <div class="card__image">
                        <picture class="image image--cover">
                            <img class="image__img" src="/assets/images/card-placeholder-image.6a8af03ab4.svg" width="800" height="450" alt="" loading="lazy" role="presentation" />
                        </picture>

                    </div>

                    <div class="card__content">

                        <div class="card__head">

                            <div class="card__headline u-line-clamp">
                                <h3 class="headline headline--3"><a href="#" class="headline__link u-overlay-link"><span class="headline__kicker">Optionaler Kicker<span class="u-hidden-visually">: </span></span><span class="headline__text u-underline">Titel mit beinahe beliebig vielen Zeichen, da die Card mitwächst</span></a></h3>
                            </div>
                        </div>

                        <dl class="card__details">

                            <div class="card__detail">
                                <dt class="card__detail-label"><span class="card__detail-label-icon"><svg class="icon icon--calendar-alt" viewBox="0 0 200 200" aria-hidden="true">
                                            <use xlink:href="/assets/icons/icons.3bafb6df0d.svg#calendar-alt"></use>
                                        </svg></span><span class="card__detail-label-text u-hidden-visually">Datum</span></dt>
                                <dd class="card__detail-text">So, 8. Sep 2024</dd>
                            </div>

                        </dl>

                    </div>
                </div>
            </article>
        </div>
        <div class="card-slider__card">
            <article class="card has-overlay-link" data-card-color="purple" data-card-type="stacked">
                <div class="card__inner">
                    <div class="card__image">
                        <picture class="image image--cover">
                            <img class="image__img" src="/assets/images/card-placeholder-image.6a8af03ab4.svg" width="800" height="450" alt="" loading="lazy" role="presentation" />
                        </picture>

                    </div>

                    <div class="card__content">
                        <div class="card__badge">
                            <span class="card__badge-icon">
                                <svg class="icon icon--bell" viewBox="0 0 200 200" aria-hidden="true">
                                    <use xlink:href="/assets/icons/icons.3bafb6df0d.svg#bell"></use>
                                </svg> </span>

                            <span class="card__badge-text u-line-clamp">Tipp aus der Redaktion</span>
                        </div>

                        <div class="card__head">

                            <div class="card__headline u-line-clamp">
                                <h3 class="headline headline--3"><a href="#" class="headline__link u-overlay-link"><span class="headline__kicker">Optionaler Kicker<span class="u-hidden-visually">: </span></span><span class="headline__text u-underline">Titel mit beinahe beliebig vielen Zeichen, da die Card mitwächst</span></a></h3>
                            </div>
                        </div>

                    </div>
                </div>
            </article>
        </div>

        <div class="card-slider__card card-slider__card--more-card">
            <article class="card has-overlay-link" data-card-color="orange" data-card-type="stacked">
                <div class="card__inner">
                    <div class="card__image">
                        <picture class="image image--cover">
                            <img class="image__img" src="/assets/images/card-more.7884338daa.svg" width="800" height="450" alt="" loading="lazy" role="presentation" />
                        </picture>

                    </div>

                    <div class="card__content">

                        <div class="card__head">

                            <div class="card__headline u-line-clamp">
                                <h3 class="headline headline--3"><a href="#" class="headline__link u-overlay-link"><span class="headline__text u-underline">Mehr erfahren</span></a></h3>
                            </div>
                        </div>

                    </div>
                </div>
            </article>
        </div>
    </div>

    <div class="card-slider__navigation">
        <div class="card-slider__scroll-bar" aria-hidden="true" data-scroll-bar-target="card-slider-16337-87180-cards">
            <div class="card-slider__scroll-bar-backdrop"></div>
            <div class="card-slider__scroll-bar-thumb"></div>
        </div>

        <div class="card-slider__buttons">
            <button class="navigation-button card-slider__navigation-button card-slider__navigation-button--left" type="button" title="Nach links scrollen" disabled tabindex="-1">
                <svg class="icon icon--caret-left" viewBox="0 0 200 200" aria-hidden="true">
                    <use xlink:href="/assets/icons/icons.3bafb6df0d.svg#caret-left"></use>
                </svg></button>

            <button class="navigation-button card-slider__navigation-button card-slider__navigation-button--right" type="button" title="Nach rechts scrollen" disabled tabindex="-1">
                <svg class="icon icon--caret-right" viewBox="0 0 200 200" aria-hidden="true">
                    <use xlink:href="/assets/icons/icons.3bafb6df0d.svg#caret-right"></use>
                </svg></button>
        </div>
    </div>
</div>
{% set id = id ??? html_id('card-slider') %}

<div {{ html_attributes({
  id: id,
  class: {
    'card-slider': true,
    'card-slider--randomize': randomize ?? false,
  },
}, attrs ?? {}) }}>
  <div class="card-slider__cards u-hide-scrollbar" id="{{ 'cards' | namespaceInputId(id) }}">
    {% for card in cards %}
      <div class="card-slider__card">
        {% include '@card' with card | merge({
          color: card.color ?? color ?? null,
          type: 'stacked',
          useFallbackImage: true,
        }) only %}
      </div>
    {% endfor %}

    {% if moreCard|default %}
      <div class="card-slider__card card-slider__card--more-card">
        {% include '@card' with moreCard | merge({
          more: true,
          color: 'orange',
          type: 'stacked',
          headline: {
            text: moreCard.text ?? 'More Information' | t('site'),
          },
        }) only %}
      </div>
    {% endif %}
  </div>

  <div class="card-slider__navigation">
    <div class="card-slider__scroll-bar" aria-hidden="true" data-scroll-bar-target="{{ 'cards' | namespaceInputId(id) }}">
      <div class="card-slider__scroll-bar-backdrop"></div>
      <div class="card-slider__scroll-bar-thumb"></div>
    </div>

    <div class="card-slider__buttons">
      {% include '@navigation-button' with {
        title: 'Scroll left' | t('site'),
        direction: 'left',
        disabled: true,
        tabindex: '-1',
        attrs: {
          class: 'card-slider__navigation-button card-slider__navigation-button--left',
        },
      } only %}

      {% include '@navigation-button' with {
        title: 'Scroll right' | t('site'),
        direction: 'right',
        disabled: true,
        tabindex: '-1',
        attrs: {
          class: 'card-slider__navigation-button card-slider__navigation-button--right',
        },
      } only %}
    </div>
  </div>
</div>
{
  "cards": [
    {
      "headline": {
        "kicker": "Optionaler Kicker",
        "text": "Titel mit beinahe beliebig vielen Zeichen, da die Card mitwächst"
      },
      "link": "#"
    },
    {
      "headline": {
        "kicker": "Optionaler Kicker",
        "text": "Titel mit beinahe beliebig vielen Zeichen, da die Card mitwächst"
      },
      "link": "#",
      "image": {
        "src": "https://bildermangel.de/555x555/fe7c09/130f26.webp?text=+++S:+555x555@1x+++",
        "width": 555,
        "height": 555,
        "cover": true,
        "alt": "Das ist ein Platzhalter-Bild",
        "placeholder": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAABaADAAQAAAABAAAABQAAAABcYj7LAAAAFUlEQVQIHWP8V8PJgASYkNggJql8AHi/AY1z9PnwAAAAAElFTkSuQmCC"
      }
    },
    {
      "headline": {
        "kicker": "Optionaler Kicker",
        "text": "Titel mit beinahe beliebig vielen Zeichen, da die Card mitwächst"
      },
      "link": "#",
      "hintBox": {
        "value": "2024-09-08T00:00:00.000Z"
      }
    },
    {
      "headline": {
        "kicker": "Optionaler Kicker",
        "text": "Titel mit beinahe beliebig vielen Zeichen, da die Card mitwächst"
      },
      "link": "#",
      "color": "purple",
      "useFallbackImage": false,
      "badge": {
        "icon": "bell",
        "text": "Tipp aus der Redaktion"
      }
    }
  ],
  "moreCard": {
    "text": "Mehr erfahren",
    "link": "#"
  }
}
  • Content:
    .card-slider--jobs {
      --card-headline-font-size-factor: 0.06;
      --card-headline-font-size-max: 3.2rem;
      --card-headline-font-size-min: 1.6rem;
      --card-headline-lines: 3;
      --card-description-lines: 1;
      --card-hyphens: auto;
    }
    
    .card-slider__inner {
      @include use-break-out();
    }
    
    .card-slider__cards {
      display: flex;
      overflow-x: scroll;
      overflow-y: hidden;
      position: relative;
      scroll-behavior: smooth;
      scroll-snap-type: x mandatory;
      z-index: 1;
    
      @include use-responsive-sizing(gap, $gaps);
    }
    
    .card-slider__cards--was-dragged {
      scroll-behavior: auto;
    }
    
    .card-slider__card {
      --focus-outline-offset: -2px;
      --card-headline-line-clamp: 3;
    
      display: grid;
      flex-shrink: 0;
      scroll-snap-align: start;
    
      .card-slider__cards--was-dragged & {
        scroll-snap-align: none;
      }
    
      @include use-responsive-sizing(width, (
        xs: 80%,
        m: column-width(m, 6),
        l: column-width(l, 4),
        xl: column-width(xl, 4),
      ));
      @include use-responsive-sizing(max-width, (
        xs: 30rem,
        m: 30rem,
        l: none,
      ));
    }
    
    .card-slider__card--more-card {
      --tile-aspect-ratio: var(--card-aspect-ratio);
      --icon-button-background-color: transparent;
      --icon-button-background-color-active: var(--color-orange);
      --tile-background-color: var(--color-cyan-light);
    }
    
    .card-slider__navigation {
      align-items: center;
      display: flex;
      gap: 2rem;
      margin-block-start: 2rem;
    }
    
    .card-slider__scroll-bar {
      block-size: 1rem;
      flex-grow: 1;
      opacity: 0.3;
      pointer-events: none;
      position: relative;
      transition-property: opacity, transform;
    }
    
    .card-slider__scroll-bar--enabled {
      cursor: pointer;
      opacity: 1;
      pointer-events: all;
    
      &:hover {
        transform: scaleY(2);
      }
    }
    
    .card-slider__scroll-bar-backdrop {
      background-color: var(--color-grey-x-light);
      border-radius: 0.5rem;
      inset: 0;
      position: absolute;
    }
    
    .card-slider__scroll-bar-thumb {
      background-color: var(--color-midnight);
      border-radius: 0.5rem;
      inset-block: 0;
      position: absolute;
      will-change: left, width, transform;
    }
    
    .card-slider__buttons {
      display: flex;
      flex-shrink: 0;
      gap: 2rem;
      touch-action: manipulation;
    }
    
  • URL: /components/raw/card-slider/card-slider.scss
  • Filesystem Path: src/components/3-organisms/card-slider/card-slider.scss
  • Size: 2.1 KB
  • Content:
    import Draggabilly from 'draggabilly';
    
    class CardSlider {
      $cardSlider: HTMLElement;
      $cards: HTMLElement;
      $scrollBar: HTMLElement;
      $thumb: HTMLElement;
      $leftScrollButton: HTMLElement | null = null;
      $rightScrollButton: HTMLElement | null = null;
      thumbLeft = 0;
      enableScrollbarUpdates = true;
      enableLeftScrollButton = false;
      enableRightScrollButton = false;
    
      constructor($cardSlider: HTMLElement) {
        this.$cardSlider = $cardSlider;
    
        const $cards = this.$cardSlider.querySelector<HTMLElement>(
          '.card-slider__cards',
        );
    
        const $scrollBar = this.$cardSlider.querySelector<HTMLElement>(
          '.card-slider__scroll-bar',
        );
    
        const $thumb = $scrollBar?.querySelector<HTMLElement>(
          '.card-slider__scroll-bar-thumb',
        );
    
        if (!$cards || !$scrollBar || !$thumb) {
          throw new Error('Cards and/or scroll bar are missing');
        }
    
        this.$cards = $cards;
        this.$scrollBar = $scrollBar;
        this.$thumb = $thumb;
    
        // Init everything
        this.initScrollButtons();
        this.initScrollBar();
    
        // Force to scroll position to zero on load
        window.requestAnimationFrame(() => {
          this.$cards.scrollTo({ left: 0, behavior: 'instant' });
          this.update();
        });
    
        // Inside a tab?
        $cardSlider
          .closest('.tabs__panel')
          ?.addEventListener('tabs:visible', () => {
            this.$cards.scrollTo({ left: 0, behavior: 'instant' });
            this.update();
          });
      }
    
      initScrollBar() {
        // Update on scroll and initial load
        this.$cards.addEventListener('scroll', () => this.update(), {
          passive: true,
        });
    
        // Update on resizing of scroll bar
        const resizeObserver = new ResizeObserver(() => this.update());
        resizeObserver.observe(this.$scrollBar);
    
        // Move on backdrop click
        this.$scrollBar
          .querySelector('.card-slider__scroll-bar-backdrop')
          ?.addEventListener('click', (event) => {
            // @ts-expect-error layerX is a non-standard, but well supported attribute on MouseEvent
            const { layerX } = event;
    
            this.$cards.scrollLeft =
              (layerX / this.$scrollBar.offsetWidth) * this.$cards.scrollWidth;
          });
    
        const draggie = new Draggabilly(this.$thumb, {
          axis: 'x',
          containment: true,
        });
    
        draggie.on('dragStart', () => {
          this.enableScrollbarUpdates = false;
          this.$cards.classList.add('card-slider__cards--was-dragged');
        });
    
        draggie.on('dragMove', (event, pointer, moveVector) => {
          this.$cards.scrollLeft =
            ((this.thumbLeft + moveVector.x) / this.$scrollBar.offsetWidth) *
            this.$cards.scrollWidth;
        });
    
        draggie.on('dragEnd', () => {
          this.enableScrollbarUpdates = true;
          this.$cards.classList.remove('card-slider__cards--was-dragged');
          this.update();
        });
      }
    
      initScrollButtons() {
        // Left scroll button
        this.$leftScrollButton = this.$cardSlider.querySelector(
          '.card-slider__navigation-button--left',
        );
    
        this.$leftScrollButton?.addEventListener('click', (event) => {
          event.preventDefault();
          this.move('left');
        });
    
        // Right scroll button
        this.$rightScrollButton = this.$cardSlider.querySelector(
          '.card-slider__navigation-button--right',
        );
    
        this.$rightScrollButton?.addEventListener('click', (event) => {
          event.preventDefault();
          this.move('right');
        });
      }
    
      move(direciton: 'left' | 'right') {
        const cardWidth =
          this.$cards.querySelector<HTMLElement>('.card-slider__card')?.offsetWidth;
    
        if (cardWidth) {
          this.$cards.scrollTo({
            left:
              this.$cards.scrollLeft +
              (direciton === 'left' ? cardWidth * -1 : cardWidth),
          });
        }
      }
    
      update() {
        if (!this.enableScrollbarUpdates) {
          return;
        }
    
        window.requestAnimationFrame(() => {
          const width = (this.$cards.offsetWidth / this.$cards.scrollWidth) * 100;
          this.thumbLeft =
            (this.$cards.scrollLeft / this.$cards.scrollWidth) *
            this.$scrollBar.offsetWidth;
    
          this.$thumb.style.setProperty('width', `${width}%`);
          this.$thumb.style.setProperty('left', `${this.thumbLeft}px`);
    
          this.$scrollBar.classList.toggle(
            'card-slider__scroll-bar--enabled',
            width < 100,
          );
    
          this.$leftScrollButton?.toggleAttribute(
            'disabled',
            this.$cards.scrollLeft === 0,
          );
    
          this.$rightScrollButton?.toggleAttribute(
            'disabled',
            this.$cards.scrollWidth <=
              this.$cards.scrollLeft + this.$cards.offsetWidth,
          );
        });
      }
    }
    
    document
      .querySelectorAll<HTMLElement>('.card-slider')
      .forEach(($cardSlider) => new CardSlider($cardSlider));
    
    document
      .querySelectorAll<HTMLElement>('.card-slider--randomize')
      .forEach(($cardSlider) => {
        const $cards = $cardSlider.querySelector<HTMLElement>(
          '.card-slider__cards',
        );
    
        if ($cards) {
          for (let i = $cards.children.length; i >= 0; i -= 1) {
            const index = Math.floor(Math.random() * (i + 1)); // nosemgrep: nodejs_scan.javascript-crypto-rule-node_insecure_random_generator
            // eslint-disable-next-line security/detect-object-injection
            const $child = $cards.children[index]; // nosemgrep: eslint.detect-object-injection
    
            if ($child) {
              $cards.appendChild($child);
            }
          }
    
          const $moreCard = $cards.querySelector('.card-slider__card--more-card');
    
          if ($moreCard) {
            $cards.appendChild($moreCard);
          }
        }
      });
    
    export default CardSlider;
    
  • URL: /components/raw/card-slider/card-slider.ts
  • Filesystem Path: src/components/3-organisms/card-slider/card-slider.ts
  • Size: 5.5 KB

No notes defined.