PageSourceSearch

https://www.zm-online.de/typo3conf/ext/lns_sitepackage/Resources/P…/a11y-lns_gadgets_slider.js?1790159505

js zm-online.de collected 2026-10-01 11:29:35 UTC 16,264 bytes, 475 lines download raw bytes

1(function () {
2  'use strict';
3  let sliderCounter = 0;
4
5  function Slider(el) {
6    // Identify slider and slides container
7    const slider = el;
8    const slides = el.querySelector('ul');
9    if (slides === null)
10      return;
11    if (!slides.classList.contains('slides'))
12      slides.classList.add('slides');
13
14    // Language strings
15    const lang = (document.documentElement.lang || 'de').toLowerCase();
16    const isDE = lang.startsWith('de');
17    const strings = {
18      next: isDE ? 'Nächste Folie' : 'Next',
19      prev: isDE ? 'Vorherige Folie' : 'Previous',
20      item: isDE ? 'Folie' : 'Item',
21      slide: isDE ? 'Folie' : 'Slide'
22    };
23
24    // Options from data-options (no autoplay)
25    const opts = (el.dataset.options || '').toLowerCase();
26    const hasOpt = (name) => opts.indexOf(name) !== -1;
27
28    const thumbs = hasOpt('thumbs') || hasOpt('thumbnails');
29    const bubbles = hasOpt('bubbles') || hasOpt('dots');
30    const arrows = hasOpt('arrows') || hasOpt('directionnav');
31    const direction = el.classList.contains('vertical') ? 'vertical' : 'horizontal';
32    const fade = el.classList.contains('fade');
33
34    // Items
35    const items = Array.from(slides.children);
36    if (!items.length)
37      return;
38
39    // Give this carousel an id if none present (for generating slide ids)
40    if (!slider.id) {
41      sliderCounter += 1;
42      slider.id = 'carousel-' + sliderCounter;
43    }
44
45    // Ensure region semantics recommended (do not overwrite if author set)
46    if (!slider.hasAttribute('role'))
47      slider.setAttribute('role', 'region');
48
49    // Live region for announcements
50    const liveRegion = document.createElement('div');
51    liveRegion.setAttribute('aria-live', 'polite');
52    liveRegion.setAttribute('aria-atomic', 'true');
53    liveRegion.className = 'sr-only';
54    slider.appendChild(liveRegion);
55
56    // State
57    let active = 0;
58    let controlsUL = null;
59    let controlTabs = []; // buttons with role=tab
60
61    // CORRECTION: Also find elements that we previously set to -1
62    function findFocusables(root) {
63      return root.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]');
64    }
65
66    function ensureSlideIdentityAndRoles() {
67      const total = items.length;
68      items.forEach((slide, i) => {
69        setAttributeIfNotExists(slide,'role','group');
70        setAttributeIfNotExists(slide,'aria-roledescription',isDE ? 'Folie' : 'Slide');
71        setAttributeIfNotExists(slide,'tabindex','-1');
72
73        if (!slide.id)
74          slide.id = slider.id + '-slide-' + (i + 1);
75
76        const title = getSlideTitle(slide);
77        let label ='';
78        if (title) {
79          if (isDE) {
80            label = `${title} (${i + 1} von ${total})`;
81          } else {
82            label = `${title} (${i + 1} of ${total})`;
83          }
84        } else {
85          if (isDE) {
86            label = `Folie ${i + 1} von ${total}`;
87          }
88          else {
89            label = `Slide ${i + 1} of ${total}`;
90          }
91        }
92        slide.setAttribute('aria-label', label);
93        if (slide.dataset.backgroundimage) {
94          slide.style.backgroundImage = "url('" + slide.dataset.backgroundimage + "')";
95        }
96      });
97    }
98
99    function setAttributeIfNotExists(el,attribute,value){
100      if(!el.hasAttribute(attribute)) {
101        el.setAttribute(attribute,value);
102      }
103    }
104
105    function getSlideTitle(slide) {
106      let title = slide.dataset.title || '';
107      if (!title) {
108        const heading = slide.querySelector('h1, h2, h3, h4, h5, h6');
109        if (heading)
110          title = heading.textContent.trim();
111      }
112      if (!title) {
113        const img = slide.querySelector('img[alt]');
114        if (img && img.alt)
115          title = img.alt.trim();
116      }
117      return title;
118    }
119
120    function announce(index) {
121      const total = items.length;
122      const slide = items[index];
123      const title = getSlideTitle(slide);
124      let txt = '';
125      if(title){
126        if(isDE){
127          txt = `${strings.slide} ${index + 1} von ${total}: ${title}`;
128        }
129        else {
130          txt = `${strings.slide} ${index + 1} of ${total}: ${title}`;
131        }
132      }
133      else {
134        if(isDE){
135          txt = `${strings.slide} ${index + 1} von ${total}`;
136        }
137        else {
138          txt = `${strings.slide} ${index + 1} of ${total}`;
139        }
140      }
141      liveRegion.textContent = txt;
142    }
143
144    function setSlideState(activeIndex) {
145      items.forEach((slide, i) => {
146        const isActive = i === activeIndex;
147        slide.classList.toggle('active', isActive);
148        slide.setAttribute('aria-hidden', isActive ? 'false' : 'true');
149
150        // Manage inert and focusability inside slides
151        if (!isActive) {
152          slide.setAttribute('inert', '');
153          findFocusables(slide).forEach(el => {
154            if (el.hasAttribute('tabindex') && !el.hasAttribute('data-prev-tabindex')) {
155              el.setAttribute('data-prev-tabindex', el.getAttribute('tabindex'));
156            }
157            el.setAttribute('tabindex', '-1');
158          });
159        } else {
160          slide.removeAttribute('inert');
161          findFocusables(slide).forEach(el => {
162            if (el.hasAttribute('data-prev-tabindex')) {
163              el.setAttribute('tabindex', el.getAttribute('data-prev-tabindex'));
164              el.removeAttribute('data-prev-tabindex');
165            } else if (el.getAttribute('tabindex') === '-1') {
166              el.removeAttribute('tabindex');
167            }
168          });
169
170          const total = items.length;
171          const title = getSlideTitle(slide);
172          let label = '';
173          if(title){
174            if(isDE){
175              label = `${title} (${activeIndex + 1} von ${total})`;
176            }
177            else {
178              label = `${title} (${activeIndex + 1} of ${total})`;
179            }
180          }
181          else {
182            if(isDE){
183              label = `${strings.slide} ${activeIndex + 1} von ${total}`;
184            }
185            else {
186              label = `${strings.slide} ${activeIndex + 1} of ${total}`;
187            }
188          }
189          slide.setAttribute('aria-label', label);
190        }
191      });
192    }
193
194    // CORRECTION: Search extended to include .inner-wrapper
195    function focusFirstInSlide(slide) {
196      const prefer = slide.querySelector('[data-focus], .news-link, #newslink, .inner-wrapper');
197      const first = prefer || slide.querySelector('a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]');
198      if (first)
199        first.focus();
200      else
201        slide.focus();
202    }
203
204    function setActiveIndicator(index) {
205      controlTabs.forEach((btn, i) => {
206        const isActive = i === index;
207        btn.classList.toggle('active', isActive);
208        btn.setAttribute('aria-selected', isActive ? 'true' : 'false');
209        btn.setAttribute('tabindex', isActive ? '0' : '-1');
210      });
211    }
212
213    function scrollToSlide(index, smooth = true) {
214      const target = items[index];
215      if (!target)
216        return;
217      if (fade)
218        return;
219      const behavior = smooth && 'scrollBehavior' in document.documentElement.style ? 'smooth' : 'auto';
220      if (direction === 'vertical') {
221        slides.scrollTo({top: target.offsetTop, behavior});
222      } else {
223        slides.scrollTo({left: target.offsetLeft, behavior});
224      }
225    }
226
227    // CORRECTION: Timing delay for focus, if fade is active
228    function activate(index, options = {smooth: true, focusContent: false}) {
229      if (index >= items.length)
230        index = 0;
231      if (index < 0)
232        index = items.length - 1;
233      active = index;
234      scrollToSlide(active, options.smooth);
235      setSlideState(active);
236      if (controlTabs.length)
237        setActiveIndicator(active);
238      announce(active);
239
240      if (options.focusContent) {
241        // Add a small delay for fade transitions to ensure element is ready to be focused
242        setTimeout(() => {
243          focusFirstInSlide(items[active]);
244        }, 50);
245      }
246    }
247
248    function buildControls() {
249      if (!(bubbles || thumbs)) return;
250      controlsUL = document.createElement('ul');
251      controlsUL.classList.add('controls', thumbs ? 'thumbs' : 'bubbles');
252      controlsUL.setAttribute('role', 'tablist');
253      slider.appendChild(controlsUL);
254
255      items.forEach((slide, i) => {
256        if (!slide.id)
257          slide.id = slider.id + '-slide-' + (i + 1);
258        const li = document.createElement('li');
259
260        const btn = document.createElement('button');
261        btn.type = 'button';
262        btn.className = 'control';
263        btn.setAttribute('role', 'tab');
264        btn.setAttribute('aria-controls', slide.id);
265        btn.setAttribute('aria-selected', i === active ? 'true' : 'false');
266        btn.setAttribute('tabindex', i === active ? '0' : '-1');
267        btn.setAttribute('aria-label', (isDE ? strings.item : strings.item) + ' ' + (i + 1));
268
269        if (thumbs) {
270          const img = slide.querySelector('img');
271          const file = slide.dataset.backgroundimage || (img && (img.src || img.dataset.src));
272          if (file)
273            btn.style.backgroundImage = "url('" + file + "')";
274        } else {
275          btn.textContent = String(i + 1);
276        }
277        btn.addEventListener('click', () => {
278          activate(i, {smooth: true, focusContent: true});
279        });
280
281        btn.addEventListener('keydown', (e) => {
282          if (e.altKey || e.ctrlKey || e.metaKey) return;
283          let currentButtonIndex = i;
284          let nextFocusIndex = null;
285          let shouldActivateSlide = false;
286
287          if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
288            e.preventDefault();
289            nextFocusIndex = (currentButtonIndex - 1 + items.length) % items.length;
290          }
291          else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
292            e.preventDefault();
293            nextFocusIndex = (currentButtonIndex + 1) % items.length;
294          }
295          else if (e.key === 'Home') {
296            e.preventDefault();
297            nextFocusIndex = 0;
298          }
299          else if (e.key === 'End') {
300            e.preventDefault();
301            nextFocusIndex = items.length - 1;
302          }
303          else if (e.key === 'Enter' || e.key === ' ') {
304            e.preventDefault();
305            shouldActivateSlide = true;
306          }
307
308          if (nextFocusIndex !== null) {
309            controlTabs.forEach((tabBtn, index) => {
310              const isNewlyFocused = index === nextFocusIndex;
311              tabBtn.setAttribute('aria-selected', isNewlyFocused ? 'true' : 'false');
312              tabBtn.setAttribute('tabindex', isNewlyFocused ? '0' : '-1');
313            });
314            controlTabs[nextFocusIndex].focus();
315          }
316          if (shouldActivateSlide) {
317            activate(currentButtonIndex, {smooth: true, focusContent: true});
318          }
319        });
320        li.appendChild(btn);
321        controlsUL.appendChild(li);
322      });
323      controlTabs = Array.from(controlsUL.querySelectorAll('button[role="tab"]'));
324      setActiveIndicator(active);
325    }
326
327    function buildArrows() {
328      if (!arrows)
329        return;
330
331      const btnPrev = document.createElement('button');
332      btnPrev.type = 'button';
333      btnPrev.className = 'prev';
334      btnPrev.setAttribute('aria-label', strings.prev);
335      btnPrev.addEventListener('click', () => activate(active - 1, {smooth: true, focusContent: false}));
336      slider.appendChild(btnPrev);
337      const btnNext = document.createElement('button');
338      btnNext.type = 'button';
339      btnNext.className = 'next';
340      btnNext.setAttribute('aria-label', strings.next);
341      btnNext.addEventListener('click', () => activate(active + 1, {smooth: true, focusContent: false}));
342      slider.appendChild(btnNext);
343    }
344
345    function setupGlobalKeydown() {
346      slider.addEventListener('keydown', (e) => {
347        if (e.altKey || e.ctrlKey || e.metaKey)
348          return;
349        const target = e.target;
350        const role = target?.getAttribute('role');
351        if (role === 'tab' || ['INPUT', 'TEXTAREA', 'SELECT', 'A'].includes(target?.tagName?.toUpperCase())) {
352          return;
353        }
354        if (e.key === 'ArrowLeft') {
355          e.preventDefault();
356          activate(active - 1, {smooth: true, focusContent: false});
357        } else if (e.key === 'ArrowRight') {
358          e.preventDefault();
359          activate(active + 1, {smooth: true, focusContent: false});
360        } else if (e.key === 'Home') {
361          e.preventDefault();
362          activate(0, {smooth: true, focusContent: false});
363        } else if (e.key === 'End') {
364          e.preventDefault();
365          activate(items.length - 1, {smooth: true, focusContent: false});
366        }
367      });
368    }
369
370    function setupScrollHandlers() {
371      let scrollTimer = null;
372      function startScroll() {
373        items.forEach(slide => slide.classList.remove('active'));
374        if (controlTabs.length)
375          controlTabs.forEach(btn => btn.classList.remove('active'));
376      }
377      function stopScroll() {
378        clearTimeout(scrollTimer);
379        scrollTimer = null;
380        const center = {
381          y: slides.scrollTop + slides.clientHeight / 2,
382          x: slides.scrollLeft + slides.clientWidth / 2
383        };
384        for (let i = 0; i < items.length; i++) {
385          const it = items[i];
386          const top = it.offsetTop;
387          const left = it.offsetLeft;
388          const bottom = top + it.offsetHeight;
389          const right = left + it.offsetWidth;
390          if (top < center.y && bottom > center.y && left < center.x && right > center.x) {
391            if (active !== i) {
392              active = i;
393              setSlideState(active);
394              if (controlTabs.length)
395                setActiveIndicator(active);
396              announce(active);
397            } else {
398              it.classList.add('active');
399              if (controlTabs.length) setActiveIndicator(active);
400            }
401            break;
402          }
403        }
404      }
405      function onScroll() {
406        if (scrollTimer)
407          clearTimeout(scrollTimer);
408        else
409          startScroll();
410        scrollTimer = setTimeout(stopScroll, 100);
411      }
412      slides.addEventListener('scroll', onScroll);
413
414      // Mouse drag logic
415      let posInitial = 0, posLast = 0, diff = 0;
416      const threshold = 100;
417      function dragStart(event) {
418        diff = 0;
419        event.preventDefault();
420        if (direction === 'horizontal') {
421          posInitial = slides.scrollLeft;
422          posLast = event.clientX;
423        } else {
424          posInitial = slides.scrollTop;
425          posLast = event.clientY;
426        }
427        slides.style.scrollSnapType = 'none';
428        document.addEventListener('mouseup', dragEnd);
429        slides.addEventListener('mousemove', dragAction);
430      }
431      function dragAction(event) {
432        if (direction === 'horizontal') {
433          diff += (posLast - event.clientX);
434          posLast = event.clientX;
435          slides.scroll(posInitial + diff, 0);
436        } else {
437          diff += (posLast - event.clientY);
438          posLast = event.clientY;
439          slides.scroll(0, posInitial + diff);
440        }
441      }
442      function dragEnd() {
443        slides.style.scrollSnapType = '';
444        if (diff < -threshold) {
445          activate(active - 1, {smooth: true, focusContent: false});
446        } else if (diff > threshold) {
447          activate(active + 1, {smooth: true, focusContent: false});
448        } else {
449          if (direction === 'horizontal')
450            slides.scroll(posInitial, 0);
451          else
452            slides.scroll(0, posInitial);
453          setSlideState(active);
454        }
455        document.removeEventListener('mouseup', dragEnd);
456        slides.removeEventListener('mousemove', dragAction);
457      }
458      slides.addEventListener('mousedown', dragStart, true);
459    }
460
461    ensureSlideIdentityAndRoles();
462    buildControls();
463    buildArrows();
464    setupGlobalKeydown();
465    setupScrollHandlers();
466
467    if (fade)
468      items[active].classList.add('active');
469    setSlideState(active);
470    announce(active);
471  }
472
473  document.querySelectorAll('div[data-type="slider"]').forEach(el => new Slider(el));
474  window.Slider = Slider;
475})();

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.