PageSourceSearch

https://www.eta.cz/js/component/carousel.js?vjs=1

js eta.cz collected 2026-09-25 02:47:43 UTC 18,288 bytes, 633 lines download raw bytes

1export class Carousel extends HTMLElement {
2  #shadowRoot;
3  #mouseDownStart = 0;
4  #startScrollPosition = 0;
5  #isMouseDown = false;
6  #moveThreshold = 5;
7  #scrollEndTimer;
8  #isScrolling = false;
9
10  constructor() {
11    super();
12
13    this.#shadowRoot = this.attachShadow({
14      mode: "open"
15    });
16
17    let wrapper = document.createElement("div");
18    wrapper.classList.add("wrapper");
19    wrapper.innerHTML = `
20        <button class="sr-only skip-end">Přeskočit na konec</button>
21        <div class="inner">
22          <button class="prev is-disabled" part="button button-prev" aria-label="Předchozí stránka">
23              <svg aria-hidden="true" part="button-icon" part="button-icon button-icon-left" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
24                <path d="m7.734 1.7344.5303.5303-3.7349 3.7348 3.7349 3.7349-.5304.5303-4.2652-4.2652L7.734 1.7344Z"/>
25              </svg>
26          </button>
27          <button class="next is-disabled" part="button button-next" aria-label="Další stránka">
28              <svg aria-hidden="true" part="button-icon" part="button-icon button-icon-right" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
29                <path d="M7.4692 5.9995 3.7344 2.2647l.5303-.5303L8.53 5.9995l-4.2652 4.2652-.5303-.5303 3.7348-3.7349Z" />
30              </svg>
31          </button>
32          <div class="scroll-wrapper" part="scroll-wrapper" role="region" aria-label="Carousel s produkty">
33              <slot></slot>
34          </div>
35          <div class="overlay" part="overlay"></div>
36        </div>
37        <button class="sr-only skip-start">Přeskočit na začátek</button>
38      `;
39
40    let style = document.createElement("style");
41    style.innerHTML = `
42         * {
43            box-sizing: border-box;
44         }
45        .wrapper {
46            position: relative;
47        }
48        .wrapper .inner {
49            position: relative;
50        }
51        .scroll-wrapper {
52            height: 100%;
53            overflow: auto;
54            display: grid;
55            grid-auto-flow: column;
56            justify-content: start;
57            scrollbar-width: none;
58            position: relative;
59        }
60        .scroll-wrapper::-webkit-scrollbar {
61          display: none;
62        }
63        .scroll-wrapper:focus {
64          outline: 2px solid #FFA02F;
65          outline-offset: 2px;
66        }
67        .sr-only {
68          position: absolute;
69          width: 1px;
70          height: 1px;
71          padding: 0;
72          margin: -1px;
73          overflow: hidden;
74          clip: rect(0, 0, 0, 0);
75          white-space: nowrap;
76          border: 0;
77        }
78        .sr-only:focus {
79          position: static;
80          width: auto;
81          height: auto;
82          padding: 8px 12px;
83          margin: 0;
84          overflow: visible;
85          clip: auto;
86          white-space: normal;
87          background: #333;
88          color: white;
89          border-radius: 4px;
90          font-size: 14px;
91          z-index: 1000;
92        }
93        .skip-start,
94        .skip-end {
95          position: absolute !important;
96          left: 0; 
97        }
98        .skip-start {
99          bottom: 0;
100          top: auto;
101        }
102        .skip-end {
103          top: 0;
104        }
105        .overlay {
106            position: absolute;
107            right: 0;
108            top: 0;
109            bottom: 0;
110            background: linear-gradient(270deg, #FFF 0%, rgba(255, 255, 255, 0.00) 100%);
111            width: 73px;
112            pointer-events: none;
113            z-index: 1;
114        }
115        button {
116            border: none;
117            display: flex;
118            position: absolute;
119            padding: 0;
120            inset-block-start: 50px;
121            inset-inline-start: 8px;
122            z-index: 1;
123            background: #F2F2F2;
124            color: #1E1E1E;
125            border-radius: 32px;
126            width: 32px;
127            height: 32px;
128            transition: color 300ms, background 300ms, opacity: 300ms;
129            cursor: pointer;
130        }
131        button:focus {
132          outline: 2px solid #FFA02F;
133          outline-offset: 2px;
134        }
135        button svg  {
136            display: block;       
137            width: 12px;
138            height: 12px;
139            margin: auto;
140            fill: currentColor; 
141            pointer-events: none;
142        }
143        button + button {
144            inset-inline-start: auto;
145            inset-inline-end: 8px;
146        }
147        button:hover {
148            background: var(--color-branding-orange, #ffa02f);
149            color: #fff;
150        }
151        button.is-disabled {
152            opacity: 0;
153            visibility: hidden;
154        }  
155        .pagination-wrapper {
156          display: flex;
157          justify-content: center;
158          gap: 8px;
159        }
160        
161        .pagination-wrapper a span {
162          display: none;
163        }
164        .pagination-wrapper a::before {
165          content: "";
166          display: block;
167          background: currentColor;
168          width: 10px;
169          height: 10px;
170          border-radius: 50%;
171        }
172        .pagination-wrapper a {
173          display: block;
174          width: 20px;
175          text-decoration: none;
176          height: 20px;
177          border: 1px solid #fff;
178          padding: 4px;
179          border-radius: 50%;
180          color: #E6E6E6;
181          cursor: pointer;
182          user-select: none;
183          background-origin: content-box;
184        }
185        .pagination-wrapper a:hover {
186          color: #FFA02F;
187        }
188        @media (hover: none) {
189          .pagination-wrapper a:hover {
190            color: #E6E6E6;
191          }
192        }
193        .pagination-wrapper a.is-active {
194          border-color: #FFA02F;
195          color: #FFA02F;
196          cursor: default;
197        }    
198        .is-hidden {
199          display: none !important;
200        }
201      `;
202
203    this.#shadowRoot.append(wrapper, style);
204  }
205
206  static register() {
207    if (customElements.get("eta-carousel") !== undefined) {
208      return;
209    }
210
211    customElements.define("eta-carousel", this);
212  }
213
214  #scrollEvent(event) {
215    clearTimeout(this.#scrollEndTimer);
216    this.#scrollEndTimer = setTimeout(() => {
217      this.#toggleButton();
218      this.#updatePagination();
219      this.#isScrolling = false;
220    }, 100);
221  }
222
223  /**
224   * @TODO
225   */
226  resizeEvent(event) {
227    this.#toggleButton();
228  }
229
230  /**
231   * @todo
232   */
233  #toggleButton() {
234    const scroller = this.#shadowRoot.querySelector(".scroll-wrapper");
235
236    let scrollPosition = Math.abs(scroller.scrollLeft);
237    let end = scroller.scrollWidth - scroller.clientWidth;
238
239    this.#shadowRoot
240      .querySelector(".prev")
241      .classList.toggle("is-disabled", scrollPosition <= 0);
242    this.#shadowRoot
243      .querySelector(".next")
244      .classList.toggle("is-disabled", Math.ceil(scrollPosition) >= end);
245
246    this.#shadowRoot.querySelector(".overlay").hidden =
247      Math.ceil(scrollPosition) >= end;
248  }
249
250  /**
251   * @param {boolean} prev
252   */
253  #moveScroller(prev) {
254    const scroller = this.#shadowRoot.querySelector(".scroll-wrapper");
255    let movePosition = scroller.getBoundingClientRect().width;
256    if (this.#isScrolling) {
257      return;
258    }
259    this.#isScrolling = true;
260
261    if (prev) {
262      movePosition = movePosition * -1;
263    }
264
265    scroller.scrollBy({
266      behavior: "smooth",
267      left: movePosition,
268    });
269  }
270
271  #moveScrollerToPosition(position) {
272    const scroller = this.#shadowRoot.querySelector(".scroll-wrapper");
273    if (this.#isScrolling) {
274      return;
275    }
276    this.#isScrolling = true;
277    this.#scrollEvent();
278
279    scroller.scrollTo({
280      behavior: "smooth",
281      left: position,
282    });
283  }
284
285  /**
286   * @param {MouseEvent} event
287   */
288  #buttonClickEvent(event) {
289    this.#moveScroller(event.target.classList.contains("prev"));
290  }
291
292  /**
293   * @param {MouseEvent} event
294   */
295  #mouseDownEvent(event) {
296    if (this.#isMouseDown || this.#isScrolling) {
297      return;
298    }
299    this.#isMouseDown = true;
300    this.#mouseDownStart = event.clientX;
301    this.#startScrollPosition =
302      this.#shadowRoot.querySelector(".scroll-wrapper").scrollLeft;
303    event.preventDefault();
304
305    window.addEventListener("mousemove", this.#mouseMoveEvent.bind(this));
306    window.addEventListener("mouseup", this.#mouseUpEvent.bind(this));
307  }
308
309  #scrollerClickEvent(event) {
310    if (Math.abs(this.#mouseDownStart - event.pageX) >= this.#moveThreshold) {
311      event.stopPropagation();
312      event.preventDefault();
313    }
314  }
315
316  /**
317   * @param {MouseEvent} event
318   */
319  #mouseMoveEvent(event) {
320    if (!this.#isMouseDown) {
321      return;
322    }
323    const deltaX = event.clientX - this.#mouseDownStart;
324    const scroller = this.#shadowRoot.querySelector(".scroll-wrapper");
325    scroller.scrollLeft = this.#startScrollPosition - deltaX; //TODO zvážit použití Carousel.#moveScrollerToPosition
326  }
327
328  /**
329   * @param {MouseEvent} event
330   */
331  #mouseUpEvent(event) {
332    this.#isMouseDown = false;
333    window.removeEventListener("mousemove", this.#mouseMoveEvent.bind(this));
334    window.removeEventListener("mouseup", this.#mouseUpEvent.bind(this));
335  }
336
337  /**
338   * @param {Event} event
339   */
340  #handlePaginationClick(event) {
341    event.preventDefault();
342    const delegateTarget = event.target.closest("[data-pagination-index]");
343    if (delegateTarget) {
344      let index = parseInt(delegateTarget.dataset.paginationIndex);
345      let targetPosition = this.#getPositionOfIndex(index);
346      this.#moveScrollerToPosition(targetPosition);
347    }
348  }
349
350  /**
351   * Obsluha skip tlačítek
352   * @param {Event} event
353   */
354  #skipButtonClickEvent(event) {
355    if (event.target.classList.contains('skip-end')) {
356      // Přeskočíme na skip-start tlačítko na konci carouselu
357      const skipStartButton = this.#shadowRoot.querySelector('.skip-start');
358      if (skipStartButton) {
359        skipStartButton.focus();
360      }
361    } else if (event.target.classList.contains('skip-start')) {
362      // Přeskočíme na skip-end tlačítko na začátku carouselu
363      const skipEndButton = this.#shadowRoot.querySelector('.skip-end');
364      if (skipEndButton) {
365        skipEndButton.focus();
366      }
367    }
368  }
369
370  /**
371   * Najde první viditelný fokusovatelný prvek v carouselu
372   * @return {HTMLElement|null}
373   */
374  #getFirstVisibleFocusableElement() {
375    const scroller = this.#shadowRoot.querySelector(".scroll-wrapper");
376    const scrollLeft = scroller.scrollLeft;
377    const scrollerWidth = scroller.clientWidth;
378
379    const focusableSelectors = 'a:not([tabindex="-1"], button:not([tabindex="-1"], [tabindex]:not([tabindex="-1"])';
380
381    const items = Array.from(this.children).flatMap(child => {
382      if (child.tagName === 'UL' &&
383          getComputedStyle(child).display === 'contents') {
384        return Array.from(child.children);
385      }
386      return child;
387    });
388
389    for (const item of items) {
390      const itemRect = item.getBoundingClientRect();
391      const scrollerRect = scroller.getBoundingClientRect();
392
393      const isVisible = itemRect.right > scrollerRect.left &&
394          itemRect.left < scrollerRect.right;
395
396      if (isVisible) {
397        const focusableElement = item.matches(focusableSelectors) ?
398            item : item.querySelector(focusableSelectors);
399
400        if (focusableElement) {
401          return focusableElement;
402        }
403      }
404    }
405
406    return null;
407  }
408
409  /**
410   * Zaměří první viditelný prvek v carouselu
411   */
412  #focusFirstVisibleElement() {
413    const firstVisible = this.#getFirstVisibleFocusableElement();
414    if (firstVisible) {
415      firstVisible.focus();
416    }
417  }
418
419  /**
420   * Obsluha klávesových zkratek
421   * @param {KeyboardEvent} event
422   */
423  /**
424   * Obsluha klávesových zkratek
425   * @param {KeyboardEvent} event
426   */
427  #keydownEvent(event) {
428    const activeElement = this.#shadowRoot.activeElement || document.activeElement;
429    const prevButton = this.#shadowRoot.querySelector('.prev');
430    const nextButton = this.#shadowRoot.querySelector('.next');
431
432    // Klávesové zkratky fungují pouze když jsou zaměřená navigační tlačítka
433    const isNavigationButtonFocused = activeElement === prevButton || activeElement === nextButton;
434
435    if (!isNavigationButtonFocused) {
436      return;
437    }
438
439    switch (event.key) {
440      case 'Tab':
441        // Při Tab z navigačních tlačítek zaměříme první viditelný prvek
442        if (!event.shiftKey) {
443          event.preventDefault();
444          this.#focusFirstVisibleElement();
445        }
446        break;
447
448      case 'ArrowLeft':
449        event.preventDefault();
450        if (!prevButton.classList.contains('is-disabled')) {
451          this.#moveScroller(true);
452        }
453        break;
454
455      case 'ArrowRight':
456        event.preventDefault();
457        if (!nextButton.classList.contains('is-disabled')) {
458          this.#moveScroller(false);
459        }
460        break;
461
462      case 'Home':
463        event.preventDefault();
464        this.#moveScrollerToPosition(0);
465        break;
466
467      case 'End':
468        event.preventDefault();
469        const scroller = this.#shadowRoot.querySelector(".scroll-wrapper");
470        const maxScroll = scroller.scrollWidth - scroller.clientWidth;
471        this.#moveScrollerToPosition(maxScroll);
472        break;
473    }
474  }
475
476  /**
477   * @param {number} index
478   * @return {number}
479   */
480  #getPositionOfIndex(index) {
481    const scroller = this.#shadowRoot.querySelector(".scroll-wrapper");
482    let pageSize = scroller.clientWidth;
483    return index * pageSize;
484  }
485
486  connectedCallback() {
487    this.#createPagination();
488
489    this.#shadowRoot
490        .querySelector(".skip-end")
491        .addEventListener("click", this.#skipButtonClickEvent.bind(this));
492
493    this.#shadowRoot
494        .querySelector(".skip-start")
495        .addEventListener("click", this.#skipButtonClickEvent.bind(this));
496
497    this.#shadowRoot
498      .querySelector(".pagination-wrapper")
499      ?.addEventListener("click", this.#handlePaginationClick.bind(this));
500
501    if (this.hasAttribute("show-pagination")) {
502      this.#shadowRoot
503        .querySelector(".pagination-wrapper")
504        ?.classList.remove("is-hidden");
505    }
506
507    this.#shadowRoot
508      .querySelector(".scroll-wrapper")
509      .addEventListener("scroll", this.#scrollEvent.bind(this));
510
511    this.#shadowRoot
512      .querySelector(".scroll-wrapper")
513      .addEventListener("click", this.#scrollerClickEvent.bind(this));
514
515    this.#shadowRoot
516      .querySelector(".scroll-wrapper")
517      .addEventListener("mousedown", this.#mouseDownEvent.bind(this));
518
519    // Přidáme posluchač pro klávesové zkratky
520    this.#shadowRoot
521      .addEventListener("keydown", this.#keydownEvent.bind(this));
522
523    this.#shadowRoot
524      .querySelector(".prev")
525      .addEventListener("click", this.#buttonClickEvent.bind(this));
526
527    this.#shadowRoot
528      .querySelector(".next")
529      .addEventListener("click", this.#buttonClickEvent.bind(this));
530
531    this.#toggleButton();
532  }
533
534  disconnectedCallback() {
535    this.#shadowRoot
536        .querySelector(".skip-end")
537        .removeEventListener("click", this.#skipButtonClickEvent.bind(this));
538
539    this.#shadowRoot
540        .querySelector(".skip-start")
541        .removeEventListener("click", this.#skipButtonClickEvent.bind(this));
542
543    this.#shadowRoot
544      .querySelector(".scroll-wrapper")
545      .removeEventListener("scroll", this.#scrollEvent.bind(this));
546
547    this.#shadowRoot
548      .querySelector(".scroll-wrapper")
549      .removeEventListener("mousedown", this.#mouseDownEvent.bind(this));
550
551    this.#shadowRoot
552      .querySelector(".scroll-wrapper")
553      .removeEventListener("keydown", this.#keydownEvent.bind(this));
554
555    this.#shadowRoot
556      .querySelector(".prev")
557      .removeEventListener("click", this.#buttonClickEvent.bind(this));
558
559    this.#shadowRoot
560      .querySelector(".next")
561      .removeEventListener("click", this.#buttonClickEvent.bind(this));
562  }
563
564  #updatePagination() {
565    const scroller = this.#shadowRoot.querySelector(".scroll-wrapper");
566    const itemsPerPage =
567      parseInt(
568        getComputedStyle(scroller).getPropertyValue("--visible-items"),
569      ) || 4;
570    const itemWidth = Math.round(scroller.clientWidth / itemsPerPage);
571    const scrollPosition = scroller.scrollLeft;
572    const maxScroll = scroller.scrollWidth - scroller.clientWidth;
573
574    // Pokud jsme na konci scrollu, vybereme poslední stránku
575    if (Math.ceil(scrollPosition) >= maxScroll) {
576      const totalItems = Array.from(this.children).filter(
577        (child) => child.tagName.toLowerCase() !== "script",
578      ).length;
579      const totalPages = Math.ceil(totalItems / itemsPerPage);
580      this.#setActivePaginationItem(totalPages - 1);
581      return;
582    }
583
584    // Jinak vypočítáme aktuální stránku podle pozice scrollu
585    const currentPageIndex = Math.round(
586      scrollPosition / (itemWidth * itemsPerPage),
587    );
588    this.#setActivePaginationItem(currentPageIndex);
589  }
590
591  /**
592   * @param {number} [activeIndex] - pokud není k dispozici použije se #index
593   */
594  #setActivePaginationItem(activeIndex) {
595    this.#shadowRoot
596      .querySelectorAll(".pagination-wrapper a")
597      .forEach((element, elementIndex) => {
598        element.classList.toggle("is-active", activeIndex === elementIndex);
599      });
600  }
601
602  #createPagination() {
603    const scroller = this.#shadowRoot.querySelector(".scroll-wrapper");
604    const itemCount = Array.from(this.children).filter(
605      (child) => child.tagName.toLowerCase() !== "script",
606    ).length;
607    const itemsPerPage =
608      parseInt(
609        getComputedStyle(scroller).getPropertyValue("--visible-items"),
610      ) || 4;
611    const pagesCount = Math.ceil(itemCount / itemsPerPage);
612    if (pagesCount === 1) {
613      return;
614    }
615
616    let pagination = document.createElement("div");
617    pagination.classList.add("pagination-wrapper");
618    pagination.classList.add("is-hidden");
619    pagination.setAttribute("part", "pagination-wrapper");
620    pagination.setAttribute("aria-hidden", "true");
621
622    let paginationItems = [];
623
624    for (let i = 0; i < pagesCount; i++) {
625      paginationItems.push(
626        `<a href='javascript:' tabindex="-1" data-pagination-index="${i}" class="${i === 0 ? "is-active" : ""}"><span>${i}</span></a>`,
627      );
628    }
629
630    pagination.innerHTML = paginationItems.join("\n");
631    this.#shadowRoot.querySelector(".wrapper").append(pagination);
632  }
633}

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.