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.