<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": "#"
}
}
.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;
}
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;
No notes defined.