PageSourceSearch

https://www.wort-christi-stiftung.de/_assets/37dd15eb4ff368730622efb21d4524ff/Js/slider.js?1788860118

js wort-christi-stiftung.de collected 2026-10-08 08:10:05 UTC 13,691 bytes, 349 lines download raw bytes

1/**
2 * OP3 shared slider
3 *
4 * A single Swiper implementation for every OP3 slider content block (hero-slider,
5 * hero-action-slider). Each block includes this file from its frontend.html via
6 * `<f:asset.script identifier="op3-slider" …>`, so it loads exactly once per page
7 * even when several sliders are present. Swiper itself is provided globally
8 * (Configuration/TypoScript/Setup/20.importCss.typoscript).
9 *
10 * DOM contract — every slider template exposes the SAME behavioural hooks; only the
11 * visual styling is scoped per component. This script never selects by CSS class that
12 * carries styling meaning, and never touches component-specific markup:
13 *   [data-slider]                 component root, and:
14 *       data-autoplay             "1" / "true" enables autoplay
15 *       data-autoplay-duration    delay in ms (default 5000)
16 *       data-effect               optional Swiper effect (e.g. "fade")
17 *       data-slider-labelled-by   ID of a visible heading that names the slider
18 *       data-slider-label         text name used when no visible heading exists
19 *       data-labels               JSON object of localized a11y strings, rendered by the
20 *                                 Fluid template via <op3:jsonLabels> from the block's
21 *                                 labels.xlf: { autoplayPlay, autoplayPause, prev, next,
22 *                                 gotoSlide } (gotoSlide holds a "%s" slide-number template).
23 *                                 This keeps slider.js translation-agnostic.
24 *   .swiper                       Swiper container
25 *   [data-direction="prev|next"]  navigation buttons
26 *   [data-pagination]             grouped pagination container
27 *   [data-autoplay-toggle]        play/pause button (data-paused reflects state)
28 *   [data-autoplay-progress]      optional progress-bar fill; the --duration custom
29 *                                 property and the data-running / data-paused state
30 *                                 are written here at runtime
31 *   [data-autoplay-progress-host] optional per-slide host; when present, the single
32 *                                 progress indicator follows the active slide's media
33 *
34 * The active pagination item is exposed through aria-current. The autoplay control uses
35 * a changing command label ("Pause" / "Start"), so it deliberately has no aria-pressed
36 * state; data-paused is only the visual icon hook.
37 */
38(function () {
39    "use strict";
40
41    /**
42     * Behavioural init hook shared by every slider (never a styling class).
43     * @constant {string}
44     */
45    var COMPONENT_SELECTOR = "[data-slider]";
46
47    /**
48     * The user's reduced-motion preference — identical for every slider on the page,
49     * so it is read once here rather than per component.
50     * @constant {boolean}
51     */
52    var PREFERS_REDUCED_MOTION = window.matchMedia(
53        "(prefers-reduced-motion: reduce)"
54    ).matches;
55
56    /**
57     * Parses the OP3 standard `data-labels` JSON attribute (see <op3:jsonLabels>).
58     * Returns an empty object for a missing or malformed value, so callers can
59     * safely merge it over their own fallbacks.
60     *
61     * @param {string|undefined} raw - The raw data-labels attribute value.
62     * @returns {Object} Parsed label map, or {} on absence/parse error.
63     */
64    function parseLabels(raw) {
65        if (!raw) {
66            return {};
67        }
68        try {
69            return JSON.parse(raw);
70        } catch (error) {
71            return {};
72        }
73    }
74
75    /**
76     * Reflects the running/paused autoplay state onto the toggle button.
77     *
78     * @param {HTMLElement|null} toggle - The autoplay toggle button.
79     * @param {boolean} paused - Whether autoplay is currently paused.
80     * @param {Object} labels - Localized labels (autoplayPlay / autoplayPause).
81     * @returns {void}
82     */
83    function updateAutoplayToggle(toggle, paused, labels) {
84        if (!toggle) {
85            return;
86        }
87        toggle.toggleAttribute("data-paused", paused);
88        toggle.setAttribute(
89            "aria-label",
90            paused ? labels.autoplayPlay : labels.autoplayPause
91        );
92    }
93
94    /**
95     * Builds and wires a single slider component.
96     *
97     * @param {HTMLElement} component - The component root element.
98     * @returns {void}
99     */
100    function initComponent(component) {
101        var swiperElement = component.querySelector(".swiper");
102        if (!swiperElement) {
103            return;
104        }
105
106        var pagination = component.querySelector("[data-pagination]");
107        var nextButton = component.querySelector('[data-direction="next"]');
108        var prevButton = component.querySelector('[data-direction="prev"]');
109        var autoplayToggle = component.querySelector("[data-autoplay-toggle]");
110        var progressBar = component.querySelector("[data-autoplay-progress]");
111        var progressIndicator = progressBar
112            ? progressBar.closest(".autoplay-progress")
113            : null;
114        var sliderLabelledBy = component.dataset.sliderLabelledBy;
115
116        if (sliderLabelledBy && document.getElementById(sliderLabelledBy)) {
117            swiperElement.setAttribute("aria-labelledby", sliderLabelledBy);
118            swiperElement.removeAttribute("aria-label");
119        } else {
120            sliderLabelledBy = "";
121        }
122
123        /**
124         * Localized a11y strings from the single data-labels JSON attribute (rendered
125         * by <op3:jsonLabels> from the block's labels.xlf), merged over German fallbacks
126         * that keep the slider functional if the attribute is missing or incomplete.
127         */
128        var labels = Object.assign(
129            {
130                autoplayPlay: "Autoplay starten",
131                autoplayPause: "Autoplay pausieren",
132                prev: "Vorheriger Slide",
133                next: "Nächster Slide",
134                gotoSlide: "Zu Slide %s wechseln",
135            },
136            parseLabels(component.dataset.labels)
137        );
138
139        var shouldAutoplay =
140            (component.dataset.autoplay === "1" ||
141                component.dataset.autoplay === "true") &&
142            !PREFERS_REDUCED_MOTION;
143
144        /**
145         * Keeps a single progress indicator attached to the active slide's media.
146         * Sliders without per-slide hosts retain their existing fixed indicator.
147         *
148         * @param {Object} swiper - The Swiper instance.
149         * @returns {void}
150         */
151        function moveProgressToActiveSlide(swiper) {
152            if (!progressIndicator) {
153                return;
154            }
155
156            var activeSlide = swiper.slides[swiper.activeIndex];
157            var progressHost = activeSlide
158                ? activeSlide.querySelector("[data-autoplay-progress-host]")
159                : null;
160
161            if (!progressHost || progressIndicator.parentElement === progressHost) {
162                return;
163            }
164
165            progressHost.appendChild(progressIndicator);
166
167            // The indicator contributes to the active slide's natural height.
168            if (swiper.params.autoHeight) {
169                swiper.updateAutoHeight();
170            }
171        }
172
173        /**
174         * Progress-bar helpers. Each is a no-op when the component has no bar, so a
175         * slider without the indicator (or with autoplay off) needs no extra guards.
176         * The state is attribute-driven; the fill animation lives in the component CSS.
177         */
178        function resetProgress() {
179            if (!progressBar) {
180                return;
181            }
182            progressBar.removeAttribute("data-running");
183            // Force a reflow so re-adding data-running restarts the CSS animation.
184            void progressBar.offsetWidth;
185        }
186        function startProgress() {
187            if (!progressBar) {
188                return;
189            }
190            progressBar.removeAttribute("data-paused");
191            progressBar.setAttribute("data-running", "");
192        }
193        function stopProgress() {
194            if (!progressBar) {
195                return;
196            }
197            progressBar.removeAttribute("data-running");
198            progressBar.removeAttribute("data-paused");
199        }
200        function pauseProgress() {
201            if (progressBar) {
202                progressBar.setAttribute("data-paused", "");
203            }
204        }
205        function resumeProgress() {
206            if (progressBar) {
207                progressBar.removeAttribute("data-paused");
208            }
209        }
210
211        var config = {
212            slidesPerView: 1,
213            spaceBetween: 0,
214            autoHeight: true,
215            speed: 800,
216            navigation: {
217                nextEl: nextButton,
218                prevEl: prevButton,
219            },
220            pagination: {
221                el: pagination,
222                clickable: true,
223                bulletElement: "button",
224                bulletClass: "pagination-dot",
225                bulletActiveClass: "pagination-dot--active",
226                // Rename Swiper's default orientation/modifier classes so the global
227                // swiper.css pagination rules (notably width:100%) don't hijack our
228                // flex layout — the component owns all pagination styling.
229                modifierClass: "pagination--",
230                horizontalClass: "pagination--horizontal",
231                verticalClass: "pagination--vertical",
232                clickableClass: "pagination--clickable",
233                lockClass: "pagination--lock",
234            },
235            a11y: {
236                // Let Swiper own the carousel container semantics. A group is not a
237                // landmark. Prefer the visible Content Block heading; otherwise the
238                // translated/editor-provided text name is applied here.
239                containerRole: "group",
240                containerMessage: sliderLabelledBy
241                    ? null
242                    : component.dataset.sliderLabel,
243                prevSlideMessage: labels.prev,
244                nextSlideMessage: labels.next,
245                paginationBulletMessage: labels.gotoSlide.replace(
246                    "%s",
247                    "{{index}}"
248                ),
249            },
250            on: {
251                init: function () {
252                    moveProgressToActiveSlide(this);
253                },
254                // Reset the fill the moment a new slide starts moving in…
255                slideChangeTransitionStart: function () {
256                    moveProgressToActiveSlide(this);
257                    resetProgress();
258                },
259                // …then restart it once settled, but only while autoplay is running.
260                slideChangeTransitionEnd: function () {
261                    if (this.autoplay && this.autoplay.running) {
262                        startProgress();
263                    }
264                },
265                autoplayStart: function () {
266                    startProgress();
267                    updateAutoplayToggle(autoplayToggle, false, labels);
268                },
269                autoplayStop: function () {
270                    stopProgress();
271                    updateAutoplayToggle(autoplayToggle, true, labels);
272                },
273                autoplayPause: function () {
274                    pauseProgress();
275                },
276                autoplayResume: function () {
277                    resumeProgress();
278                },
279            },
280        };
281
282        // Optional transition effect (e.g. fade), driven by data-effect on the root.
283        if (component.dataset.effect) {
284            config.effect = component.dataset.effect;
285            if (component.dataset.effect === "fade") {
286                config.fadeEffect = { crossFade: true };
287            }
288        }
289
290        if (shouldAutoplay) {
291            config.autoplay = {
292                delay: parseInt(component.dataset.autoplayDuration, 10) || 5000,
293                disableOnInteraction: true,
294                pauseOnMouseEnter: true,
295            };
296            // Hand the delay to CSS so the fill animates over the same duration.
297            if (progressBar) {
298                progressBar.style.setProperty(
299                    "--duration",
300                    config.autoplay.delay + "ms"
301                );
302            }
303        }
304
305        var swiper = new Swiper(swiperElement, config);
306
307        if (!config.autoplay || !autoplayToggle) {
308            return;
309        }
310
311        // Manual play/pause. The autoplayStart/Stop events (above) keep the toggle's
312        // command label and the progress bar in sync, so we only drive Swiper here.
313        autoplayToggle.addEventListener("click", function () {
314            if (swiper.autoplay.running) {
315                swiper.autoplay.stop();
316            } else {
317                swiper.autoplay.start();
318            }
319        });
320
321        // Stop rotation when focus enters the carousel. Moving between its controls must
322        // not undo an explicit "Start autoplay" command.
323        component.addEventListener("focusin", function (event) {
324            if (
325                !component.contains(event.relatedTarget) &&
326                swiper.autoplay.running
327            ) {
328                swiper.autoplay.stop();
329            }
330        });
331    }
332
333    /**
334     * Initialises every slider on the page once Swiper and the DOM are ready.
335     * @returns {void}
336     */
337    function init() {
338        if (typeof Swiper === "undefined") {
339            return;
340        }
341        document.querySelectorAll(COMPONENT_SELECTOR).forEach(initComponent);
342    }
343
344    if (document.readyState === "loading") {
345        document.addEventListener("DOMContentLoaded", init);
346    } else {
347        init();
348    }
349})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.