# Carousel & JavaScript Patterns

> **Reference for building components that include sliders, carousels, or custom JS interactions**

## Carousel Component

### Architecture

The carousel stack is Vue 2 + Swiper 8:

```
carousel.twig (Twig include)
  └── <mtc-slider> (Vue component: MtcSliderWrapper)
        └── <mtc-swiper> (Vue component: SwiperCompat — wraps Swiper 8)
              └── <mtc-slide> (Vue component: SwiperSlideCompat — renders .swiper-slide)
```

All three Vue components are registered globally in `sites/default/js/main-app.js` and mount inside `#vue_environment` which wraps the entire page content.

### How to Use: `items` + `itemTemplate` Pattern (Recommended)

This is the proven approach used by `category_hero.twig` and other working carousels. Always prefer this over embed blocks or raw `<mtc-slider>` tags.

**Step 1: Build a flat items array in your template**

```twig
{% set carousel_items = [] %}
{% for product in products %}
    {% set carousel_items = carousel_items | merge([{
        title: product["Title"]["value"] | default(""),
        image: prod_img,
        description: product["Description"]["value"] | default("")
    }]) %}
{% endfor %}
```

**Step 2: Include carousel.twig with items + itemTemplate**

```twig
{% include 'includes/elements/carousel.twig' with {
    items: carousel_items,
    cssClass: 'myCarousel',
    itemTemplate: 'includes/page_blocks/my_carousel_slide.twig',
    options: {
        perPage: 1.05,
        gap: 24,
        arrows: false,
        pagination: false,
        loop: false,
        speed: 400,
        breakpoints: {
            '980': { perPage: 1.05, gap: 24 },
            '640': { perPage: 1, gap: 16 }
        }
    }
} %}
```

**Step 3: Create a slide template**

The slide template receives each item as `item`. Example (`my_carousel_slide.twig`):

```twig
<div class="myCarousel__card">
    {% if item.image is defined and item.image is not empty %}
        <img src="{{ item.image }}" alt="{{ item.title | default('') }}" loading="lazy">
    {% endif %}
    <h3>{{ item.title }}</h3>
</div>
```

### Why NOT to Use Other Approaches

**Do not use raw `<mtc-slider>` tags directly.** While technically valid, you lose the wrapper structure and default handling that `carousel.twig` provides.

**Do not use `embed` with `{% block carousel_slides %}`.** The carousel.twig template sets default dummy items at the top (`{% if items is not defined %}`), which means the `items` branch always wins and the block is never reached.

### Reference: `carousel.twig` Options

Options use Splide naming (the wrapper translates to Swiper):

| Option | Type | Description |
|--------|------|-------------|
| `perPage` | number | Slides visible at once (Swiper: slidesPerView) |
| `gap` | number | Space between slides in px (Swiper: spaceBetween) |
| `arrows` | boolean | Show prev/next navigation |
| `pagination` | boolean | Show dot pagination |
| `loop` | boolean | Infinite loop mode |
| `speed` | number | Transition speed in ms |
| `autoplay` | boolean | Enable autoplay |
| `autoplaySpeed` | number | Autoplay delay in ms |
| `perMove` | number | Slides to move per interaction (Swiper: slidesPerGroup) |
| `breakpoints` | object | Responsive overrides keyed by min-width |

### Custom Navigation Arrows

If you need a custom arrow button outside the Swiper navigation, access the Swiper instance via the DOM element:

```twig
<button class="myComponent__arrow" data-my-next aria-label="Next slide">
    <i class="fa-regular fa-chevron-right"></i>
</button>
```

```js
document.addEventListener('click', function (e) {
    const arrow = e.target.closest('[data-my-next]');
    if (!arrow) return;

    const container = arrow.closest('.myComponent');
    if (!container) return;

    const swiperEl = container.querySelector('.swiper-initialized');
    if (swiperEl && swiperEl.swiper) {
        try {
            swiperEl.swiper.slideNext();
        } catch (err) {
            // Swiper may throw on edge slides
        }
    }
});
```

## Swiper Gotchas

### `display: none` Breaks Swiper

Swiper cannot calculate slide dimensions inside a `display: none` container. Slides get zero width and the snap grid is invalid.

**If you need to hide a panel containing a Swiper** (e.g. tabs), use visibility-based hiding:

```less
&__panel {
    visibility: hidden;
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    pointer-events: none;
    z-index: 0;

    &--active {
        visibility: visible;
        position: relative;
        pointer-events: auto;
        z-index: 1;
    }
}
```

The parent container needs `position: relative` so absolute panels overlay correctly.

### `perPage: 1` With Few Slides Creates No Snap Points

When slides exactly fill the container (e.g. `perPage: 1` with 2 slides of 100% width), Swiper calculates a single snap point and `isEnd: true` at index 0. The slider appears frozen.

**Fix:** Use a fractional value like `perPage: 1.05`. This creates a slight "peek" of the next slide and forces Swiper to generate proper snap points.

### Swiper Update After Showing Hidden Panels

When switching tabs that contain Swipers, call `update()` after the panel becomes visible:

```js
setTimeout(function () {
    const swiperEl = targetPanel.querySelector('.swiper-initialized');
    if (swiperEl && swiperEl.swiper) {
        swiperEl.swiper.update();
    }
}, 50);
```

## JavaScript Patterns for `site_scripts/`

### Event Delegation (Required Pattern)

All JS in `sites/default/js/site_scripts/` must use **event delegation** on `document`. Do NOT use `DOMContentLoaded` wrappers.

**Why:** Vite HMR loads modules dynamically after `DOMContentLoaded` has already fired. Event listeners wrapped in `DOMContentLoaded` will never attach.

**Correct:**
```js
document.addEventListener('click', function (e) {
    const trigger = e.target.closest('[data-my-trigger]');
    if (!trigger) return;

    // Handle click
});
```

**Wrong:**
```js
// This will NOT work — DOMContentLoaded has already fired
document.addEventListener('DOMContentLoaded', function () {
    document.querySelectorAll('[data-my-trigger]').forEach(function (el) {
        el.addEventListener('click', function () { /* ... */ });
    });
});
```

### Reference: `mtc_media_accordion.js`

This is the canonical example of the event delegation pattern. All new site_scripts should follow this structure.

### Importing Site Scripts

Add the import to `sites/default/js/main-app.js`:

```js
import '@site_scripts/my_component.js'
```

Place it with the other site_scripts imports (after the Vue component registrations, before `import './script.js'`).
