PageSourceSearch

https://www.ceskaposta.cz/o/cpost-liferay7-theme/js/component-utils.js?version=2026-09-24-1517

js ceskaposta.cz collected 2026-09-24 15:19:10 UTC 130,799 bytes, 4,151 lines download raw bytes

1function initSimpleTable() {
2  const tables = document.querySelectorAll(".simple-table:not(.no-script)");
3
4  tables.forEach((table) => {
5    try {
6      const thRow = table.querySelector("thead tr") || table.querySelector("tr");
7      const thElements = thRow ? Array.from(thRow.querySelectorAll("th")) : [];
8
9      // Pokud nejsou <th>, označ tabulku chybou
10      if (thElements.length === 0) {
11        table.classList.add("edit-error");
12        return;
13      }
14
15      // Detekce colspan / rowspan v celé tabulce
16      const hasSpan = table.querySelector("th[colspan], th[rowspan], td[colspan], td[rowspan]");
17
18      if (hasSpan) {
19        table.classList.add("edit-error");
20        return;
21      }
22
23      const bodyRows = table.querySelectorAll("tbody tr");
24
25      bodyRows.forEach((row) => {
26        const tds = row.querySelectorAll("td");
27
28        tds.forEach((td, colIndex) => {
29          const th = thElements[colIndex];
30
31          // Pokud neexistuje odpovídající <th>
32          if (!th) {
33            table.classList.add("edit-error");
34            return;
35          }
36
37          const thText = th.textContent ?? "";
38
39          if (td.getAttribute("data-th") !== thText) {
40            td.setAttribute("data-th", thText);
41          }
42        });
43      });
44    } catch (e) {
45      // Absolutní fallback – skript nikdy nespadne
46      table.classList.add("edit-error");
47    }
48  });
49}
50
51function initPriceCard() {
52  document
53    .querySelectorAll(".price-card:not(.no-script)")
54    //  .forEach((item, index) => new PriceCardAccordion(item, index, 834));
55    .forEach((item, index) => new PriceCardAccordion(item, index, 999999)); // 999999 zařídí že vždy bude price card defaultně zabalená (nový požadavek)
56}
57
58function initEditableTwoColumnAccordion() {
59  document
60    .querySelectorAll(".editable-two-column-accordion:not(.no-script)")
61    .forEach((item, index) => new EditableTwoColumnAccordion(item, index, 576)); // 834
62}
63
64function initHomepageBanners() {
65  if (document.getElementById("banner-slider")) {
66    new BannerSlider("banner-slider");
67  }
68}
69// Application menu / slider
70function initAppMenuSlider(breakpoints = [992, 600, 400]) {
71  const classNames = ["is-lg", "is-md", "is-sm"];
72  const root = document.querySelector(".application-menu");
73
74  if (!root) return;
75
76  const menu = root.querySelector("ul");
77  const btnLeft = document.getElementById("application-menu-slider-btn-left");
78  const btnRight = document.getElementById("application-menu-slider-btn-right");
79  let currentIndex = 0;
80
81  if (!menu || !btnLeft || !btnRight) return;
82
83  btnLeft.title = "Předchozí položka aplikačního menu";
84  btnRight.title = "Další položka aplikačního menu";
85
86  function updateClasses() {
87    const width = window.innerWidth;
88    root.classList.remove(...classNames);
89    breakpoints.forEach((bp, i) => {
90      if (width <= bp) root.classList.add(classNames[i]);
91    });
92    updateButtons();
93  }
94
95  function updateTransform() {
96    const firstItem = menu.querySelector("li");
97    if (!firstItem) return;
98
99    const itemWidth = firstItem.getBoundingClientRect().width;
100    menu.style.transform = `translateX(-${currentIndex * itemWidth}px)`;
101    updateButtons();
102  }
103
104  function updateButtons() {
105    if (!root.classList.contains("is-sm")) {
106      btnLeft.style.display = "none";
107      btnRight.style.display = "none";
108      return;
109    }
110    const items = menu.querySelectorAll("li");
111    const maxIndex = items.length - 1;
112    btnLeft.style.display = currentIndex <= 0 ? "none" : "block";
113    btnRight.style.display = currentIndex >= maxIndex ? "none" : "block";
114  }
115
116  btnLeft.addEventListener("click", () => {
117    if (root.classList.contains("is-sm")) {
118      currentIndex = Math.max(0, currentIndex - 1);
119      updateTransform();
120    }
121  });
122
123  btnRight.addEventListener("click", () => {
124    if (root.classList.contains("is-sm")) {
125      const items = menu.querySelectorAll("li");
126      const maxIndex = items.length - 1;
127      currentIndex = Math.min(maxIndex, currentIndex + 1);
128      updateTransform();
129    }
130  });
131
132  // Dotykové ovládání
133  let startX = 0;
134  let moveX = 0;
135
136  menu.addEventListener("touchstart", (e) => {
137    if (!root.classList.contains("is-sm")) return;
138    startX = e.touches[0].clientX;
139  });
140
141  menu.addEventListener("touchmove", (e) => {
142    if (!root.classList.contains("is-sm")) return;
143    moveX = e.touches[0].clientX - startX;
144  });
145
146  menu.addEventListener("touchend", () => {
147    if (!root.classList.contains("is-sm")) return;
148    const threshold = 50;
149    if (moveX < -threshold) btnRight.click();
150    else if (moveX > threshold) btnLeft.click();
151    moveX = 0;
152  });
153
154  // Klávesové ovládání
155  menu.addEventListener("keydown", (e) => {
156    const isNoBreakpoint =
157      !root.classList.contains("is-lg") && !root.classList.contains("is-md") && !root.classList.contains("is-sm");
158
159    if (!isNoBreakpoint) return;
160
161    const items = Array.from(menu.querySelectorAll("li.application-menu-item:not(.disabled)"));
162    let current = items.findIndex((el) => el.classList.contains("selected"));
163    if (current === -1) current = 0;
164
165    if (["ArrowDown", "ArrowUp", "Home", "End"].includes(e.key)) e.preventDefault();
166
167    if (e.key === "ArrowDown") {
168      const next = (current + 1) % items.length;
169      items[current].classList.remove("selected");
170      items[current].setAttribute("aria-selected", "false");
171      items[next].classList.add("selected");
172      items[next].setAttribute("aria-selected", "true");
173      items[next].querySelector("a").focus();
174    } else if (e.key === "ArrowUp") {
175      const prev = (current - 1 + items.length) % items.length;
176      items[current].classList.remove("selected");
177      items[current].setAttribute("aria-selected", "false");
178      items[prev].classList.add("selected");
179      items[prev].setAttribute("aria-selected", "true");
180      items[prev].querySelector("a").focus();
181    } else if (e.key === "Home") {
182      items[current].classList.remove("selected");
183      items[current].setAttribute("aria-selected", "false");
184      items[0].classList.add("selected");
185      items[0].setAttribute("aria-selected", "true");
186      items[0].querySelector("a").focus();
187    } else if (e.key === "End") {
188      items[current].classList.remove("selected");
189      items[current].setAttribute("aria-selected", "false");
190      const last = items[items.length - 1];
191      last.classList.add("selected");
192      last.setAttribute("aria-selected", "true");
193      last.querySelector("a").focus();
194    }
195  });
196
197  window.addEventListener("resize", () => {
198    updateClasses();
199    if (!root.classList.contains("is-sm")) {
200      menu.style.transform = "translateX(0)";
201      currentIndex = 0;
202    }
203    updateButtons();
204  });
205
206  // Inicializace
207  updateClasses();
208  updateButtons();
209
210  requestAnimationFrame(() => {
211    if (document.body.contains(root)) {
212      root.classList.add("is-ready");
213    }
214  });
215}
216/** Info tooltip (otazníček s tooltipem)
217 *  @example <button data-info-tooltip="<strong>HTML tooltip</strong>" data-info-tooltip-html="true" ...
218 *  @example <button data-info-tooltip="HTML tooltip" ...
219 * */
220
221function initInfoTooltip() {
222  // Odstraníme všechny staré tooltipy z DOMu
223  document.querySelectorAll(".info-tooltip").forEach((t) => t.remove());
224
225  // Odstraníme příznaky "už inicializováno"
226  document.querySelectorAll("[data-info-tooltip]").forEach((el) => {
227    delete el.dataset.tooltipInitialized;
228  });
229
230  // Najdeme aktuální tooltipové prvky
231  const tooltipElements = document.querySelectorAll("[data-info-tooltip]");
232  if (!tooltipElements.length) return; // nic k inicializaci
233
234  // Inicializace každého tooltipu
235  tooltipElements.forEach((el, index) => {
236    if (el.dataset.tooltipInitialized === "true") return;
237    el.dataset.tooltipInitialized = "true";
238
239    const tooltip = document.createElement("div");
240    tooltip.className = "info-tooltip";
241    tooltip.dataset.visible = "false";
242    tooltip.setAttribute("role", "tooltip");
243
244    const tooltipId = `info-tooltip-${Date.now()}-${index}`;
245    tooltip.id = tooltipId;
246    document.body.appendChild(tooltip);
247
248    function showTooltip() {
249      const text = el.getAttribute("data-info-tooltip");
250      if (!text) return;
251
252      const allowHtml = el.getAttribute("data-info-tooltip-html") === "true";
253
254      if (allowHtml) {
255        tooltip.innerHTML = text;
256      } else {
257        tooltip.textContent = text;
258      }
259
260      tooltip.dataset.visible = "true";
261      el.setAttribute("aria-describedby", tooltipId);
262      positionTooltip();
263    }
264
265    function hideTooltip() {
266      tooltip.dataset.visible = "false";
267      el.removeAttribute("aria-describedby");
268    }
269
270    function positionTooltip() {
271      const rect = el.getBoundingClientRect();
272      const ttRect = tooltip.getBoundingClientRect();
273      const margin = 8;
274      const position = el.getAttribute("data-info-tooltip-position") || "top";
275      tooltip.dataset.position = position;
276
277      let top = 0,
278        left = 0;
279
280      switch (position) {
281        case "top":
282          top = rect.top + window.scrollY - ttRect.height - margin;
283          left = rect.left + window.scrollX + (rect.width - ttRect.width) / 2;
284          if (top < margin) top = rect.bottom + window.scrollY + margin;
285          break;
286        case "bottom":
287          top = rect.bottom + window.scrollY + margin;
288          left = rect.left + window.scrollX + (rect.width - ttRect.width) / 2;
289          if (top + ttRect.height > window.scrollY + window.innerHeight - margin)
290            top = rect.top + window.scrollY - ttRect.height - margin;
291          break;
292        case "left":
293          top = rect.top + window.scrollY + (rect.height - ttRect.height) / 2;
294          left = rect.left + window.scrollX - ttRect.width - margin;
295          if (left < margin) left = rect.right + window.scrollX + margin;
296          break;
297        case "right":
298          top = rect.top + window.scrollY + (rect.height - ttRect.height) / 2;
299          left = rect.right + window.scrollX + margin;
300          if (left + ttRect.width > window.scrollX + window.innerWidth - margin)
301            left = rect.left + window.scrollX - ttRect.width - margin;
302          break;
303      }
304
305      tooltip.style.top = `${top}px`;
306      tooltip.style.left = `${left}px`;
307    }
308
309    // Myš + klávesnice
310    el.addEventListener("mouseenter", showTooltip);
311    el.addEventListener("mouseleave", hideTooltip);
312    el.addEventListener("focus", showTooltip);
313    el.addEventListener("blur", hideTooltip);
314
315    // Mobilní podpora
316    el.addEventListener("touchstart", (e) => {
317      e.stopPropagation();
318      if (tooltip.dataset.visible === "true") hideTooltip();
319      else showTooltip();
320    });
321
322    // Přepočet při resize/scroll
323    window.addEventListener("resize", () => {
324      if (tooltip.dataset.visible === "true") positionTooltip();
325    });
326
327    window.addEventListener("scroll", () => {
328      if (tooltip.dataset.visible === "true") positionTooltip();
329    });
330
331    //  Skrytí při tapnutí mimo
332    document.addEventListener("touchstart", (e) => {
333      if (!el.contains(e.target) && tooltip.dataset.visible === "true") {
334        hideTooltip();
335      }
336    });
337  });
338}
339
340function sanitizeTooltipHtml(html) {
341  const template = document.createElement("template");
342
343  template.innerHTML = html;
344
345  const allowedTags = ["strong", "b", "em", "i", "br", "a", "span"];
346
347  const allowedAttrs = {
348    a: ["href", "target", "rel"],
349    span: ["class"],
350  };
351
352  function walk(node) {
353    Array.from(node.children).forEach((child) => {
354      const tag = child.tagName.toLowerCase();
355
356      if (!allowedTags.includes(tag)) {
357        child.replaceWith(...child.childNodes);
358
359        return;
360      }
361
362      Array.from(child.attributes).forEach((attr) => {
363        const allowed = allowedAttrs[tag] || [];
364
365        if (!allowed.includes(attr.name)) {
366          child.removeAttribute(attr.name);
367        }
368      });
369
370      walk(child);
371    });
372  }
373
374  walk(template.content);
375
376  return template.innerHTML;
377}
378/**
379 * Homepage banners
380 * @deprecated
381 **/
382
383class BannerSlider {
384  constructor(sliderId, intervalTime = 5000) {
385    this.currentIndex = 0;
386    this.slider = document.getElementById(sliderId);
387    this.slides = this.slider.querySelectorAll(".hp-slide");
388    this.totalSlides = this.slides.length;
389    this.intervalTime = intervalTime;
390    this.sliderInterval = null;
391
392    this.init();
393  }
394
395  init() {
396    this.sliderInterval = setInterval(() => {
397      this.moveSlider();
398    }, this.intervalTime);
399
400    this.slider.addEventListener("mouseenter", () => {
401      this.pause();
402    });
403
404    this.slider.addEventListener("mouseleave", () => {
405      this.play();
406    });
407  }
408
409  moveSlider() {
410    this.currentIndex++;
411    if (this.currentIndex >= this.totalSlides) {
412      this.currentIndex = 0;
413    }
414    this.slider.querySelector(".hp-slider").style.transform = `translateX(-${this.currentIndex * 100}%)`;
415  }
416
417  pause() {
418    clearInterval(this.sliderInterval);
419  }
420
421  play() {
422    this.sliderInterval = setInterval(() => {
423      this.moveSlider();
424    }, this.intervalTime);
425  }
426}
427/**
428 * Třída EditableTwoColumnAccordion implementuje rozbalovací dvousloupcový akordeon (FaQ, Parametry služby)
429 * s podporou přístupnosti (ARIA), klávesového ovládání.
430 */
431class EditableTwoColumnAccordion {
432  /**
433   * @param {HTMLElement} container - Hlavní element akordeonu (s třídou .editable-two-column-accordion)
434   * @param {number} index - Pořadí komponenty na stránce (pro generování unikátních ID)
435   * @param {number} breakpoint - Číslo v px pro šířku obrazovky. Pod touto hranicí bude akordeon zobrazen jako zabalený. Nad touto hranicí bude akordeon zobrazen jako rozbalený.
436   */
437  constructor(container, index, breakpoint) {
438    this.container = container;
439    this.index = index;
440    this.oneColumnVersion = Liferay.Util.getWindowWidth() <= breakpoint;
441    this.buttons = container.querySelectorAll(".editable-two-column-accordion-button");
442    this.container = container.querySelector("div.editable-two-column-accordion-container");
443    this.focusedIndex = 0;
444
445    this.init();
446  }
447
448  init() {
449    this.container.setAttribute("id", "editable-two-column-accordion-" 
449+ this.index);
450
451    if (this.oneColumnVersion) {
452      this.container.style.flexDirection = "column";
453    }
454
455    for (var i = 0; i < this.buttons.length; i++) {
456      this.setupButton(this.buttons[i], i);
457    }
458  }
459
460  setupButton(button, i) {
461    var self = this;
462    var wrapper = button.closest(".editable-two-column-accordion-button-wrapper");
463    var panel = wrapper.nextElementSibling;
464
465    var buttonId = "editable-two-column-accordion-button-" + this.index + "-" + i;
466    var panelId = "editable-two-column-accordion-panel-" + this.index + "-" + i;
467
468    // ARIA a počáteční stav
469    button.setAttribute("id", buttonId);
470    button.setAttribute("aria-controls", panelId);
471    button.setAttribute("aria-expanded", "false");
472    panel.setAttribute("id", panelId);
473    panel.setAttribute("aria-labelledby", buttonId);
474    panel.style.maxHeight = this.expandAll ? panel.scrollHeight + "px" : null;
475
476    // Kliknutí – toggle panelu
477    button.addEventListener("click", function () {
478      self.togglePanel(button, panel);
479    });
480
481    // Klávesové ovládání
482    button.addEventListener("keydown", function (event) {
483      self.handleKeydown(event, i);
484    });
485  }
486
487  togglePanel(button, panel) {
488    var isExpanded = button.getAttribute("aria-expanded") === "true";
489    button.setAttribute("aria-expanded", isExpanded ? "false" : "true");
490    panel.style.maxHeight = isExpanded ? null : panel.scrollHeight + 20 + "px";
491  }
492
493  handleKeydown(event, index) {
494    var key = event.key;
495    var total = this.buttons.length;
496    var lengthOfLeftColumn = Math.ceil(total / 2);
497
498    if (key === "ArrowUp" || key === "Up") {
499      event.preventDefault();
500      this.focusedIndex = (index - 1 + total) % total;
501      this.buttons[this.focusedIndex].focus();
502    }
503
504    if (key === "ArrowDown" || key === "Down") {
505      event.preventDefault();
506      this.focusedIndex = (index + 1) % total;
507      this.buttons[this.focusedIndex].focus();
508    }
509
510    if (key === "ArrowRight" || key === "Right") {
511      event.preventDefault();
512      if (this.oneColumnVersion) return;
513      this.focusedIndex = index + lengthOfLeftColumn > total - 1 ? index : index + lengthOfLeftColumn;
514      this.buttons[this.focusedIndex].focus();
515    }
516
517    if (key === "ArrowLeft" || key === "Left") {
518      event.preventDefault();
519      if (this.oneColumnVersion) return;
520      this.focusedIndex = index - lengthOfLeftColumn < 0 ? index : index - lengthOfLeftColumn;
521      this.buttons[this.focusedIndex].focus();
522    }
523
524    if (key === "Home") {
525      event.preventDefault();
526      this.focusedIndex = 0;
527      this.buttons[0].focus();
528    }
529
530    if (key === "End") {
531      event.preventDefault();
532      this.focusedIndex = total - 1;
533      this.buttons[this.focusedIndex].focus();
534    }
535
536    if (key === " " || key === "Enter") {
537      event.preventDefault();
538      this.buttons[index].click();
539    }
540  }
541}
542/**
543 * Horizontal Carousel – Pointer Events version
544 */
545
546class HorizontalCarousel {
547  constructor(componentId) {
548    this.carousel = document.getElementById(componentId);
549    if (!this.carousel) return;
550
551    this.wrapper = this.carousel.querySelector(".horizontal-carousel-images-wrapper");
552    this.imagesContainer = this.carousel.querySelector(".horizontal-carousel-images");
553    this.buttons = this.carousel.querySelectorAll(".horizontal-carousel-controls button");
554    this.prevBtn = this.buttons[0];
555    this.nextBtn = this.buttons[1];
556
557    this.currentIndex = 0;
558    this.imagePositions = [];
559    this.isTransitioning = false;
560
561    this.originalImages = [];
562    this.allImages = [];
563    this.originalCount = 0;
564
565    // drag state
566    this.isDragging = false;
567    this.pointerId = null;
568    this.startX = 0;
569    this.startTranslate = 0;
570    this.currentTranslate = 0;
571
572    this.listeners = [];
573
574    this.init();
575    this.bindEvents();
576  }
577
578  /* ================= INIT ================= */
579
580  init() {
581    this.imagesContainer.querySelectorAll(".clone-before, .clone-after").forEach((el) => el.remove());
582
583    this.originalImages = Array.from(this.imagesContainer.querySelectorAll("img"));
584    this.originalCount = this.originalImages.length;
585    if (this.originalCount === 0) return;
586
587    const clonesBefore = this.originalImages.map((img) => {
588      const clone = img.cloneNode(true);
589      clone.classList.add("clone-before");
590      return clone;
591    });
592
593    clonesBefore.reverse().forEach((clone) => {
594      this.imagesContainer.insertBefore(clone, this.imagesContainer.firstChild);
595    });
596
597    const clonesAfter = this.originalImages.map((img) => {
598      const clone = img.cloneNode(true);
599      clone.classList.add("clone-after");
600      return clone;
601    });
602
603    clonesAfter.forEach((clone) => {
604      this.imagesContainer.appendChild(clone);
605    });
606
607    this.allImages = Array.from(this.imagesContainer.querySelectorAll("img"));
608
609    Promise.all(
610      this.allImages.map((img) => {
611        if (img.complete) return Promise.resolve();
612        return new Promise((resolve) => {
613          img.onload = resolve;
614          img.onerror = resolve;
615        });
616      }),
617    ).then(() => {
618      this.calculatePositions();
619      this.currentIndex = this.originalCount;
620      this.setPosition(this.currentIndex, false);
621    });
622  }
623
624  /* ================= CORE ================= */
625
626  calculatePositions() {
627    this.imagePositions = [];
628    let cumulative = 0;
629    const gap = 16;
630
631    this.allImages.forEach((img) => {
632      this.imagePositions.push(cumulative);
633      const width = img.offsetWidth || img.naturalWidth || 532;
634      cumulative += width + gap;
635    });
636  }
637
638  setPosition(index, animate = true) {
639    this.imagesContainer.classList.toggle("no-transition", !animate);
640    this.imagesContainer.style.transform = `translateX(${-this.imagePositions[index]}px)`;
641  }
642
643  movePrev() {
644    if (this.isTransitioning) return;
645    this.isTransitioning = true;
646
647    this.currentIndex--;
648    this.setPosition(this.currentIndex, true);
649
650    setTimeout(() => {
651      if (this.currentIndex < this.originalCount) {
652        this.currentIndex += this.originalCount;
653        this.setPosition(this.currentIndex, false);
654      }
655      this.isTransitioning = false;
656    }, 400);
657  }
658
659  moveNext() {
660    if (this.isTransitioning) return;
661    this.isTransitioning = true;
662
663    this.currentIndex++;
664    this.setPosition(this.currentIndex, true);
665
666    setTimeout(() => {
667      if (this.currentIndex >= this.originalCount * 2) {
668        this.currentIndex -= this.originalCount;
669        this.setPosition(this.currentIndex, false);
670      }
671      this.isTransitioning = false;
672    }, 400);
673  }
674
675  /* ================= EVENTS ================= */
676
677  addListener(target, type, handler, options) {
678    target.addEventListener(type, handler, options);
679    this.listeners.push({ target, type, handler, options });
680  }
681
682  bindEvents() {
683    /* ===== POINTER EVENTS ===== */
684
685    this.addListener(this.wrapper, "pointerdown", (e) => {
686      if (this.isTransitioning || e.button !== 0) return;
687
688      this.isDragging = true;
689      this.pointerId = e.pointerId;
690      this.startX = e.pageX;
691
692      const matrix = new DOMMatrixReadOnly(getComputedStyle(this.imagesContainer).transform);
693      this.startTranslate = matrix.m41;
694
695      this.wrapper.setPointerCapture(this.pointerId);
696      this.wrapper.style.cursor = "grabbing";
697      this.imagesContainer.classList.add("no-transition");
698    });
699
700    this.addListener(this.wrapper, "pointermove", (e) => {
701      if (!this.isDragging || e.pointerId !== this.pointerId) return;
702
703      const diff = e.pageX - this.startX;
704      this.currentTranslate = this.startTranslate + diff;
705      this.imagesContainer.style.transform = `translateX(${this.currentTranslate}px)`;
706    });
707
708    this.addListener(this.wrapper, "pointerup", (e) => {
709      if (e.pointerId === this.pointerId) {
710        this.finishPointerDrag(e.pageX);
711      }
712    });
713
714    this.addListener(this.wrapper, "pointercancel", () => {
715      this.cancelPointerDrag();
716    });
717
718    /* ===== BUTTONS ===== */
719
720    this.addListener(this.prevBtn, "click", () => this.movePrev());
721    this.addListener(this.nextBtn, "click", () => this.moveNext());
722
723    /* ===== RESIZE ===== */
724
725    this.addListener(window, "resize", () => {
726      this.calculatePositions();
727      this.setPosition(this.currentIndex, false);
728    });
729  }
730
731  finishPointerDrag(endX) {
732    if (!this.isDragging) return;
733
734    this.wrapper.releasePointerCapture(this.pointerId);
735    this.isDragging = false;
736    this.pointerId = null;
737    this.wrapper.style.cursor = "grab";
738
739    const diff = endX - this.startX;
740    const threshold = 50;
741
742    if (diff > threshold) {
743      this.movePrev();
744    } else if (diff < -threshold) {
745      this.moveNext();
746    } else {
747      this.setPosition(this.currentIndex, true);
748    }
749  }
750
751  cancelPointerDrag() {
752    if (!this.isDragging) return;
753
754    this.wrapper.releasePointerCapture(this.pointerId);
755    this.isDragging = false;
756    this.pointerId = null;
757    this.wrapper.style.cursor = "grab";
758    this.setPosition(this.currentIndex, true);
759  }
760
761  /* ================= CLEANUP ================= */
762
763  destroy() {
764    this.listeners.forEach(({ target, type, handler, options }) => {
765      target.removeEventListener(type, handler, options);
766    });
767    this.listeners = [];
768  }
769}
770/**
771 * Modal
772 *
773 * @class Modal
774 *
775 * @param {string} modalId
776 *   ID DOM elementu reprezentujícího modal.
777 *
778 * @param {Object} [options]
779 *   Konfigurační objekt.
780 *
781 * @param {Function|null} [options.onClose]
782 *   Callback volaný při pokusu o zavření modalu.
783 *   Podpis: (event: Event | null, reason: string) => boolean | void
784 *   Pokud vrátí false, zavření modalu bude zrušeno.
785 *   Reason může být:
786 *   - "close"    – klik na close button
787 *   - "accept"   – potvrzovací akce
788 *   - "backdrop" – klik na backdrop
789 *   - "escape"   – klávesa Escape
790 *
791 * @param {Function|null} [options.onAccept]
792 *   Callback volaný při akci accept.
793 *   Podpis: (event: Event) => boolean | void
794 *   Pokud vrátí false, modal zůstane otevřený.
795 *
796 * @param {boolean} [options.backdropClose=true]
797 *   Určuje, zda klik na backdrop může zavřít modal.
798 *
799 * @param {boolean} [options.autofocus=false]
800 *   Pokud je true, při otevření modalu se automaticky focusne
801 *   vhodný focusable prvek (křížek se ignoruje).
802 */
803
804class Modal {
805  constructor(modalId, options = {}) {
806    this.modalId = modalId;
807    this.modal = document.getElementById(modalId);
808
809    if (!this.modal) {
810      throw new Error(`Modal s id "${modalId}" nebyl nalezen`);
811    }
812
813    this.options = {
814      onClose: options.onClose || null,
815      onAccept: options.onAccept || null,
816      backdropClose: options.backdropClose !== false,
817      autofocus: options.autofocus === true,
818    };
819
820    this.wrapper = null;
821    this.backdrop = null;
822    this._previouslyFocusedElement = null;
823    this._scrollY = null;
824
825    // bind handlers
826    this._onKeyDown = this._onKeyDown.bind(this);
827    this._onActionClick = this._onActionClick.bind(this);
828    this._onBackdropClick = this._onBackdropClick.bind(this);
829    this._onBeforeNavigate = this._onBeforeNavigate.bind(this);
830
831    this.originalParent = this.modal.parentElement;
832    this.originalNextSibling = this.modal.nextSibling;
833
834    this._init();
835    this._bindActions();
836
837    // SPA lifecycle hook
838    Liferay.on("beforeNavigate", this._onBeforeNavigate);
839  }
840
841  // SPA LIFECYCLE
842
843  _onBeforeNavigate() {
844    this.destroy();
845  }
846
847  // INIT
848
849  _init() {
850    if (this.wrapper) return;
851
852    const wrapper = document.createElement("div");
853    wrapper.className = "modal-generic-wrapper";
854    wrapper.setAttribute("role", "presentation");
855
856    Object.assign(wrapper.style, {
857      position: "fixed",
858      inset: "0",
859      zIndex: "9999",
860      display: "none",
861      alignItems: "center",
862      justifyContent: "center",
863    });
864
865    const backdrop = document.createElement("div");
866    backdrop.className = "modal-generic-backdrop";
867    Object.assign(backdrop.style, {
868      position: "absolute",
869      inset: "0",
870    });
871
872    if (this.options.backdropClose) {
873      backdrop.addEventListener("click", this._onBackdropClick);
874    }
875
876    wrapper.appendChild(backdrop);
877    wrapper.appendChild(this.modal);
878    document.body.appendChild(wrapper);
879
880    this.wrapper = wrapper;
881    this.backdrop = backdrop;
882  }
883
884  _bindActions() {
885    this.modal.addEventListener("click", this._onActionClick);
886  }
887
888  // ACTION HANDLING
889
890  _onActionClick(e) {
891    const actionEl = e.target.closest("[data-modal-action]");
892    if (!actionEl) return;
893
894    const action = actionEl.dataset.modalAction;
895
896    if (action === "close") {
897      this._requestClose(e, "close");
898    }
899
900    if (action === "accept") {
901      let result = true;
902
903      if (typeof this.options.onAccept === "function") {
904        result = this.options.onAccept(e);
905      }
906
907      if (result !== false) {
908        this._requestClose(e, "accept");
909      }
910    }
911  }
912
913  // CLOSE FLOW
914
915  _requestClose(event, reason) {
916    if (typeof this.options.onClose === "function") {
917      const result = this.options.onClose(event, reason);
918      if (result === false) return;
919    }
920
921    this._close();
922  }
923
924  _close() {
925    if (!this.wrapper) return;
926
927    if (this.modal.contains(document.activeElement)) {
928      document.activeElement.blur();
929    }
930
931    this.wrapper.style.display = "none";
932
933    this.modal.style.display = "none";
934    this.modal.setAttribute("aria-hidden", "true");
935    this.modal.removeAttribute("aria-modal");
936
937    document.removeEventListener("keydown", this._onKeyDown);
938
939    this._unlockBodyScroll();
940
941    if (this._previouslyFocusedElement) {
942      this._previouslyFocusedElement.focus();
943      this._previouslyFocusedElement = null;
944    }
945  }
946
947  // BODY SCROLL
948
949  _lockBodyScroll() {
950    this._scrollY = window.scrollY;
951
952    const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
953
954    document.body.style.position = "fixed";
955    document.body.style.top = `-${this._scrollY}px`;
956    document.body.style.left = "0";
957    document.body.style.right = "0";
958
959    if (scrollbarWidth > 0) {
960      document.body.style.paddingRight = `${scrollbarWidth}px`;
961    }
962  }
963
964  _unlockBodyScroll() {
965    document.body.style.position = "";
966    document.body.style.top = "";
967    document.body.style.left = "";
968    document.body.style.right = "";
969    document.body.style.paddingRight = "";
970
971    if (this._scrollY !== null) {
972      window.scrollTo(0, this._scrollY);
973      this._scrollY = null;
974    }
975  }
976
977  // PUBLIC API
978  open() {
979    this._previouslyFocusedElement = document.activeElement;
980
981    this._lockBodyScroll();
982
983    this.wrapper.style.display = "flex";
984
985    this.modal.style.display = "";
986    this.modal.removeAttribute("aria-hidden");
987    this.modal.setAttribute("aria-modal", "true");
988
989    document.addEventListener("keydown", this._onKeyDown);
990
991    if (this.options.autofocus) {
992      requestAnimationFrame(() => {
993        const target = this._getAutofocusTarget();
994        if (target) target.focus();
995      });
996    }
997  }
998
999  destroy() {
1000    if (!this.wrapper) return;
1001
1002    this._close();
1003
1004    // odregistrování SPA hooku
1005    Liferay.off("beforeNavigate", this._onBeforeNavigate);
1006
1007    this.modal.removeEventListener("click", this._onActionClick);
1008
1009    if (this.backdrop && this.options.backdropClose) {
1010      this.backdrop.removeEventListener("click", this._onBackdropClick);
1011    }
1012
1013    if (this.originalNextSibling) {
1014      this.originalParent.insertBefore(this.modal, this.originalNextSibling);
1015    } else {
1016      this.originalParent.appendChild(this.modal);
1017    }
1018
1019    this.wrapper.remove();
1020    this.wrapper = null;
1021    this.backdrop = null;
1022  }
1023
1024  // EVENTS
1025
1026  _onKeyDown(e) {
1027    if (e.key === "Escape") {
1028      this._requestClose(e, "escape");
1029      return;
1030    }
1031
1032    if (e.key === "Tab") {
1033      this._handleFocusTrap(e);
1034    }
1035  }
1036
1037  _onBackdropClick() {
1038    this._requestClose(null, "backdrop");
1039  }
1040
1041  // FOCUS TRAP
1042
1043  _getFocusableElements(root = this.modal) {
1044    return Array.from(
1045      root.querySelectorAll(
1046        `a[href], button:not([disabled]),
1047         textarea:not([disabled]), input:not([disabled]),
1048         select:not([disabled]),
1049         [tabindex]:not([tabindex="-1"])`,
1050      ),
1051    ).filter((el) => el.offsetParent !== null);
1052  }
1053
1054  _getAutofocusTarget() {
1055    const content = this.modal.querySelector(".modal-generic-content");
1056    if (content) {
1057      const focusables = this._getFocusableElements(content);
1058      if (focusables.length) return focusables[0];
1059    }
1060
1061    return this._getFocusableElements(this.modal).find((el) => !el.classList.contains("modal-generic-close-button"));
1062  }
1063
1064  _handleFocusTrap(e) {
1065    const focusable = this._getFocusableElements();
1066    if (!focusable.length) return;
1067
1068    const first = focusable[0];
1069    const last = focusable[focusable.length - 1];
1070
1071    if (e.shiftKey && document.activeElement === first) {
1072      e.preventDefault();
1073      last.focus();
1074    } else if (!e.shiftKey && document.activeElement === last) {
1075      e.preventDefault();
1076      first.focus();
1077    }
1078  }
1079}
1080/**
1081 * Třída PriceAccordion implementuje rozbalovací seznam cen (akordeon)
1082 * s podporou přístupnosti (ARIA), klávesového ovládání a přechodů.
1083 */
1084class PriceCardAccordion {
1085  /**
1086   * @param {HTMLElement} container - Hlavní element akordeonu (s třídou .price-card)
1087   * @param {number} index - Pořadí komponenty na stránce (pro generování unikátních ID)
1088   * @param {number} breakpoint - Číslo v px pro šířku obrazovky. Pod touto hranicí bude akordeon zobrazen jako zabalený. Nad touto hranicí bude akordeon zobrazen jako rozbalený.
1089   */
1090  constructor(container, index, breakpoint) {
1091    this.container = container;
1092    this.index = index;
1093    this.isExpandedDefaultValue = Liferay.Util.getWindowWidth() > breakpoint; // zjistí, zda je šířka okna větší než breakpoint a dle toho nastaví výchozí hodnotu pro rozbalení
1094    this.buttons = container.querySelectorAll(".price-card-button");
1095    this.focusedIndex = 0;
1096
1097    this.init();
1098  }
1099
1100  init() {
1101    this.container.setAttribute("id", "price-list-" + this.index);
1102
1103    if (this.container.querySelector(".price-card-description")) {
1104      this.container.classList.add("price-list-with-description");
1105    }
1106
1107    for (var i = 0; i < this.buttons.length; i++) {
1108      this.setupButton(this.buttons[i], i);
1109    }
1110  }
1111
1112  setupButton(button, i) {
1113    var self = this;
1114    var wrapper = button.closest(".price-card-button-wrapper");
1115    var panel = wrapper.nextElementSibling;
1116
1117    var buttonId = "price-list-button-" + this.index + "-" + i;
1118    var panelId = "price-list-panel-" + this.index + "-" + i;
1119
1120    // ARIA a počáteční stav
1121    button.setAttribute("id", buttonId);
1122    button.setAttribute("aria-controls", panelId);
1123    button.setAttribute("aria-expanded", String(self.isExpandedDefaultValue));
1124    panel.setAttribute("id", panelId);
1125    panel.setAttribute("aria-labelledby", buttonId);
1126    panel.style.maxHeight = self.isExpandedDefaultValue ? panel.scrollHeight + "px" : null;
1127
1128    // Kliknutí – toggle panelu
1129    button.addEventListener("click", function () {
1130      self.togglePanel(button, panel);
1131    });
1132
1133    // Klávesové ovládání
1134    button.addEventListener("keydown", function (event) {
1135      self.handleKeydown(event, i);
1136    });
1137  }
1138
1139  togglePanel(button, panel) {
1140    var isExpanded = button.getAttribute("aria-expanded") === "true";
1141    button.setAttribute("aria-expanded", isExpanded ? "false" : "true");
1142    panel.style.maxHeight = isExpanded ? null : panel.scrollHeight + "px";
1143  }
1144
1145  handleKeydown(event, index) {
1146    var key = event.key;
1147    var total = this.buttons.length;
1148
1149    if (key === "ArrowUp" || key === "Up") {
1150      event.preventDefault();
1151      this.focusedIndex = (index - 1 + total) % total;
1152      this.buttons[this.focusedIndex].focus();
1153    }
1154
1155    if (key === "ArrowDown" || key === "Down") {
1156      event.preventDefault();
1157      this.focusedIndex = (index + 1) % total;
1158      this.buttons[this.focusedIndex].focus();
1159    }
1160
1161    if (key === "Home") {
1162      event.preventDefault();
1163      this.focusedIndex = 0;
1164      this.buttons[0].focus();
1165    }
1166
1167    if (key === "End") {
1168      event.preventDefault();
1169      this.focusedIndex = total - 1;
1170      this.buttons[this.focusedIndex].focus();
1171    }
1172
1173    if (key === " " || key === "Enter") {
1174      event.preventDefault();
1175      this.buttons[index].click();
1176    }
1177  }
1178}
1179function basketRecalculate() {
1180  const rows = document.querySelectorAll("tr");
1181
1182  let basketTotal = 0;
1183
1184  rows.forEach((row) => {
1185    const qtyInput = row.querySelector('.quantity-selector input[type="number"]');
1186    const unitPriceCell = row.querySelector("[data-unit-price]");
1187    const lineTotalCell = row.querySelector(".line-total");
1188
1189    if (!qtyInput || !unitPriceCell || !lineTotalCell) {
1190      return;
1191    }
1192
1193    const quantity = Number(qtyInput.value);
1194    const unitPrice = Number(unitPriceCell.dataset.unitPrice);
1195
1196    const lineTotal = quantity * unitPrice;
1197    basketTotal += lineTotal;
1198
1199    lineTotalCell.innerText = formatPrice(lineTotal);
1200  });
1201
1202  const totalEl = document.getElementById("basket-total-price");
1203  if (totalEl) {
1204    totalEl.innerText = formatPrice(basketTotal);
1205  }
1206}
1207
1208// TODO globalní funkce pro formátování ceny, aby se všude používal stejný formát
1209function formatPrice(value) {
1210  return (
1211    value.toLocaleString("cs-CZ", {
1212      minimumFractionDigits: 2,
1213      maximumFractionDigits: 2,
1214    }) + " Kč"
1215  );
1216}
1217
1218// Utility funkce pro získání elementů
1219function quantitySelectorGetInput(element) {
1220  // Pokud je element tlačítko, najdi input ve stejném containeru
1221  if (element.tagName === "BUTTON") {
1222    return element.parentElement.querySelector('input[type="number"]');
1223  }
1224  // Pokud je element input, vrať ho
1225  return element;
1226}
1227
1228function quantitySelectorGetDecreaseButton(input) {
1229  return input.parentElement.querySelector("button:first-of-type");
1230}
1231
1232function quantitySelectorGetIncreaseButton(input) {
1233  return input.parentElement.querySelector("button:last-of-type");
1234}
1235
1236// Získání min/max z atributů
1237function quantitySelectorGetMin(input) {
1238  const min = input.getAttribute("min");
1239  return min !== null ? Number(min) : 1;
1240}
1241
1242function quantitySelectorGetMax(input) {
1243  const max = input.getAttribute("max");
1244  return max !== null ? Number(max) : Infinity;
1245}
1246
1247function quantitySelectorGetValue(input) {
1248  return Number(input.value);
1249}
1250
1251// Validace hodnoty
1252function quantitySelectorIsValid(input, value) {
1253  const num = Number(value);
1254  const min = quantitySelectorGetMin(input);
1255  const max = quantitySelectorGetMax(input);
1256
1257  return value !== "" && !Number.isNaN(num) && num >= min && num <= max && Number.isInteger(num);
1258}
1259
1260// Nastavení hodnoty s omezením na rozsah
1261function quantitySelectorSetValue(input, value) {
1262  const min = quantitySelectorGetMin(input);
1263  const max = quantitySelectorGetMax(input);
1264  const clamped = Math.max(min, Math.min(max, value));
1265
1266  input.value = clamped;
1267  quantitySelectorUpdateAriaAttributes(input);
1268  quantitySelectorUpdateButtonStates(input);
1269  input.classList.remove("invalid");
1270}
1271
1272// Aktualizace ARIA atributů
1273function quantitySelectorUpdateAriaAttributes(input) {
1274  const value = quantitySelectorGetValue(input);
1275  const min = quantitySelectorGetMin(input);
1276  const max = quantitySelectorGetMax(input);
1277
1278  input.setAttribute("aria-valuenow", value);
1279  input.setAttribute("aria-valuemin", min);
1280
1281  if (max !== Infinity) {
1282    input.setAttribute("aria-valuemax", max);
1283  } else {
1284    input.removeAttribute("aria-valuemax");
1285  }
1286}
1287
1288// Aktualizace stavu tlačítek (disabled)
1289function quantitySelectorUpdateButtonStates(input) {
1290  const value = quantitySelectorGetValue(input);
1291  const min = quantitySelectorGetMin(input);
1292  const max = quantitySelectorGetMax(input);
1293
1294  const decreaseBtn = quantitySelectorGetDecreaseButton(input);
1295  const increaseBtn = quantitySelectorGetIncreaseButton(input);
1296
1297  decreaseBtn.disabled = value <= min;
1298  increaseBtn.disabled = value >= max;
1299
1300  decreaseBtn.setAttribute("aria-disabled", value <= min);
1301  increaseBtn.setAttribute("aria-disabled", value >= max);
1302}
1303
1304// Inicializace jednoho inputu
1305function quantitySelectorInit(input) {
1306  input.setAttribute("novalidate", "");
1307
1308  // Pokud input nemá hodnotu nebo je nevalidní, nastav min (nebo 1)
1309  if (input.value === "" || Number.isNaN(Number(input.value))) {
1310    const min = quantitySelectorGetMin(input);
1311    input.value = min;
1312  }
1313
1314  quantitySelectorUpdateAriaAttributes(input);
1315  quantitySelectorUpdateButtonStates(input);
1316}
1317
1318// Event handler funkce
1319function quantitySelectorHandleIncrease(button) {
1320  const input = quantitySelectorGetInput(button);
1321  const value = quantitySelectorGetValue(input);
1322  const max = quantitySelectorGetMax(input);
1323
1324  if (value < max) {
1325    quantitySelectorSetValue(input, value + 1);
1326    basketRecalculate();
1327  }
1328}
1329
1330function quantitySelectorHandleDecrease(button) {
1331  const input = quantitySelectorGetInput(button);
1332  const value = quantitySelectorGetValue(input);
1333  const min = quantitySelectorGetMin(input);
1334
1335  if (value > min) {
1336    quantitySelectorSetValue(input, value - 1);
1337    basketRecalculate();
1338  }
1339}
1340
1341function quantitySelectorHandleInput(input) {
1342  // Během psaní pouze vizuálně označíme nevalidní hodnotu
1343  if (input.value !== "" && !quantitySelectorIsValid(input, input.value)) {
1344    input.classList.add("invalid");
1345  } else {
1346    input.classList.remove("invalid");
1347  }
1348
1349  // Aktualizace ARIA atributů a tlačítek (pokud je hodnota validní)
1350  if (quantitySelectorIsValid(input, input.value)) {
1351    quantitySelectorUpdateAriaAttributes(input);
1352    quantitySelectorUpdateButtonStates(input);
1353    basketRecalculate();
1354  }
1355}
1356
1357function quantitySelectorHandleBlur(input) {
1358  // Po opuštění inputu validujeme a případně resetujeme na min
1359  if (!quantitySelectorIsValid(input, input.value)) {
1360    const min = quantitySelectorGetMin(input);
1361    quantitySelectorSetValue(input, min);
1362    basketRecalculate();
1363  } else {
1364    // Zajistíme, že hodnota je v rozsahu
1365    quantitySelectorSetValue(input, quantitySelectorGetValue(input));
1366    basketRecalculate();
1367  }
1368}
1369
1370function quantitySelectorHandleKeydown(event, input) {
1371  const min = quantitySelectorGetMin(input);
1372  const max = quantitySelectorGetMax(input);
1373
1374  if (event.key === "ArrowUp") {
1375    event.preventDefault();
1376    quantitySelectorHandleIncrease(input);
1377  } else if (event.key === "ArrowDown") {
1378    event.preventDefault();
1379    quantitySelectorHandleDecrease(input);
1380  } else if (event.key === "Home") {
1381    event.preventDefault();
1382    quantitySelectorSetValue(input, min);
1383  } else if (event.key === "End" && max !== Infinity) {
1384    event.preventDefault();
1385    quantitySelectorSetValue(input, max);
1386  }
1387}
1388/*
1389// Inicializace všech inputů při načtení stránky
1390document.addEventListener("DOMContentLoaded", () => {
1391  document.querySelectorAll('.quantity-selector input[type="number"]').forEach((input) => {
1392    quantitySelectorInit(input);
1393  });
1394});
1395*/
1396class AddressAutocompleteInput {
1397  constructor(componentId, config = {}) {
1398    this.root = document.getElementById(componentId);
1399    if (!this.root) {
1400      throw new Error(`AddressAutocompleteInput: component "${componentId}" not found`);
1401    }
1402
1403    this.input = this.root.querySelector("input");
1404    this.label = this.root.querySelector("label");
1405    this.iconWrapper = this.root.querySelector(".input-icon");
1406
1407    if (!this.input) {
1408      throw new Error("AddressAutocompleteInput: input not found");
1409    }
1410    this.name = this.input.getAttribute("name") || "address-autocomplete";
1411    this.input.setAttribute("autocomplete", "off");
1412
1413    this.state = {
1414      loading: false,
1415      disabled: false,
1416      readOnly: false,
1417      value: "",
1418      options: [],
1419      message: "",
1420      error: "",
1421      debounceInternal: 0,
1422      ...config,
1423    };
1424
1425    this.onChange = config.onChange || null;
1426    this.onSelect = config.onSelect || null;
1427
1428    this.wrapper = null;
1429    this.list = null;
1430    this.messageEl = null;
1431    this.debounceTimer = null;
1432    this.activeIndex = -1;
1433
1434    this.hasFocus = false;
1435    this.isProgrammaticFocus = false;
1436
1437    this._setupAria();
1438    this._bind();
1439    this._renderAll();
1440  }
1441
1442  /* ================= PUBLIC API ================= */
1443
1444  set(key, value) {
1445    this.state[key] = value ?? "";
1446
1447    switch (key) {
1448      case "loading":
1449        this._renderLoading();
1450        break;
1451      case "disabled":
1452        this._renderDisabled();
1453        break;
1454      case "readOnly":
1455        this._renderReadOnly();
1456        break;
1457      case "value": {
1458        if (!this.state.value) {
1459          this.input.value = "";
1460          break;
1461        }
1462        const option = this.state.options.find((o) => o.value === this.state.value);
1463        if (option) {
1464          this.input.value = option.label;
1465        } else {
1466          console.warn(`AddressAutocompleteInput[${this.name}]: value "${this.state.value}" not found in options`);
1467        }
1468        break;
1469      }
1470      case "options":
1471        if (this.hasFocus) this._renderOptions();
1472        break;
1473      case "message":
1474        /* message přebíjí error */
1475        if (this.state.message) this.state.error = "";
1476        this._renderMessageOrError();
1477        break;
1478      case "error":
1479        this._renderMessageOrError();
1480        break;
1481    }
1482  }
1483
1484  get(key) {
1485    return this.state[key] ?? null;
1486  }
1487
1488  focus() {
1489    if (this.input.disabled) return;
1490
1491    this.isProgrammaticFocus = true;
1492    requestAnimationFrame(() => {
1493      this.input.focus();
1494      this.isProgrammaticFocus = false;
1495    });
1496  }
1497
1498  destroy() {
1499    this.input.removeEventListener("input", this._onInput);
1500    this.input.removeEventListener("keydown", this._onKeyDown);
1501    this.input.removeEventListener("focus", this._onFocus);
1502    this.input.removeEventListener("blur", this._onBlur);
1503
1504    this._closeOptions();
1505    if (this.messageEl) this.messageEl.remove();
1506    this.iconWrapper.innerHTML = "";
1507  }
1508
1509  /* ================= INIT ================= */
1510
1511  _bind() {
1512    this._onInput = this._handleInput.bind(this);
1513    this._onKeyDown = this._handleKeyDown.bind(this);
1514    this._onFocus = this._handleFocus.bind(this);
1515    this._onBlur = this._handleBlur.bind(this);
1516
1517    this.input.addEventListener("input", this._onInput);
1518    this.input.addEventListener("keydown", this._onKeyDown);
1519    this.input.addEventListener("focus", this._onFocus);
1520    this.input.addEventListener("blur", this._onBlur);
1521  }
1522
1523  _setupAria() {
1524    this.input.setAttribute("role", "combobox");
1525    this.input.setAttribute("aria-autocomplete", "list");
1526    this.input.setAttribute("aria-expanded", "false");
1527    this.input.setAttribute("aria-invalid", "false");
1528  }
1529
1530  _renderAll() {
1531    this._renderDisabled();
1532    this._renderReadOnly();
1533    this._renderLoading();
1534    this._renderMessageOrError();
1535    this.set("value", this.state.value);
1536  }
1537
1538  /* ================= FOCUS ================= */
1539  _handleFocus() {
1540    this.hasFocus = true;
1541    // Nezobrazovat options pokud je input readonly
1542    if (this.state.readOnly) return;
1543    if (this.state.options.length) this._renderOptions();
1544  }
1545
1546  _handleBlur() {
1547    if (this.isProgrammaticFocus) return;
1548    this.hasFocus = false;
1549    setTimeout(() => this._closeOptions(), 150);
1550  }
1551
1552  /* ================= INPUT ================= */
1553  _handleInput(e) {
1554    const value = e.target.value;
1555
1556    // Nezobrazovat options pokud je input readonly
1557    if (this.hasFocus && this.state.options.length && !this.state.readOnly) {
1558      this._renderOptions();
1559    }
1560
1561    if (!this.onChange) return;
1562
1563    if (this.state.debounceInternal > 0) {
1564      clearTimeout(this.debounceTimer);
1565      this.debounceTimer = setTimeout(() => {
1566        this.onChange(value, null, "change");
1567      }, this.state.debounceInternal);
1568    } else {
1569      this.onChange(value, null, "change");
1570    }
1571  }
1572
1573  /* ================= OPTIONS ================= */
1574  _renderOptions() {
1575    this._closeOptions();
1576
1577    // Přidána kontrola readOnly stavu
1578    if (!this.hasFocus || !this.state.options.length || this.state.readOnly) return;
1579
1580    this.wrapper = document.createElement("div");
1581    this.wrapper.className = "address-autocomplete-wrapper";
1582
1583    this.list = document.createElement("ul");
1584    this.list.id = `autocomplete-options-${this.name}`;
1585    this.list.setAttribute("role", "listbox");
1586
1587    this.state.options.forEach((option, index) => {
1588      const li = document.createElement("li");
1589      li.id = `autocomplete-${this.name}-option-${index}`;
1590      li.textContent = option.label;
1591      li.dataset.value = option.value;
1592      li.setAttribute("role", "option");
1593      li.setAttribute("aria-selected", "false");
1594      li.addEventListener("mousedown", (e) => e.preventDefault());
1595      li.addEventListener("click", () => this._selectOption(index));
1596      this.list.appendChild(li);
1597    });
1598
1599    this.wrapper.appendChild(this.list);
1600    this.root.appendChild(this.wrapper);
1601    this.input.setAttribute("aria-expanded", "true");
1602  }
1603
1604  _selectOption(index) {
1605    const option = this.state.options[index];
1606    this.input.value = option.label;
1607    this.state.value = option.value;
1608
1609    if (this.onSelect) this.onSelect(option.value, option.label);
1610    if (this.onChange) this.onChange(option.value, option.label, "select");
1611
1612    this._closeOptions();
1613  }
1614
1615  _closeOptions() {
1616    if (this.wrapper) {
1617      this.wrapper.remove();
1618      this.wrapper = null;
1619      this.list = null;
1620    }
1621
1622    this.activeIndex = -1;
1623    this.input.setAttribute("aria-expanded", "false");
1624    this.input.removeAttribute("aria-activedescendant");
1625  }
1626
1627  /* ================= KEYBOARD ================= */
1628
1629  _handleKeyDown(e) {
1630    if (!this.list) return;
1631
1632    const items = [...this.list.children];
1633    if (!items.length) return;
1634
1635    if (e.key === "ArrowDown") {
1636      e.preventDefault();
1637      this.activeIndex = this.activeIndex < items.length - 1 ? this.activeIndex + 1 : 0;
1638    }
1639
1640    if (e.key === "ArrowUp") {
1641      e.preventDefault();
1642      this.activeIndex = this.activeIndex > 0 ? this.activeIndex - 1 : items.length - 1;
1643    }
1644
1645    if (e.key === " " && this.activeIndex >= 0) {
1646      e.preventDefault();
1647      this._selectOption(this.activeIndex);
1648      return;
1649    }
1650
1651    items.forEach((li, index) => {
1652      const selected = index === this.activeIndex;
1653      li.classList.toggle("selected", selected);
1654      li.setAttribute("aria-selected", selected.toString());
1655
1656      if (selected) {
1657        this.input.setAttribute("aria-activedescendant", li.id);
1658      }
1659    });
1660  }
1661
1662  /* ================= STATES ================= */
1663
1664  _renderLoading() {
1665    this.iconWrapper.innerHTML = this.state.loading
1666      ? `<svg width="15" height="15" viewBox="0 0 15 15" fill="none"
1667          xmlns="http://www.w3.org/2000/svg">
1668          <path
1669            d="M7.46672 0.799988V3.46665
1670               M7.46672 11.4667V14.1333
1671               M2.75338 2.75332L4.64005 4.63999
1672               M10.2934 10.2933L12.18 12.18
1673               M0.800049 7.46665H3.46672
1674               M11.4667 7.46665H14.1334
1675               M2.75338 12.18L4.64005 10.2933
1676               M10.2934 4.63999L12.18 2.75332"
1677            stroke="#888D9F"
1678            stroke-width="1.6"
1679            stroke-linecap="round"
1680            stroke-linejoin="round"/>
1681        </svg>`
1682      : "";
1683  }
1684
1685  _renderDisabled() {
1686    this.input.disabled = !!this.state.disabled;
1687    this.label?.classList.toggle("disabled", !!this.state.disabled);
1688  }
1689
1690  _renderReadOnly() {
1691    this.input.readOnly = !!this.state.readOnly;
1692  }
1693
1694  /* ================= MESSAGE / ERROR (sjednoceno) ================= */
1695
1696  _renderMessageOrError() {
1697    if (this.messageEl) {
1698      this.messageEl.remove();
1699      this.messageEl = null;
1700    }
1701
1702    const text = this.state.error || this.state.message;
1703    const isError = !!this.state.error;
1704
1705    this.root.classList.toggle("has-error", isError);
1706    this.input.setAttribute("aria-invalid", isError.toString());
1707
1708    if (!text) {
1709      this.input.removeAttribute("aria-describedby");
1710      return;
1711    }
1712
1713    this.messageEl = document.createElement("p");
1714    this.messageEl.id = `error-${this.name}`;
1715    this.messageEl.className = "form-text";
1716    this.messageEl.textContent = text;
1717
1718    this.input.setAttribute("aria-describedby", this.messageEl.id);
1719    this.root.appendChild(this.messageEl);
1720  }
1721}
1722class HomepageBannerSlider {
1723  constructor(containerId, options = {}) {
1724    this.container = document.getElementById(containerId);
1725    if (!this.container) return;
1726
1727    this.interval = options.interval ?? 5000;
1728    this.slides = Array.from(this.container.querySelectorAll(".homepage-banner-slide"));
1729    this.dotsWrap = this.container.querySelector(".homepage-banner-slider-dots");
1730    this.total = this.slides.length;
1731    this.current = Math.floor(Math.random() * this.total);
1732    this.timer = null;
1733    this.dots = [];
1734
1735    // Proměnné pro trackování skutečné pauzy
1736    this.remainingTime = this.interval;
1737    this.startTime = 0;
1738
1739    // Uložení referencí na funkce pro správné odstranění v destroy()
1740    this._handlePause = this.pause.bind(this);
1741    this._handleResume = this.resume.bind(this);
1742
1743    this._buildDots();
1744    this._setupSlideAria(); // Nová metoda pro přístupnost slidů
1745
1746    this._loadSlide(this.current);
1747    this._loadSlide((this.current + 1) % this.total);
1748    this.slides[this.current].classList.add("is-active");
1749
1750    // Aktivace počáteční tečky
1751    this.dots[this.current].classList.add("is-active");
1752    this.dots[this.current].setAttribute("aria-current", "true");
1753
1754    this._startAuto(this.interval);
1755    this._setupAccessibility();
1756  }
1757
1758  // Vylepšení sémantiky pro čtečky obrazovky
1759  _setupSlideAria() {
1760    this.slides.forEach((slide, index) => {
1761      slide.setAttribute("role", "group");
1762      slide.setAttribute("aria-roledescription", "slide");
1763      slide.setAttribute("aria-label", `Banner ${index + 1} z ${this.total}`);
1764    });
1765  }
1766
1767  // Nastavení pauzy při najetí myší nebo focusu klávesnicí
1768  _setupAccessibility() {
1769    this.container.addEventListener("mouseenter", this._handlePause);
1770    this.container.addEventListener("mouseleave", this._handleResume);
1771    this.container.addEventListener("focusin", this._handlePause);
1772    this.container.addEventListener("focusout", this._handleResume);
1773  }
1774
1775  _loadSlide(idx) {
1776    const slide = this.slides[idx];
1777    if (!slide || slide.dataset.loaded) return;
1778    slide.dataset.loaded = "true";
1779
1780    const sources = Array.from(slide.querySelectorAll("source"));
1781
1782    sources.forEach((s) => {
1783      if (s.dataset.srcset) s.srcset = s.dataset.srcset;
1784    });
1785
1786    const img = slide.querySelector("img");
1787
1788    if (img?.dataset.src) {
1789      img.src = img.dataset.src;
1790    }
1791
1792    // 👉 přidat toto
1793    if (img) {
1794      const matchedSource = sources.find((s) => {
1795        const media = s.media;
1796        return !media || window.matchMedia(media).matches;
1797      });
1798
1799      const alt = matchedSource?.dataset.alt;
1800      if (alt) {
1801        img.alt = alt;
1802      }
1803    }
1804  }
1805
1806  _buildDots() {
1807    this.dots = this.slides.map((_, i) => {
1808      const d = document.createElement("button");
1809      d.className = "homepage-banner-slider-dot";
1810
1811      const img = this.slides[i].querySelector("img");
1812      const imgAlt = img?.alt?.trim();
1813      const label = imgAlt ? `Zobraz banner ${imgAlt}` : `Zobraz banner č.${i + 1}`;
1814      d.setAttribute("aria-label", label);
1815
1816      d.addEventListener("click", () => this.goTo(i));
1817      this.dotsWrap.appendChild(d);
1818      return d;
1819    });
1820  }
1821
1822  _startAuto(duration) {
1823    clearTimeout(this.timer);
1824    this.startTime = Date.now();
1825    this.timer = setTimeout(() => this.next(), duration);
1826  }
1827
1828  goTo(idx) {
1829    // Úklid předchozího slidu
1830    this.slides[this.current].classList.remove("is-active");
1831    this.dots[this.current].classList.remove("is-active");
1832    this.dots[this.current].removeAttribute("aria-current");
1833
1834    this.current = (idx + this.total) % this.total;
1835
1836    // Načtení potřebných slidů
1837    this._loadSlide((this.current - 1 + this.total) % this.total);
1838    this._loadSlide(this.current);
1839    this._loadSlide((this.current + 1) % this.total);
1840
1841    // Nastavení nového slidu
1842    this.slides[this.current].classList.add("is-active");
1843    this.dots[this.current].classList.add("is-active");
1844    this.dots[this.current].setAttribute("aria-current", "true");
1845
1846    this.remainingTime = this.interval;
1847    this._startAuto(this.interval);
1848  }
1849
1850  next() {
1851    this.goTo(this.current + 1);
1852  }
1853
1854  prev() {
1855    this.goTo(this.current - 1);
1856  }
1857
1858  pause() {
1859    clearTimeout(this.timer);
1860    this.remainingTime -= Date.now() - this.startTime;
1861  }
1862
1863  resume() {
1864    if (this.remainingTime <= 0) {
1865      this.next();
1866      return;
1867    }
1868
1869    this._startAuto(this.remainingTime);
1870  }
1871
1872  destroy() {
1873    clearTimeout(this.timer);
1874
1875    if (this.container) {
1876      this.container.removeEventListener("mouseenter", this._handlePause);
1877      this.container.removeEventListener("mouseleave", this._handleResume);
1878      this.container.removeEventListener("focusin", this._handlePause);
1879      this.container.removeEventListener("focusout", this._handleResume);
1880    }
1881
1882    this.dots.forEach((d) => d.remove());
1883  }
1884}
1885/**
1886 * DatePicker
1887 * Použití:
1888 *   const dp = new DatePicker('#muj-kontejner', {
1889 *     locale: 'cs',
1890 *     disabled: true,
1891 *     minDate: new Date(2020, 0, 1),
1892 *     maxDate: new Date(2030, 11, 31),
1893 *     disabledDates: [new Date(2025, 5, 15)],
1894 *     disabledDayIndexes: [0, 6],          // 0 = neděle, 6 = sobota
1895 *     value: new Date(),
1896 *     onDateChange: (date) => console.log(date),
1897 *     onError: (reason) => console.warn(reason),
1898 *   });
1899 *
1900 *   dp.getValue()      // → Date | null
1901 *   dp.destroy()       // odstraní vše (listenery, DOM)
1902 *
1903 * Liferay SPA cleanup:
1904 *   Třída se automaticky zaregistruje na Liferay.on('beforeNavigate', ...).
1905 *   Pokud Liferay není dostupný, cleanup je třeba volat ručně přes dp.destroy().
1906 */
1907
1908class DatePicker {
1909  // ─── Konstanty ────────────────────────────────────────────────────────────────
1910
1911  static DAY_LABELS_EN = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
1912  static DAY_LABELS_CS = ["pondělí", "úterý", "středa", "čtvrtek", "pátek", "sobota", "neděle"];
1913
1914  static MONTH_LABELS_EN = [
1915    "January",
1916    "February",
1917    "March",
1918    "April",
1919    "May",
1920    "June",
1921    "July",
1922    "August",
1923    "September",
1924    "October",
1925    "November",
1926    "December",
1927  ];
1928  static MONTH_LABELS_CS = [
1929    "leden",
1930    "únor",
1931    "březen",
1932    "duben",
1933    "květen",
1934    "červen",
1935    "červenec",
1936    "srpen",
1937    "září",
1938    "říjen",
1939    "listopad",
1940    "prosinec",
1941  ];
1942
1943  static WEEK_DAYS_EN = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
1944  static WEEK_DAYS_CS = ["Po", "Út", "St", "Čt", "Pá", "So", "Ne"];
1945
1946  static LOCALE_TEXTS = {
1947    en: {
1948      input: {
1949        icon: { default: "Choose date", selected: "Change date" },
1950        placeholder: "D. M. YYYY",
1951        error: {
1952          invalidFormat: "Invalid date format",
1953          beforeMinDate: "Date must not be earlier than {date}",
1954          afterMaxDate: "Date must not be later than {date}",
1955          disabledDate: "This date is not allowed",
1956          disabledDay: "This day is not allowed",
1957          required: "This field is required",
1958        },
1959      },
1960      dialog: {
1961        title: "Choose Date",
1962        header: {
1963          button: {
1964            previousYear: "Previous year",
1965            previousMonth: "Previous month",
1966            nextMonth: "Next month",
1967            nextYear: "Next year",
1968          },
1969        },
1970        footer: { button: { clear: "Clear date", close: "Close" } },
1971        message: {
1972          navigation: {
1973            days: "Cursor keys can navigate dates",
1974            other: "You can navigate between elements using Tab and Shift + Tab.",
1975          },
1976        },
1977      },
1978    },
1979    cs: {
1980      input: {
1981        icon: { default: "Vyberte datum", selected: "Změnit datum" },
1982        placeholder: "D. M. RRRR",
1983        error: {
1984          invalidFormat: "Nesprávný formát data",
1985          beforeMinDate: "Datum nesmí být dříve než {date}",
1986          afterMaxDate: "Datum nesmí být později než {date}",
1987          disabledDate: "Toto datum není povoleno",
1988          disabledDay: "Tento den není povolen",
1989          required: "Toto pole je povinné",
1990        },
1991      },
1992      dialog: {
1993        title: "Vyberte datum",
1994        header: {
1995          button: {
1996            previousYear: "Předchozí rok",
1997            previousMonth: "Předchozí měsíc",
1998            nextMonth: "Následující měsíc",
1999            nextYear: "Následující rok",
2000          },
2001        },
2002        footer: {
2003          button: { clear: "Vymazat datum", close: "Zavřít okno" },
2004        },
2005        message: {
2006          navigation: {
2007            days: "Kurzorovými klávesami lze přecházet mezi daty.",
2008            other: "Mezi prvky lze přecházet pomocí Tab a Shift + Tab",
2009          },
2010        },
2011      },
2012    },
2013  };
2014
2015  // ─── SVG ikony ────────────────────────────────────────────────────────────────
2016
2017  static ICONS = {
2018    chevronLeft:
2019      '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewB
2019ox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><polyline points="15 18 9 12 15 6"/></svg>',
2020    chevronRight:
2021      '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><polyline points="9 18 15 12 9 6"/></svg>',
2022    doubleLeft:
2023      '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><polyline points="11 17 6 12 11 7"/><polyline points="18 17 13 12 18 7"/></svg>',
2024    doubleRight:
2025      '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><polyline points="13 17 18 12 13 7"/><polyline points="6 17 11 12 6 7"/></svg>',
2026    trash:
2027      '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>',
2028  };
2029
2030  // ─── Pomocné funkce ───────────────────────────────────────────────────────────
2031
2032  static normalizeDate(d) {
2033    return new Date(d.getFullYear(), d.getMonth(), d.getDate());
2034  }
2035
2036  static formatDate(date) {
2037    return date.getDate() + ". " + (date.getMonth() + 1) + ". " + date.getFullYear();
2038  }
2039
2040  static parseDateText(value) {
2041    if (!value) return null;
2042    var regex = /^(\d{1,2})\.\s*(\d{1,2})\.\s*(\d{4})$/;
2043    var match = value.match(regex);
2044    if (!match) return null;
2045
2046    var day = parseInt(match[1], 10);
2047    var month = parseInt(match[2], 10) - 1;
2048    var year = parseInt(match[3], 10);
2049    var date = new Date(year, month, day);
2050
2051    // Striktní kontrola přetečení (např. 31. únor nechceme povolit jako březen)
2052    if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) {
2053      return null;
2054    }
2055    return date;
2056  }
2057
2058  static pad2(n) {
2059    return String(n).padStart(2, "0");
2060  }
2061
2062  static isSameDay(a, b) {
2063    return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
2064  }
2065
2066  static isBeforeDate(a, b) {
2067    return DatePicker.normalizeDate(a).getTime() < DatePicker.normalizeDate(b).getTime();
2068  }
2069
2070  static isAfterDate(a, b) {
2071    return DatePicker.normalizeDate(a).getTime() > DatePicker.normalizeDate(b).getTime();
2072  }
2073
2074  static isDateInArray(date, dates) {
2075    if (!dates || !dates.length) return false;
2076    var t = DatePicker.normalizeDate(date).getTime();
2077    return dates.some(function (d) {
2078      return DatePicker.normalizeDate(d).getTime() === t;
2079    });
2080  }
2081
2082  static isDayInDisabledIndexes(date, indexes) {
2083    if (!indexes || !indexes.length) return false;
2084    return indexes.indexOf(date.getDay()) !== -1;
2085  }
2086
2087  static getMonthEnd(date) {
2088    return new Date(date.getFullYear(), date.getMonth() + 1, 0);
2089  }
2090
2091  static getMonthStart(date) {
2092    return new Date(date.getFullYear(), date.getMonth(), 1);
2093  }
2094
2095  static shiftMonth(date, delta, pinnedDay) {
2096    var target = new Date(date.getFullYear(), date.getMonth() + delta, 1);
2097    var maxDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
2098    var day = Math.min(pinnedDay != null ? pinnedDay : date.getDate(), maxDay);
2099    return new Date(target.getFullYear(), target.getMonth(), day);
2100  }
2101
2102  static buildGrid(year, month) {
2103    var firstDay = new Date(year, month, 1).getDay();
2104    var offset = (firstDay + 6) % 7;
2105    var start = new Date(year, month, 1 - offset);
2106    var grid = [];
2107    for (var i = 0; i < 42; i++) {
2108      var d = new Date(start);
2109      d.setDate(start.getDate() + i);
2110      grid.push(d);
2111    }
2112    return grid;
2113  }
2114
2115  static isPrevDisabled(focusDay, minDate, delta) {
2116    if (delta === undefined) delta = -1;
2117    if (!minDate) return false;
2118    return (
2119      DatePicker.getMonthEnd(new Date(focusDay.getFullYear(), focusDay.getMonth() + delta, 1)) <
2120      DatePicker.normalizeDate(minDate)
2121    );
2122  }
2123
2124  static isNextDisabled(focusDay, maxDate, delta) {
2125    if (delta === undefined) delta = 1;
2126    if (!maxDate) return false;
2127    return (
2128      DatePicker.getMonthStart(new Date(focusDay.getFullYear(), focusDay.getMonth() + delta, 1)) >
2129      DatePicker.normalizeDate(maxDate)
2130    );
2131  }
2132
2133  static isDayDisabled(date, opts) {
2134    if (opts.minDate && date < opts.minDate) return true;
2135    if (opts.maxDate && date > opts.maxDate) return true;
2136    if (DatePicker.isDayInDisabledIndexes(date, opts.disabledDayIndexes)) return true;
2137    if (DatePicker.isDateInArray(date, opts.disabledDates)) return true;
2138    return false;
2139  }
2140
2141  static findClosestEnabledDate(candidate, opts) {
2142    if (!DatePicker.isDayDisabled(candidate, opts)) return candidate;
2143    var month = candidate.getMonth(),
2144      year = candidate.getFullYear();
2145    for (var i = 1; i < 31; i++) {
2146      var fwd = new Date(year, month, candidate.getDate() + i);
2147      if (fwd.getMonth() === month && !DatePicker.isDayDisabled(fwd, opts)) return fwd;
2148      var bwd = new Date(year, month, candidate.getDate() - i);
2149      if (bwd.getMonth() === month && !DatePicker.isDayDisabled(bwd, opts)) return bwd;
2150    }
2151    return candidate;
2152  }
2153
2154  static getDefaultFocusedDate(today, minDate, maxDate) {
2155    if (minDate && DatePicker.isAfterDate(minDate, today) && (!maxDate || DatePicker.isBeforeDate(minDate, maxDate)))
2156      return minDate;
2157    if (maxDate && DatePicker.isBeforeDate(maxDate, today)) return maxDate;
2158    return today;
2159  }
2160
2161  static validateDisabledDayIndexes(indexes) {
2162    if (!indexes) return true;
2163    var unique = [];
2164    indexes.forEach(function (d) {
2165      if (unique.indexOf(d) === -1) unique.push(d);
2166    });
2167    for (var i = 0; i < unique.length; i++) {
2168      if (!Number.isInteger(unique[i]) || unique[i] < 0 || unique[i] > 6) return false;
2169    }
2170    if (unique.length > 6) {
2171      console.warn("DatePicker: disabledDayIndexes nesmí obsahovat všech 7 dní. Nastavení bude ignorováno.");
2172      return false;
2173    }
2174    return true;
2175  }
2176
2177  static applyInput(raw, key) {
2178    var dots = (raw.match(/\./g) || []).length;
2179    if (key === "." || key === " ") {
2180      if (!raw || /[\s.]$/.test(raw) || dots >= 2) return raw;
2181      return raw + ". ";
2182    }
2183    if (!/^\d$/.test(key)) return raw;
2184    if (dots === 0) {
2185      if (raw.length === 0) return parseInt(key, 10) > 3 ? key + ". " : key;
2186      if (raw.length === 1) return raw + key + ". ";
2187      return raw;
2188    }
2189    if (dots === 1) {
2190      var month = raw.split(". ")[1] || "";
2191      if (month.length === 0) return parseInt(key, 10) > 1 ? raw + key + ". " : raw + key;
2192      if (month.length === 1) return raw + key + ". ";
2193      return raw;
2194    }
2195    if (dots === 2) {
2196      var year = raw.split(". ")[2] || "";
2197      if (year.length < 4) return raw + key;
2198      return raw;
2199    }
2200    return raw;
2201  }
2202
2203  static applyBackspace(raw) {
2204    if (!raw) return raw;
2205    if (raw.endsWith(". ")) return raw.slice(0, -2);
2206    return raw.slice(0, -1);
2207  }
2208
2209  // ─── DatePicker třída ─────────────────────────────────────────────────────────
2210
2211  /**
2212   * @param {string|Element} id of input CSS selektor nebo DOM element
2213   * @param {Object}         options
2214   */
2215
2216  // ─── Konstruktor a metody ───────────────────────────────────────────────────
2217
2218  constructor(input, options) {
2219    options = options || {};
2220    this.textInput = typeof input === "string" ? document.querySelector(input) : input;
2221
2222    if (!this.textInput) {
2223      throw new Error("DatePicker: input nenalezen – zkontrolujte selektor nebo předaný element.");
2224    }
2225
2226    const container = this.textInput?.closest(".form-group") ?? null;
2227    this.container = container;
2228    if (!this.container) {
2229      throw new Error("DatePicker: container `.form-group` nenalezen");
2230    }
2231
2232    const label = this.container?.querySelector("label") ?? null;
2233    this.label = label;
2234
2235    const inputWrapper = this.textInput?.closest(".input-wrapper") ?? null;
2236    this.inputWrapper = inputWrapper;
2237    if (!this.inputWrapper) {
2238      throw new Error("DatePicker: `.input-wrapper` nenalezen");
2239    }
2240
2241    const button = this.container?.querySelector(".input-icon button") ?? null;
2242    this.toggleBtn = button;
2243    if (!this.toggleBtn) {
2244      throw new Error("DatePicker: button pro otevření vyběru nenalezen");
2245    }
2246
2247    const helperTextElement = this.container?.querySelector(".form-text") ?? null;
2248    this.helperTextElement = helperTextElement;
2249
2250    var locale = options.locale || "cs";
2251    var rawMin = options.minDate;
2252    var rawMax = options.maxDate;
2253
2254    this.locale = locale;
2255    this.minDate = rawMin ? DatePicker.normalizeDate(rawMin) : DatePicker.normalizeDate(new Date(1900, 0, 1));
2256    this.maxDate = rawMax ? DatePicker.normalizeDate(rawMax) : DatePicker.normalizeDate(new Date(2100, 11, 31));
2257    this.disabledDates = options.disabledDates || [];
2258    this.disabledDayIndexes = DatePicker.validateDisabledDayIndexes(options.disabledDayIndexes)
2259      ? options.disabledDayIndexes || []
2260      : [];
2261    this.onDateChange = options.onDateChange || null;
2262    this.onError = options.onError || null;
2263
2264    this.disabled = !!options.disabled;
2265    this.propError = !!options.error;
2266    this.helperText = options.message || "";
2267    this.id = options.id || "dp-" + Math.random().toString(36).slice(2, 9);
2268
2269    this.showInternalError = !!options.showInternalError;
2270
2271    this.today = DatePicker.normalizeDate(new Date());
2272    this._defaultFocusedDate = DatePicker.getDefaultFocusedDate(this.today, this.minDate, this.maxDate);
2273
2274    this.required = !!options.required;
2275
2276    // ── Stav ──
2277    this._isOpen = false;
2278    this._textValue = options.value ? DatePicker.formatDate(DatePicker.normalizeDate(options.value)) : "";
2279    this._focusDay = this._defaultFocusedDate;
2280    this._selectedDay = options.value ? DatePicker.normalizeDate(options.value) : null;
2281    this._pinnedDay = null;
2282    this._message = "";
2283    this._internalError = false;
2284
2285    // Sledování listenerů pro cleanup
2286    this._inputListeners = []; // na input části (trvalé)
2287    this._dialogListeners = []; // na dialog části (dočasné)
2288    this._docListeners = []; // na document.body (dočasné, click-outside)
2289    this._liferayHandler = null;
2290
2291    this._buildInputDOM();
2292    this._bindInputEvents();
2293    this._bindLiferay();
2294  }
2295
2296  // ─── DOM – input ─────────────────────────────────────────────────────────────
2297
2298  _el(tag, attrs) {
2299    var el = document.createElement(tag);
2300    attrs = attrs || {};
2301    Object.keys(attrs).forEach(function (k) {
2302      if (k === "class") {
2303        el.className = attrs[k];
2304      } else if (k === "style") {
2305        el.style.cssText = attrs[k];
2306      } else {
2307        el.setAttribute(k, attrs[k]);
2308      }
2309    });
2310    return el;
2311  }
2312  _buildInputDOM() {
2313    var t = DatePicker.LOCALE_TEXTS[this.locale];
2314    this.toggleBtn.setAttribute("aria-label", this._textValue ? t.input.icon.selected : t.input.icon.default);
2315    if (!this.textInput.placeholder?.trim()) this.textInput.placeholder = t.input.placeholder;
2316
2317    if (this.propError || (this._internalError && this.showInternalError)) {
2318      this.container.classList.add("has-error");
2319      this.textInput.setAttribute("aria-invalid", "true");
2320    }
2321
2322    if (this.disabled) this.textInput.disabled = true;
2323    if (this._textValue) this.textInput.value = this._textValue;
2324
2325    this._setRequired(this.required);
2326
2327    const helperMessage = this.propError || (this._internalError && this.showInternalError) ? this.helperText : "";
2328
2329    if (!this.helperTextElement && helperMessage) {
2330      this.helperTextElement = this._el("p", {
2331        class: "form-text",
2332        id: `${this.id}-helper-text`,
2333      });
2334      this.helperTextElement.textContent = helperMessage;
2335      this.container.appendChild(this.helperTextElement);
2336    }
2337
2338    if (this.helperTextElement) {
2339      this.helperTextElement.textContent = helperMessage;
2340      this.helperTextElement.setAttribute("id", `${this.id}-helper-text`);
2341      this.textInput.setAttribute("aria-describedby", `${this.id}-helper-text`);
2342    }
2343  }
2344  // ─── DOM – dialog ─────────────────────────────────────────────────────────────
2345
2346  _buildDialog() {
2347    this._removeDialog();
2348
2349    var t = DatePicker.LOCALE_TEXTS[this.locale];
2350    var MONTH_LABELS = this.locale === "cs" ? DatePicker.MONTH_LABELS_CS : DatePicker.MONTH_LABELS_EN;
2351    var WEEK_DAYS = this.locale === "cs" ? DatePicker.WEEK_DAYS_CS : DatePicker.WEEK_DAYS_EN;
2352    var DAY_LABELS = this.locale === "cs" ? DatePicker.DAY_LABELS_CS : DatePicker.DAY_LABELS_EN;
2353
2354    this._dialog = this._el("div", {
2355      class: "dp-dialog",
2356      role: "dialog",
2357      "aria-modal": "true",
2358      "aria-label": t.dialog.title,
2359    });
2360
2361    // ── Hlavička ──
2362    this._header = this._el("div", { class: "dp-header" });
2363    this._navBtns = {};
2364    var self = this;
2365    var navCfg = [
2366      {
2367        delta: -12,
2368        icon: DatePicker.ICONS.doubleLeft,
2369        label: t.dialog.header.button.previousYear,
2370        dis: DatePicker.isPrevDisabled(this._focusDay, this.minDate, -12),
2371      },
2372      {
2373        delta: -1,
2374        icon: DatePicker.ICONS.chevronLeft,
2375        label: t.dialog.header.button.previousMonth,
2376        dis: DatePicker.isPrevDisabled(this._focusDay, this.minDate, -1),
2377      },
2378      {
2379        delta: 1,
2380        icon: DatePicker.ICONS.chevronRight,
2381        label: t.dialog.header.button.nextMonth,
2382        dis: DatePicker.isNextDisabled(this._focusDay, this.maxDate, 1),
2383      },
2384      {
2385        delta: 12,
2386        icon: DatePicker.ICONS.doubleRight,
2387        label: t.dialog.header.button.nextYear,
2388        dis: DatePicker.isNextDisabled(this._focusDay, this.maxDate, 12),
2389      },
2390    ];
2391
2392    navCfg.forEach(function (cfg) {
2393      var btn = self._el("button", {
2394        type: "button",
2395        class: "dp-header-btn",
2396        "aria-label": cfg.label,
2397      });
2398      if (cfg.dis) btn.disabled = true;
2399      btn.innerHTML = cfg.icon;
2400      self._navBtns[cfg.delta] = btn;
2401    });
2402
2403    this._header.appendChild(this._navBtns[-12]);
2404    this._header.appendChild(this._navBtns[-1]);
2405
2406    this._headerTitle = this._el("span", {
2407      class: "dp-header-title",
2408      "aria-live": "polite",
2409    });
2410    this._headerTitle.textContent = MONTH_LABELS[this._focusDay.getMonth()] + " " + this._focusDay.getFullYear();
2411    this._header.appendChild(this._headerTitle);
2412
2413    this._header.appendChild(this._navBtns[1]);
2414    this._header.appendChild(this._navBtns[12]);
2415    this._dialog.appendChild(this._header);
2416
2417    // ── Mřížka ──
2418    var monthLabel = MONTH_LABELS[this._focusDay.getMonth()] + " " + this._focusDay.getFullYear();
2419    this._table = this._el("table", {
2420      class: "dp-table",
2421      role: "grid",
2422      "aria-label": monthLabel,
2423    });
2424
2425    var thead = document.createElement("thead");
2426    var headRow = document.createElement("tr");
2427    WEEK_DAYS.forEach(function (wd, i) {
2428      var th = self._el("th", { scope: "col", abbr: DAY_LABELS[i] });
2429      th.textContent = wd;
2430      headRow.appendChild(th);
2431    });
2432    thead.appendChild(headRow);
2433    this._table.appendChild(thead);
2434
2435    this._tbody = document.createElement("tbody");
2436    this._table.appendChild(this._tbody);
2437    this._renderGrid();
2438    this._dialog.appendChild(this._table);
2439
2440    // ── Patička ──
2441    var footer = this._el("div", { class: "dp-footer" });
2442
2443    this._clearBtn = this._el("button", {
2444      type: "button",
2445      class: "dp-clear-btn",
2446      "aria-label": t.dialog.footer.button.clear,
2447      title: t.dialog.footer.button.clear,
2448    });
2449    this._clearBtn.innerHTML = DatePicker.ICONS.trash;
2450    if (!this._selectedDay) {
2451      this._clearBtn.disabled = true;
2452      this._clearBtn.setAttribute("aria-hidden", "true");
2453    }
2454
2455    this._closeBtn = this._el("button", {
2456      type: "button",
2457      class: "btn btn-secondary btn-sm",
2458    });
2459    this._closeBtn.textContent = t.dialog.footer.button.close;
2460
2461    footer.appendChild(this._clearBtn);
2462    footer.appendChild(this._closeBtn);
2463    this._dialog.appendChild(footer);
2464
2465    // Live region
2466    this._liveRegion = this._el("span", {
2467      class: "sr-only",
2468      "aria-live": "polite",
2469    });
2470    this._dialog.appendChild(this._liveRegion);
2471
2472    this.inputWrapper.appendChild(this._dialog);
2473    this._bindDialogEvents();
2474  }
2475  _renderGrid() {
2476    if (!this._tbody) return;
2477    this._tbody.innerHTML = "";
2478
2479    var self = this;
2480    var grid = DatePicker.buildGrid(this._focusDay.getFullYear(), this._focusDay.getMonth());
2481    var showLastRow = grid.slice(35).some(function (d) {
2482      return d.getMonth() === self._focusDay.getMonth();
2483    });
2484    var DAY_LABELS = this.locale === "cs" ? DatePicker.DAY_LABELS_CS : DatePicker.DAY_LABELS_EN;
2485    var MONTH_LABELS = this.locale === "cs" ? DatePicker.MONTH_LABELS_CS : DatePicker.MONTH_LABELS_EN;
2486    var opts = {
2487      minDate: this.minDate,
2488      maxDate: this.maxDate,
2489      disabledDates: this.disabledDates,
2490      disabledDayIndexes: this.disabledDayIndexes,
2491    };
2492    for (var row = 0; row < 6; row++) {
2493      if (row === 5 && !showLastRow) continue;
2494      var tr = document.createElement("tr");
2495
2496      for (var col = 0; col < 7; col++) {
2497        var idx = row * 7 + col;
2498        var day = grid[idx];
2499        var inMonth = day.getMonth() === this._focusDay.getMonth();
2500
2501        var isSelected = this._selectedDay !== null && DatePicker.isSameDay(day, this._selectedDay);
2502        var isFocused = DatePicker.isSameDay(day, this._focusDay) && inMonth;
2503        var isToday = DatePicker.isSameDay(day, this.today);
2504        var dayIndex = day.getDay();
2505        var isWeekend = dayIndex === 0 || dayIndex === 6;
2506        var dayIdxMon = (dayIndex + 6) % 7;
2507        var dateStr =
2508          day.getFullYear() + "-" + DatePicker.pad2(day.getMonth() + 1) + "-" + DatePicker.pad2(day.getDate());
2509
2510        var disabled = !inMonth || DatePicker.isDayDisabled(day, opts);
2511
2512        var cls = "dp-day";
2513        if (!inMonth) cls += " dp-day-other";
2514        if (isToday && !isSelected) cls += " dp-day-today";
2515        if (isWeekend && inMonth && !isSelected) cls += " dp-day-weekend";
2516        if (isSelected) cls += " dp-day-selected";
2517        if (isSelected && this._internalError) cls += " dp-day-error";
2518
2519        var td = this._el("td", { role: "gridcell", style: "padding:0;" });
2520        if (isSelected) td.setAttribute("aria-selected", "true");
2521
2522        var btn = this._el("button", {
2523          type: "button",
2524          class: cls,
2525          "data-date": dateStr,
2526          tabindex: isFocused ? "0" : "-1",
2527        });
2528        if (disabled) btn.disabled = true;
2529
2530        if (inMonth) {
2531          btn.setAttribute(
2532            "aria-label",
2533            DAY_LABELS[dayIdxMon] + " " + day.getDate() + ". " + MONTH_LABELS[day.getMonth()] + " " + day.getFullYear(),
2534          );
2535          btn.textContent = String(day.getDate());
2536        }
2537
2538        td.appendChild(btn);
2539        tr.appendChild(td);
2540      }
2541      this._tbody.appendChild(tr);
2542    }
2543  }
2544  _updateHeader() {
2545    if (!this._header) return;
2546    var MONTH_LABELS = this.locale === "cs" ? DatePicker.MONTH_LABELS_CS : DatePicker.MONTH_LABELS_EN;
2547    var label = MONTH_LABELS[this._focusDay.getMonth()] + " " + this._focusDay.getFullYear();
2548    this._headerTitle.textContent = label;
2549    this._table.setAttribute("aria-label", label);
2550
2551    var self = this;
2552    [
2553      {
2554        delta: -12,
2555        dis: DatePicker.isPrevDisabled(this._focusDay, this.minDate, -12),
2556      },
2557      {
2558        delta: -1,
2559        dis: DatePicker.isPrevDisabled(this._focusDay, this.minDate, -1),
2560      },
2561      {
2562        delta: 1,
2563        dis: DatePicker.isNextDisabled(this._focusDay, this.maxDate, 1),
2564      },
2565      {
2566        delta: 12,
2567        dis: DatePicker.isNextDisabled(this._focusDay, this.maxDate, 12),
2568      },
2569    ].forEach(function (c) {
2570      if (self._navBtns[c.delta]) self._navBtns[c.delta].disabled = c.dis;
2571    });
2572  }
2573  _updateGridTabIndexes() {
2574    if (!this._tbody) return;
2575    var self = this;
2576    this._tbody.querySelectorAll("[data-date]").forEach(function (btn) {
2577      var parts = btn.dataset.date.split("-").map(Number);
2578      var day = new Date(parts[0], parts[1] - 1, parts[2]);
2579      var focused = DatePicker.isSameDay(day, self._focusDay) && day.getMonth() === self._focusDay.getMonth();
2580      btn.tabIndex = focused ? 0 : -1;
2581    });
2582  }
2583  // ─── Bindování událostí ───────────────────────────────────────────────────────
2584
2585  _addListener(store, el, evt, fn, opts) {
2586    el.addEventListener(evt, fn, opts);
2587    store.push({ el: el, evt: evt, fn: fn, opts: opts });
2588  }
2589
2590  _bindInputEvents() {
2591    var self = this;
2592    var add = function (el, evt, fn) {
2593      self._addListener(self._inputListeners, el, evt, fn);
2594    };
2595
2596    // --- Backspace/Delete přes keydown (funguje všude) ---
2597    add(this.textInput, "keydown", function (e) {
2598      if (e.key === "Backspace" || e.key === "Delete") {
2599        e.preventDefault();
2600        self._textValue = DatePicker.applyBackspace(self._textValue);
2601        self.textInput.value = self._textValue;
2602        self._validateInput(false);
2603        return;
2604      }
2605      if (e.key === "Escape" && self._isOpen) {
2606        self._closeDialog();
2607        return;
2608      }
2609      // Desktop: zpracuj tisknutelné znaky zde (mimo IME prohlížeče)
2610      // Na mobilním Chrome/Edge/Opera přijde key === "Unidentified" – přeskočíme,
2611      // postará se beforeinput/input
2612      if (e.key.length === 1 && !e.isComposing) {
2613        e.preventDefault();
2614        self._textValue = DatePicker.applyInput(self._textValue, e.key);
2615        self.textInput.value = self._textValue;
2616        self._validateInput(false);
2617      }
2618    });
2619
2620    // --- Mobilní Chrome/Edge/Opera: znaky přicházejí přes beforeinput ---
2621    add(this.textInput, "beforeinput", function (e) {
2622      // Pouze pro vkládání znaků (ne delete, které řešíme výše)
2623      if (e.inputType === "insertText" && e.data) {
2624        e.preventDefault();
2625        self._textValue = DatePicker.applyInput(self._textValue, e.data);
2626        self.textInput.value = self._textValue;
2627        self._validateInput(false);
2628      }
2629      // Mobilní backspace přes beforeinput
2630      if (e.inputType === "deleteContentBackward" || e.inputType === "deleteContentForward") {
2631        e.preventDefault();
2632        self._textValue = DatePicker.applyBackspace(self._textValue);
2633        self.textInput.value = self._textValue;
2634        self._validateInput(false);
2635      }
2636    });
2637
2638    // --- Fallback: pokud beforeinput nestačil (input se přesto změnil) ---
2639    // Resyncuje DOM hodnotu zpět na náš řízený stav
2640    add(this.textInput, "input", function () {
2641      if (self.textInput.value !== self._textValue) {
2642        self.textInput.value = self._textValue;
2643      }
2644    });
2645
2646    add(this.textInput, "blur", function () {
2647      self._validateInput(true);
2648      if (!self._internalError) {
2649        if (!self._textValue) {
2650          if (self._selectedDay !== null) {
2651            self._selectedDay = null;
2652            if (self.onDateChange) self.onDateChange(null);
2653          }
2654        } else {
2655          var parsed = DatePicker.parseDateText(self._textValue);
2656          if (parsed && (!self._selectedDay || !DatePicker.isSameDay(parsed, self._selectedDay))) {
2657            self._selectedDay = DatePicker.normalizeDate(parsed);
2658            if (self.onDateChange) self.onDateChange(new Date(self._selectedDay));
2659          }
2660        }
2661      }
2662    });
2663
2664    add(this.textInput, "paste", function (e) {
2665      e.preventDefault();
2666      var text = (e.clipboardData || window.clipboardData).getData("text");
2667      var parsed = DatePicker.parseDateText((text || "").trim());
2668      if (parsed) {
2669        self._textValue = DatePicker.formatDate(parsed);
2670        self.textInput.value = self._textValue;
2671        self._validateInput(false);
2672      }
2673    });
2674
2675    add(this.toggleBtn, "click", function (e) {
2676      e.preventDefault();
2677      if (self._isOpen) self._closeDialog();
2678      else self._openDialog();
2679    });
2680
2681    add(this.toggleBtn, "keydown", function (e) {
2682      if (e.key === "Enter" || e.key === " ") {
2683        self._openDialog();
2684        e.preventDefault();
2685      }
2686    });
2687  }
2688
2689  /*
2690  _bindInputEvents() {
2691    var self = this;
2692    var add = function (el, evt, fn) {
2693      self._addListener(self._inputListeners, el, evt, fn);
2694    };
2695    add(this.textInput, "keydown", function (e) {
2696      if (e.key === "Backspace" || e.key === "Delete") {
2697        e.preventDefault();
2698        self._textValue = DatePicker.applyBackspace(self._textValue);
2699        self.textInput.value = self._textValue;
2700        self._validateInput(false);
2701      } else if (e.key === "Escape" && self._isOpen) {
2702        self._closeDialog();
2703      } else if (e.key.length === 1) {
2704        e.preventDefault();
2705        self._textValue = DatePicker.applyInput(self._textValue, e.key);
2706        self.textInput.value = self._textValue;
2707        self._validateInput(false);
2708      }
2709    });
2710
2711    add(this.textInput, "blur", function () {
2712      self._validateInput(true);
2713      if (!self._internalError) {
2714        if (!self._textValue) {
2715          if (self._selectedDay !== null) {
2716            self._selectedDay = null;
2717            if (self.onDateChange) self.onDateChange(null);
2718          }
2719        } else {
2720          var parsed = DatePicker.parseDateText(self._textValue);
2721          if (parsed && (!self._selectedDay || !DatePicker.isSameDay(parsed, self._selectedDay))) {
2722            self._selectedDay = DatePicker.normalizeDate(parsed);
2723            if (self.onDateChange) self.onDateChange(new Date(self._selectedDay));
2724          }
2725        }
2726      }
2727    });
2728
2729    add(this.textInput, "paste", function (e) {
2730      e.preventDefault();
2731      var text = (e.clipboardData || window.clipboardData).getData("text");
2732      var parsed = DatePicker.parseDateText((text || "").trim());
2733      if (parsed) {
2734        self._textValue = DatePicker.formatDate(parsed);
2735        self.textInput.value = self._textValue;
2736        self._validateInput(false);
2737      }
2738    });
2739
2740    add(this.toggleBtn, "click", function (e) {
2741      e.preventDefault();
2742      if (self._isOpen) self._closeDialog();
2743      else self._openDialog();
2744    });
2745
2746    add(this.toggleBtn, "keydown", function (e) {
2747      if (e.key === "Enter" || e.key === " ") {
2748        self._openDialog();
2749        e.preventDefault();
2750      }
2751    });
2752  }
2753  */
2754  _bindDialogEvents() {
2755    var self = this;
2756    var add = function (el, evt, fn, opts) {
2757      self._addListener(self._dialogListeners, el, evt, fn, opts);
2758    };
2759    // Nav tlačítka
2760    [-12, -1, 1, 12].forEach(function (delta) {
2761      var btn = self._navBtns[delta];
2762      if (!btn) return;
2763
2764      add(btn, "click", function () {
2765        self._navMonth(delta);
2766      });
2767
2768      add(btn, "keydown", function (e) {
2769        if (e.key === "Enter") {
2770          self._navMonth(delta);
2771          e.preventDefault();
2772          return;
2773        }
2774        if (e.key === "Escape" || e.key === "Esc") {
2775          self._closeDialog();
2776          e.preventDefault();
2777          return;
2778        }
2779        if (e.key !== "Tab") return;
2780        // Tab: přechod mezi povolnými tlačítky nebo do gridu
2781        var buttons = Array.from(self._header.querySelectorAll("button"));
2782        var cur = buttons.indexOf(e.currentTarget);
2783        var dir = e.shiftKey ? -1 : 1;
2784        for (var i = cur + dir; i >= 0 && i < buttons.length; i += dir) {
2785          if (!buttons[i].disabled) {
2786            e.preventDefault();
2787            buttons[i].focus();
2788            return;
2789          }
2790        }
2791        e.preventDefault();
2792        self._focusCurrentDay();
2793      });
2794    });
2795
2796    // Grid – event delegation
2797    add(this._table, "click", function (e) {
2798      var btn = e.target.closest("[data-date]");
2799      if (!btn || btn.disabled) return;
2800      var parts = btn.dataset.date.split("-").map(Number);
2801      self._confirmDay(new Date(parts[0], parts[1] - 1, parts[2]));
2802    });
2803
2804    add(this._table, "keydown", function (e) {
2805      if (e.target.closest("[data-date]")) self._handleDayKeyDown(e);
2806    });
2807
2808    add(this._table, "focusin", function (e) {
2809      if (!e.target.closest("[data-date]")) return;
2810      self._message = DatePicker.LOCALE_TEXTS[self.locale].dialog.message.navigation.days;
2811      if (self._liveRegion) self._liveRegion.textContent = self._message;
2812    });
2813
2814    // Tlačítko Zavřít
2815    add(this._closeBtn, "click", function () {
2816      self._closeDialog();
2817    });
2818
2819    add(this._closeBtn, "keydown", function (e) {
2820      if (e.key === "Tab" && !e.shiftKey) {
2821        self._focusCurrentDay();
2822        e.preventDefault();
2823      } else if (e.key === "Tab" && e.shiftKey) {
2824        if (self._selectedDay && self._clearBtn && !self._clearBtn.disabled) {
2825          self._clearBtn.focus();
2826        } else {
2827          self._focusCurrentDay();
2828        }
2829        e.preventDefault();
2830      } else if (e.key === "Escape" || e.key === "Esc") {
2831        self._closeDialog();
2832        e.preventDefault();
2833      }
2834    });
2835
2836    // Tlačítko Smazat
2837    add(this._clearBtn, "click", function () {
2838      self._clearDate();
2839    });
2840
2841    add(this._clearBtn, "keydown", function (e) {
2842      if (e.key === "Tab" && !e.shiftKey) {
2843        self._closeBtn.focus();
2844        e.preventDefault();
2845      } else if (e.key === "Tab" && e.shiftKey) {
2846        self._focusCurrentDay();
2847        e.preventDefault();
2848      } else if (e.key === "Escape" || e.key === "Esc") {
2849        self._closeDialog();
2850        e.preventDefault();
2851      }
2852    });
2853
2854    // Klik mimo dialog
2855    var outsideHandler = function (e) {
2856      if (!self.toggleBtn.contains(e.target) && self._dialog && !self._dialog.contains(e.target)) {
2857        self._closeDialog(false);
2858      }
2859    };
2860    self._addListener(self._docListeners, document.body, "pointerup", outsideHandler, true);
2861  }
2862  // ─── Akce ─────────────────────────────────────────────────────────────────────
2863
2864  _openDialog() {
2865    var parsed = DatePicker.parseDateText(this._textValue);
2866    var opts = {
2867      minDate: this.minDate,
2868      maxDate: this.maxDate,
2869      disabledDates: this.disabledDates,
2870      disabledDayIndexes: this.disabledDayIndexes,
2871    };
2872
2873    if (parsed && DatePicker.isDayDisabled(parsed, opts)) {
2874      parsed = null;
2875    }
2876
2877    var base = parsed || this._defaultFocusedDate;
2878    var fd = DatePicker.findClosestEnabledDate(base, opts);
2879
2880    this._focusDay = fd;
2881    this._selectedDay = parsed;
2882    this._pinnedDay = fd.getDate();
2883    this._isOpen = true;
2884    this._message = "";
2885
2886    this._buildDialog();
2887    this.toggleBtn.setAttribute("aria-expanded", "true");
2888
2889    if (this._dialog) {
2890      var rect = this.container.getBoundingClientRect();
2891      var dialogRect = this._dialog.getBoundingClientRect();
2892      var spaceBelow = window.innerHeight - rect.bottom;
2893      if (spaceBelow < dialogRect.height && rect.top > dialogRect.height) {
2894        this._dialog.classList.add("dp-dialog-up");
2895      }
2896    }
2897
2898    var self = this;
2899    requestAnimationFrame(function () {
2900      self._focusDateCell(fd);
2901    });
2902  }
2903  _closeDialog(returnFocus) {
2904    if (returnFocus === undefined) returnFocus = true;
2905    if (!this._isOpen && !this._dialog) return;
2906    this._isOpen = false;
2907
2908    this._docListeners.forEach(function (l) {
2909      l.el.removeEventListener(l.evt, l.fn, l.opts);
2910    });
2911    this._docListeners = [];
2912
2913    this._dialogListeners.forEach(function (l) {
2914      l.el.removeEventListener(l.evt, l.fn, l.opts);
2915    });
2916    this._dialogListeners = [];
2917
2918    this._removeDialog();
2919
2920    this.toggleBtn.setAttribute("aria-expanded", "false");
2921    if (returnFocus) {
2922      var self = this;
2923      setTimeout(function () {
2924        self.toggleBtn.focus();
2925      }, 0);
2926    }
2927  }
2928  _removeDialog() {
2929    if (this._dialog) {
2930      this._dialog.remove();
2931      this._dialog = null;
2932      this._tbody = null;
2933      this._table = null;
2934      this._header = null;
2935      this._navBtns = null;
2936      this._clearBtn = null;
2937      this._closeBtn = null;
2938      this._liveRegion = null;
2939    }
2940  }
2941  _navMonth(delta) {
2942    this._focusDay = DatePicker.shiftMonth(this._focusDay, delta, this._pinnedDay);
2943    this._pinnedDay = this._focusDay.getDate();
2944    this._updateHeader();
2945    this._renderGrid();
2946  }
2947  _confirmDay(day) {
2948    var d = day || this._focusDay;
2949    this._selectedDay = DatePicker.normalizeDate(d);
2950    this._textValue = DatePicker.formatDate(d);
2951    this._internalError = false;
2952    this.textInput.value = this._textValue;
2953    this.toggleBtn.setAttribute("aria-label", DatePicker.LOCALE_TEXTS[this.locale].input.icon.selected);
2954    this._updateInputError("");
2955    if (this.onDateChange) this.onDateChange(new Date(this._selectedDay));
2956    this._closeDialog();
2957  }
2958  _clearDate() {
2959    this._selectedDay = null;
2960    this._textValue = "";
2961    this._message = "";
2962    this._internalError = false;
2963    this.textInput.value = "";
2964    this.toggleBtn.setAttribute("aria-label", DatePicker.LOCALE_TEXTS[this.locale].input.icon.default);
2965
2966    if (this.required) {
2967      if (this.showInternalError) {
2968        this._updateInputError("required");
2969      }
2970      this.onError("required");
2971    } else this._updateInputError("");
2972
2973    if (this.onDateChange) this.onDateChange(null);
2974    this._closeDialog();
2975  }
2976  _validateInput(onBlur) {
2977    var v = this._textValue;
2978    if (!v) {
2979      this._internalError = false;
2980      this._updateInputError("");
2981      return;
2982    }
2983
2984    var regex = /^(\d{1,2})\.\s*(\d{1,2})\.\s*(\d{4})$/;
2985    var match = v.match(regex);
2986
2987    if (!match) {
2988      if (onBlur) {
2989        this._internalError = true;
2990        if (this.onError) this.onError("invalidFormat");
2991      }
2992      this._updateInputError("invalidFormat");
2993      return;
2994    }
2995
2996    var day = parseInt(match[1], 10);
2997    var month = parseInt(match[2], 10) - 1;
2998    var year = parseInt(match[3], 10);
2999    var date = new Date(year, month, day);
3000    var valid = date.getFullYear() === year && date.getMonth() === month && date.getDate() === day;
3001
3002    var reason = null;
3003    if (!valid) reason = "invalidDate";
3004    else if (this.minDate && date < this.minDate) reason = "beforeMinDate";
3005    else if (this.maxDate && date > this.maxDate) reason = "afterMaxDate";
3006    else if (DatePicker.isDateInArray(date, this.disabledDates)) reason = "disabledDate";
3007    else if (DatePicker.isDayInDisabledIndexes(date, this.disabledDayIndexes)) reason = "disabledDay";
3008
3009    if (reason) {
3010      this._internalError = true;
3011      if (this.onError) this.onError(reason);
3012    } else {
3013      this._internalError = false;
3014    }
3015
3016    this._updateInputError(reason);
3017  }
3018  _getErrorText(reason) {
3019    if (!reason) return "";
3020    var t = DatePicker.LOCALE_TEXTS[this.locale].input.error;
3021    var text = t[reason];
3022    if (!text) return reason; // fallback: vrátí surový klíč
3023    if (reason === "beforeMinDate" && this.minDate) {
3024      text = text.replace("{date}", DatePicker.formatDate(this.minDate));
3025    } else if (reason === "afterMaxDate" && this.maxDate) {
3026      text = text.replace("{date}", DatePicker.formatDate(this.maxDate));
3027    }
3028    return text;
3029  }
3030
3031  _setRequired(required) {
3032    const labelEl = this.container.querySelector("label");
3033
3034    this.textInput.required = !!required;
3035    this.required = !!required;
3036
3037    if (!labelEl) return;
3038
3039    if (required) {
3040      const existing = [...labelEl.querySelectorAll('span[aria-hidden="true"]')].find(
3041        (el) => el.textContent.trim() === "*",
3042      );
3043
3044      if (!existing) {
3045        const star = this._el("span", {
3046          "aria-hidden": "true",
3047        });
3048        star.textContent = "*";
3049
3050        const firstElement = labelEl.querySelector(":scope > *");
3051
3052        const space = document.createTextNode(" ");
3053
3054        if (firstElement) {
3055          labelEl.insertBefore(space, firstElement);
3056          labelEl.insertBefore(star, firstElement);
3057          labelEl.insertBefore(space, firstElement);
3058        } else {
3059          labelEl.appendChild(space);
3060          labelEl.appendChild(star);
3061        }
3062      }
3063    } else {
3064      const requiredMark = labelEl.querySelector("span");
3065      requiredMark?.remove();
3066    }
3067  }
3068
3069  _updateInputError(error) {
3070    if (!this.showInternalError) return;
3071
3072    var hasError = this.propError || this._internalError;
3073    if (hasError) {
3074      var msg = this._getErrorText(error); // překlad
3075      if (this.helperTextElement) {
3076        this.helperTextElement.textContent = msg;
3077      } else {
3078        this.helperTextElement = this._el("p", {
3079          class: "form-text",
3080          id: `${this.id}-helper-text`,
3081        });
3082        this.helperTextElement.textContent = msg; // překlad
3083        this.container.appendChild(this.helperTextElement);
3084      }
3085      this.container.classList.add("has-error");
3086      this.textInput.setAttribute("aria-invalid", "true");
3087    } else {
3088      this.helperTextElement?.remove();
3089      this.helperTextElement = null;
3090      this.container.classList.remove("has-error");
3091      this.textInput.setAttribute("aria-invalid", "false");
3092    }
3093  }
3094
3095  _focusDateCell(date) {
3096    if (!this._dialog) return;
3097    var dateStr =
3098      date.getFullYear() + "-" + DatePicker.pad2(date.getMonth() + 1) + "-" + DatePicker.pad2(date.getDate());
3099    var btn = this._dialog.querySelector('[data-date="' + dateStr + '"]');
3100    if (btn) btn.focus();
3101  }
3102  _focusCurrentDay() {
3103    this._focusDateCell(this._focusDay);
3104  }
3105  _handleDayKeyDown(e) {
3106    var cur = this._focusDay;
3107    var next = null;
3108
3109    switch (e.key) {
3110      case "Escape":
3111      case "Esc":
3112        this._closeDialog();
3113        return;
3114
3115      case "Enter":
3116        this._confirmDay(cur);
3117        e.preventDefault();
3118        return;
3119
3120      case " ":
3121        this._selectedDay = DatePicker.normalizeDate(new Date(cur));
3122        this._textValue = DatePicker.formatDate(cur);
3123        this.textInput.value = this._textValue;
3124        e.preventDefault();
3125        return;
3126
3127      case "Tab":
3128        if (e.shiftKey) {
3129          var btns = Array.from(this._header.querySelectorAll("button"));
3130          var firstOk = null;
3131          for (var i = 0; i < btns.length; i++) {
3132            if (!btns[i].disabled) {
3133              firstOk = btns[i];
3134              break;
3135            }
3136          }
3137          if (firstOk) firstOk.focus();
3138          else this._closeBtn.focus();
3139        } else {
3140          this._closeBtn.focus();
3141        }
3142        this._message = DatePicker.LOCALE_TEXTS[this.locale].dialog.message.navigation.other;
3143        if (this._liveRegion) this._liveRegion.textContent = this._message;
3144        e.preventDefault();
3145        return;
3146
3147      case "ArrowRight":
3148      case "Right": {
3149        var d = new Date(cur);
3150        d.setDate(d.getDate() + 1);
3151        next = d;
3152        break;
3153      }
3154      case "ArrowLeft":
3155      case "Left": {
3156        var d = new Date(cur);
3157        d.setDate(d.getDate() - 1);
3158        next = d;
3159        break;
3160      }
3161      case "ArrowDown":
3162      case "Down": {
3163        var d = new Date(cur);
3164        d.setDate(d.getDate() + 7);
3165        next = d;
3166        break;
3167      }
3168      case "ArrowUp":
3169      case "Up": {
3170        var d = new Date(cur);
3171        d.setDate(d.getDate() - 7);
3172        next = d;
3173        break;
3174      }
3175      case "Home": {
3176        var d = new Date(cur);
3177        d.setDate(d.getDate() - ((d.getDay() + 6) % 7));
3178        next = d;
3179        break;
3180      }
3181      case "End": {
3182        var d = new Date(cur);
3183        d.setDate(d.getDate() + (6 - ((d.getDay() + 6) % 7)));
3184        next = d;
3185        break;
3186      }
3187      case "PageUp": {
3188        next = DatePicker.shiftMonth(cur, e.shiftKey ? -12 : -1, this._pinnedDay);
3189        break;
3190      }
3191      case "PageDown": {
3192        next = DatePicker.shiftMonth(cur, e.shiftKey ? 12 : 1, this._pinnedDay);
3193        break;
3194      }
3195      default:
3196        return;
3197    }
3198
3199    if (next) {
3200      var prevMonth = this._focusDay.getMonth();
3201      var prevYear = this._focusDay.getFullYear();
3202      this._pinnedDay = next.getDate();
3203      this._focusDay = next;
3204      e.preventDefault();
3205
3206      var monthChanged = next.getMonth() !== prevMonth || next.getFullYear() !== prevYear;
3207      if (monthChanged) {
3208        this._updateHeader();
3209        this._renderGrid();
3210      } else {
3211        this._updateGridTabIndexes();
3212      }
3213
3214      var self = this;
3215      requestAnimationFrame(function () {
3216        self._focusDateCell(next);
3217      });
3218    }
3219  }
3220  // ─── Liferay integrace ────────────────────────────────────────────────────────
3221
3222  _bindLiferay() {
3223    if (typeof Liferay !== "undefined" && typeof Liferay.on === "function") {
3224      var self = this;
3225      this._liferayHandler = function () {
3226        self.destroy();
3227      };
3228      Liferay.on("beforeNavigate", this._liferayHandler);
3229    }
3230  }
3231  // ─── Veřejné API ─────────────────────────────────────────────────────────────
3232
3233  /** Vrátí aktuálně vybraný datum nebo null */
3234  getValue() {
3235    return this._selectedDay ? new Date(this._selectedDay) : null;
3236  }
3237  /**
3238   * Univerzální setter
3239   * @param {"value"|"minDate"|"maxDate"|"error"|"message"} key
3240   * @param {*} val
3241   */
3242  set(key, val) {
3243    switch (key) {
3244      case "value": {
3245        if (val) {
3246          const normalized = DatePicker.normalizeDate(val);
3247          const opts = {
3248            minDate: this.minDate,
3249            maxDate: this.maxDate,
3250            disabledDates: this.disabledDates,
3251            disabledDayIndexes: this.disabledDayIndexes,
3252          };
3253          if (DatePicker.isDayDisabled(normalized, opts)) {
3254            console.warn(
3255              `DatePicker.set: hodnota ${normalized} je mimo povolený rozsah nebo je zakázána. Nastavení bude ignorováno.`,
3256            );
3257            return;
3258          }
3259          this._selectedDay = normalized;
3260          this._textValue = DatePicker.formatDate(val);
3261          this.textInput.value = this._textValue;
3262          this.toggleBtn.setAttribute("aria-label", DatePicker.LOCALE_TEXTS[this.locale].input.icon.selected);
3263        } else {
3264          this._selectedDay = null;
3265          this._textValue = "";
3266          this.textInput.value = "";
3267          this.toggleBtn.setAttribute("aria-label", DatePicker.LOCALE_TEXTS[this.locale].input.icon.default);
3268        }
3269        this._internalError = false;
3270        this._updateInputError("");
3271        break;
3272      }
3273
3274      case "minDate": {
3275        const newMin = val ? DatePicker.normalizeDate(val) : null;
3276        if (newMin && this.maxDate && newMin > this.maxDate) {
3277          console.warn("DatePicker.set: minDate nesmí být větší než maxDate. Nastavení bude ignorováno.");
3278          return;
3279        }
3280        this.minDate = newMin;
3281        this._defaultFocusedDate = DatePicker.getDefaultFocusedDate(this.today, this.minDate, this.maxDate);
3282        this._validateInput(false);
3283        break;
3284      }
3285
3286      case "maxDate": {
3287        const newMax = val ? DatePicker.normalizeDate(val) : null;
3288        if (newMax && this.minDate && newMax < this.minDate) {
3289          console.warn("DatePicker.set: maxDate nesmí být menší než minDate. Nastavení bude ignorováno.");
3290          return;
3291        }
3292        this.maxDate = newMax;
3293        this._defaultFocusedDate = DatePicker.getDefaultFocusedDate(this.today, this.minDate, this.maxDate);
3294        this._validateInput(false);
3295        break;
3296      }
3297
3298      case "error": {
3299        if (!val) {
3300          this.helperTextElement?.remove();
3301          this.helperTextElement = null;
3302          this.textInput.removeAttribute("aria-describedby");
3303          this.textInput.setAttribute("aria-invalid", "false");
3304          this.container.classList.remove("has-error");
3305        } else {
3306          this.container.classList.add("has-error");
3307          this.textInput.setAttribute("aria-invalid", "true");
3308
3309          if (this.helperTextElement) {
3310            this.helperTextElement.textContent = val;
3311          } else {
3312            this.helperTextElement = this._el("p", {
3313              class: "form-text",
3314            });
3315            this.helperTextElement.textContent = val;
3316            this.container.appendChild(this.helperTextElement);
3317          }
3318          this.helperTextElement.setAttribute("id", `${this.id}-helper-text`);
3319          this.textInput.setAttribute("aria-describedby", `${this.id}-helper-text`);
3320        }
3321        break;
3322      }
3323
3324      case "message": {
3325        if (!val) {
3326          this.helperTextElement?.remove();
3327          this.helperTextElement = null;
3328          this.textInput.removeAttribute("aria-describedby");
3329          return;
3330        }
3331        if (this.helperTextElement) {
3332          this.helperTextElement.textContent = val;
3333        } else {
3334          this.helperTextElement = this._el("p", {
3335            class: "form-text",
3336          });
3337          this.helperTextElement.textContent = val;
3338          this.container.appendChild(this.helperTextElement);
3339        }
3340        this.helperTextElement.setAttribute("id", `${this.id}-helper-text`);
3341        this.textInput.setAttribute("aria-describedby", `${this.id}-helper-text`);
3342        break;
3343      }
3344      case "required": {
3345        this._setRequired(val);
3346        break;
3347      }
3348
3349      default:
3350        console.warn(`DatePicker.set: neznámý klíč "${key}"`);
3351    }
3352  }
3353  /* Nastaví focus na input */
3354  focus() {
3355    if (this.textInput && !this.textInput.disabled) {
3356      this.textInput.focus();
3357    }
3358  }
3359
3360  /**
3361   * Odstraní všechny event listenery a vymaže DOM.
3362   * Volá se automaticky při Liferay.beforeNavigate.
3363   * Při manuálním použití zavolejte explicitně před odstraněním containeru.
3364   */
3365  destroy() {
3366    this._closeDialog(false);
3367
3368    this._inputListeners.forEach(function (l) {
3369      l.el.removeEventListener(l.evt, l.fn, l.opts);
3370    });
3371    this._inputListeners = [];
3372
3373    if (typeof Liferay !== "undefined" && typeof Liferay.detach === "function" && this._liferayHandler) {
3374      Liferay.detach("beforeNavigate", this._liferayHandler);
3375      this._liferayHandler = null;
3376    }
3377
3378    //  this.container.classList.remove("dp-wrap");
3379    //  this.container.innerHTML = "";
3380  }
3381}
3382class Collapse {
3383  /**
3384   * @param {string|HTMLElement} trigger - ID nebo přímo element
3385   * @param {string|HTMLElement} panel   - ID nebo přímo element
3386   * @param {Object}   [options]
3387   * @param {number}   [options.duration=300] - délka animace v ms
3388   * @param {Function} [options.onOpening]    - zavolá se na začátku otevírání (před animací)
3389   * @param {Function} [options.onOpen]       - zavolá se po dokončení otevírací animace
3390   * @param {Function} [options.onClosing]    - zavolá se na začátku zavírání (před animací)
3391   * @param {Function} [options.onClose]      - zavolá se po dokončení zavírací animace
3392   */
3393  constructor(trigger, panel, { duration = 300, onOpening, onOpen, onClosing, onClose } = {}) {
3394    this.trigger = Collapse._resolveElement(trigger);
3395    this.panel = Collapse._resolveElement(panel);
3396    this.duration = duration;
3397
3398    this.onOpening = onOpening;
3399    this.onOpen = onOpen;
3400    this.onClosing = onClosing;
3401    this.onClose = onClose;
3402
3403    if (!this.trigger || !this.panel) {
3404      console.warn(`Collapse: element not found`, { trigger, panel });
3405      return;
3406    }
3407
3408    this._onClick = () => this.toggle();
3409    this._onKeydown = (e) => {
3410      if (e.key === "Enter" || e.key === " ") {
3411        e.preventDefault();
3412        this.toggle();
3413      }
3414    };
3415    this._onBeforeNavigate = () => this.destroy();
3416
3417    const panelId = this.panel.id;
3418    const triggerId = this.trigger.id;
3419
3420    this.panel.style.overflow = "hidden";
3421    this.panel.style.transition = `max-height ${this.duration}ms ease`;
3422    this.panel.style.maxHeight = "0";
3423    this.panel.setAttribute("hidden", "");
3424    this.trigger.setAttribute("aria-expanded", "false");
3425
3426    if (panelId) {
3427      this.trigger.setAttribute("aria-controls", panelId);
3428    }
3429
3430    this.panel.setAttribute("role", "region");
3431
3432    if (triggerId) {
3433      this.panel.setAttribute("aria-labelledby", triggerId);
3434    }
3435
3436    this.trigger.addEventListener("click", this._onClick);
3437    this.trigger.addEventListener("keydown", this._onKeydown);
3438
3439    if (window.Liferay) {
3440      Liferay.on("beforeNavigate", this._onBeforeNavigate);
3441    }
3442  }
3443
3444  static _resolveElement(value) {
3445    if (typeof value === "string") {
3446      return document.getElementById(value);
3447    }
3448    if (value instanceof Element) {
3449      return value;
3450    }
3451    return null;
3452  }
3453
3454  _getPanelHeight() {
3455    return this.panel.scrollHeight;
3456
3457    /* const lastChild = this.panel.lastElementChild;
3458
3459    if (!lastChild) {
3460      return this.panel.scrollHeight;
3461    }
3462
3463    const panelTop = this.panel.getBoundingClientRect().top;
3464    const lastBottom = lastChild.getBoundingClientRect().bottom;
3465    const marginBottom = parseFloat(getComputedStyle(lastChild).marginBottom);
3466
3467    return lastBottom - panelTop + marginBottom; */
3468  }
3469
3470  open() {
3471    if (typeof this.onOpening === "function") {
3472      this.onOpening(this);
3473    }
3474
3475    this.panel.removeAttribute("hidden");
3476    this.trigger.setAttribute("aria-expanded", "true");
3477    this.trigger.classList.add("opening");
3478    this.panel.classList.add("opening");
3479    this.panel.style.maxHeight = this._getPanelHeight() + "px";
3480
3481    this.panel.addEventListener(
3482      "transitionend",
3483      () => {
3484        if (this.trigger.getAttribute("aria-expanded") === "true") {
3485          this.trigger.classList.remove("opening");
3486          this.panel.classList.remove("opening");
3487          this.trigger.classList.add("open");
3488          this.panel.classList.add("open");
3489          this.panel.style.maxHeight = "none";
3490
3491          if (typeof this.onOpen === "function") {
3492            this.onOpen(this);
3493          }
3494        }
3495      },
3496      { once: true },
3497    );
3498  }
3499
3500  close() {
3501    if (typeof this.onClosing === "function") {
3502      this.onClosing(this);
3503    }
3504
3505    this.panel.style.maxHeight = this._getPanelHeight() + "px";
3506
3507    requestAnimationFrame(() =>
3508      requestAnimationFrame(() => {
3509        this.trigger.setAttribute("aria-expanded", "false");
3510        this.trigger.classList.remove("open");
3511        this.panel.classList.remove("open");
3512        this.trigger.classList.add("closing");
3513        this.panel.classList.add("closing");
3514        this.panel.style.maxHeight = "0";
3515
3516        this.panel.addEventListener(
3517          "transitionend",
3518          () => {
3519            this.trigger.classList.remove("closing");
3520            this.panel.classList.remove("closing");
3521            this.panel.setAttribute("hidden", "");
3522
3523            if (typeof this.onClose === "function") {
3524              this.onClose(this);
3525            }
3526          },
3527          { once: true },
3528        );
3529      }),
3530    );
3531  }
3532
3533  toggle() {
3534    this.trigger.getAttribute("aria-expanded") === "true" ? this.close() : this.open();
3535  }
3536
3537  isOpen() {
3538    return this.trigger.getAttribute("aria-expanded") === "true";
3539  }
3540
3541  destroy() {
3542    if (this.trigger) {
3543      this.trigger.removeEventListener("click", this._onClick);
3544      this.trigger.removeEventListener("keydown", this._onKeydown);
3545    }
3546    if (window.Liferay) {
3547      Liferay.detach("beforeNavigate", this._onBeforeNavigate);
3548    }
3549  }
3550}
3551/*
3552 *   This content is licensed according to the W3C Software License at
3553 *   https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
3554 *
3555 *   File:   tabs-manual.js
3556 *
3557 *   Desc:   Tablist widget that implements ARIA Authoring Practices
3558 *           with Liferay 7.4 / Senna.js lifecycle support
3559 */
3560
3561"use strict";
3562
3563class TabsManual {
3564  constructor(groupNode) {
3565    this.tablistNode = groupNode;
3566
3567    this.tabs = [];
3568    this.firstTab = null;
3569    this.lastTab = null;
3570    this.tabpanels = [];
3571
3572    // Bind handlers once so we can remove them later
3573    this._boundOnKeydown = this.onKeydown.bind(this);
3574    this._boundOnClick = this.onClick.bind(this);
3575
3576    this.tabs = Array.from(this.tablistNode.querySelectorAll("[role=tab]"));
3577
3578    for (var i = 0; i < this.tabs.length; i += 1) {
3579      var tab = this.tabs[i];
3580      var tabpanel = document.getElementById(tab.getAttribute("aria-controls"));
3581
3582      tab.tabIndex = -1;
3583      tab.setAttribute("aria-selected", "false");
3584      this.tabpanels.push(tabpanel);
3585
3586      tab.addEventListener("keydown", this._boundOnKeydown);
3587      tab.addEventListener("click", this._boundOnClick);
3588
3589      if (!this.firstTab) {
3590        this.firstTab = tab;
3591      }
3592      this.lastTab = tab;
3593    }
3594
3595    this.setSelectedTab(this.firstTab);
3596  }
3597
3598  /**
3599   * Removes all event listeners from tabs.
3600   * Called on destroy / Senna navigate-away.
3601   */
3602  destroy() {
3603    for (var i = 0; i < this.tabs.length; i += 1) {
3604      var tab = this.tabs[i];
3605      tab.removeEventListener("keydown", this._boundOnKeydown);
3606      tab.removeEventListener("click", this._boundOnClick);
3607    }
3608    this.tabs = [];
3609    this.tabpanels = [];
3610    this.firstTab = null;
3611    this.lastTab = null;
3612  }
3613
3614  setSelectedTab(currentTab) {
3615    for (var i = 0; i < this.tabs.length; i += 1) {
3616      var tab = this.tabs[i];
3617      if (currentTab === tab) {
3618        tab.setAttribute("aria-selected", "true");
3619        tab.removeAttribute("tabindex");
3620        tab.classList.add("active");
3621        this.tabpanels[i].classList.add("show", "active");
3622      } else {
3623        tab.setAttribute("aria-selected", "false");
3624        tab.tabIndex = -1;
3625        tab.classList.remove("active");
3626        this.tabpanels[i].classList.remove("show", "active");
3627      }
3628    }
3629  }
3630
3631  moveFocusToTab(currentTab) {
3632    currentTab.focus();
3633  }
3634
3635  moveFocusToPreviousTab(currentTab) {
3636    var index;
3637    if (currentTab === this.firstTab) {
3638      this.moveFocusToTab(this.lastTab);
3639    } else {
3640      index = this.tabs.indexOf(currentTab);
3641      this.moveFocusToTab(this.tabs[index - 1]);
3642    }
3643  }
3644
3645  moveFocusToNextTab(currentTab) {
3646    var index;
3647    if (currentTab === this.lastTab) {
3648      this.moveFocusToTab(this.firstTab);
3649    } else {
3650      index = this.tabs.indexOf(currentTab);
3651      this.moveFocusToTab(this.tabs[index + 1]);
3652    }
3653  }
3654
3655  onKeydown(event) {
3656    var tgt = event.currentTarget,
3657      flag = false;
3658
3659    switch (event.key) {
3660      case "ArrowLeft":
3661        this.moveFocusToPreviousTab(tgt);
3662        flag = true;
3663        break;
3664
3665      case "ArrowRight":
3666        this.moveFocusToNextTab(tgt);
3667        flag = true;
3668        break;
3669
3670      case "Home":
3671        this.moveFocusToTab(this.firstTab);
3672        flag = true;
3673        break;
3674
3675      case "End":
3676        this.moveFocusToTab(this.lastTab);
3677        flag = true;
3678        break;
3679
3680      default:
3681        break;
3682    }
3683
3684    if (flag) {
3685      event.stopPropagation();
3686      event.preventDefault();
3687    }
3688  }
3689
3690  onClick(event) {
3691    this.setSelectedTab(event.currentTarget);
3692  }
3693}
3694
3695// ---------------------------------------------------------------------------
3696// Lifecycle manager – handles both classic page load and Senna.js navigation
3697// ---------------------------------------------------------------------------
3698
3699(function () {
3700  /** Holds all active TabsManual instances for the current "page view". */
3701  var activeInstances = [];
3702
3703  /**
3704   * Initialises TabsManual for every tablist found in the document
3705   * (or within an optional rootNode for partial DOM updates).
3706   */
3707  function initTabs(rootNode) {
3708    rootNode = rootNode || document;
3709    var tablists = rootNode.querySelectorAll("[role=tablist].nav-tabs");
3710    for (var i = 0; i < tablists.length; i++) {
3711      activeInstances.push(new TabsManual(tablists[i]));
3712    }
3713  }
3714
3715  /**
3716   * Destroys every active instance and clears the list.
3717   */
3718  function destroyTabs() {
3719    for (var i = 0; i < activeInstances.length; i++) {
3720      activeInstances[i].destroy();
3721    }
3722    activeInstances = [];
3723  }
3724
3725  // ── Senna.js (Liferay SPA) integration ────────────────────────────────────
3726  //
3727  // Liferay 7.4 ships Senna.js and fires these custom DOM events on the
3728  // document during navigation:
3729  //
3730  //   startNavigate   – user triggered a SPA navigation (page is about to leave)
3731  //   endNavigate     – new page content has been injected into the DOM
3732  //
3733  // We listen for them here so instances are cleaned up and re-created
3734  // correctly without any memory leaks.
3735
3736  var sennaAvailable = typeof Liferay !== "undefined" && Liferay.SPA && Liferay.SPA.app; // Senna app object exposed by Liferay 7.4
3737
3738  if (sennaAvailable) {
3739    // Liferay also exposes these as Liferay global events via Liferay.on().
3740    // Using document-level DOM events is more portable and works the same way.
3741
3742    document.addEventListener("startNavigate", function () {
3743      destroyTabs();
3744    });
3745
3746    document.addEventListener("endNavigate", function (event) {
3747      // event.applicationData or event.path can be used to scope the
3748      // re-init if needed; here we simply re-scan the whole document.
3749      initTabs();
3750    });
3751
3752    // Initial load within the SPA context: Liferay may have already rendered
3753    // content before this script runs (portlet-level loading).  Always do an
3754    // eager init here as well.
3755    if (document.readyState === "complete" || document.readyState === "interactive") {
3756      initTabs();
3757    } else {
3758      document.addEventListener("DOMContentLoaded", function () {
3759        initTabs();
3760      });
3761    }
3762  } else {
3763    // ── Classic (non-SPA) page load ──────────────────────────────────────────
3764    window.addEventListener("load", function () {
3765      initTabs();
3766    });
3767  }
3768})();
3769class FileUpload {
3770  /**
3771   * @param {HTMLElement} root  – kořenový element (.file-upload-wrapper)
3772   * @param {object}      opts
3773   * @param {Function}    [opts.onFiles] – callback(files[]) po každé změně
3774   * @param {Function}    [opts.onError] – callback(reason, files[]) při chybě
3775   * @param {number}      [opts.maxSize] – max velikost v bytech (default: 10485760 = 10MB)
3776   * @param {number}      [opts.maxFiles] – max počet souborů (default: 20)
3777   */
3778  constructor(root, opts) {
3779    opts = opts || {};
3780    this.root = root;
3781    this.files = [];
3782    this.onFiles = opts.onFiles || null;
3783    this._onErrorCallback = opts.onError || null;
3784
3785    this.maxSize = opts.maxSize || 10485760;
3786    this.maxFiles = opts.maxFiles || 20;
3787
3788    this._icons = {
3789      file: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.75391 10.5C7.04938 10.5 7.34196 10.4418 7.61494 10.3287C7.88793 10.2157 8.13597 10.0499 8.3449 9.84099C8.55383 9.63206 8.71956 9.38402 8.83264 9.11104C8.94571 8.83806 9.00391 8.54547 9.00391 8.25C9.00391 7.95453 8.94571 7.66194 8.83264 7.3889
37896C8.71956 7.11598 8.55383 6.86794 8.3449 6.65901C8.13597 6.45008 7.88793 6.28434 7.61494 6.17127C7.34196 6.0582 7.04938 6 6.75391 6C6.15717 6 5.58487 6.23705 5.16292 6.65901C4.74096 7.08097 4.50391 7.65326 4.50391 8.25C4.50391 8.84674 4.74096 9.41903 5.16292 9.84099C5.58487 10.2629 6.15717 10.5 6.75391 10.5Z" fill="currentColor"/><path d="M18 21C18 21.7956 17.6839 22.5587 17.1213 23.1213C16.5587 23.6839 15.7956 24 15 24H3C2.20435 24 1.44129 23.6839 0.87868 23.1213C0.31607 22.5587 0 21.7956 0 21V3C0 2.20435 0.31607 1.44129 0.87868 0.87868C1.44129 0.316071 2.20435 0 3 0L11.25 0L18 6.75V21ZM3 1.5C2.60218 1.5 2.22064 1.65804 1.93934 1.93934C1.65804 2.22064 1.5 2.60218 1.5 3V18L4.836 14.664C4.95422 14.5461 5.10843 14.4709 5.27417 14.4506C5.43992 14.4302 5.60773 14.4657 5.751 14.5515L9 16.5L12.2355 11.97C12.2988 11.8815 12.3806 11.8078 12.4753 11.754C12.5699 11.7003 12.6751 11.6678 12.7836 11.6588C12.8921 11.6498 13.0012 11.6645 13.1034 11.702C13.2056 11.7394 13.2985 11.7986 13.3755 11.8755L16.5 15V6.75H13.5C12.9033 6.75 12.331 6.51295 11.909 6.09099C11.4871 5.66903 11.25 5.09674 11.25 4.5V1.5H3Z" fill="currentColor"/></svg>',
3790      x: '<svg viewBox="0 0 8 8" width="8" height="8" xmlns="http://www.w3.org/2000/svg"><line x1="8" y1="0" x2="0" y2="8"/><line x1="0" y1="0" x2="8" y2="8"/></svg>',
3791    };
3792
3793    var inputEl = this._query("file-upload-input");
3794    if (!inputEl) {
3795      throw new Error("FileUpload: Nelze najít input[type=file] element");
3796    }
3797
3798    this.isMulti = inputEl.multiple;
3799    this._dragCounter = 0;
3800    this._eventsBound = false;
3801    this._handlers = {};
3802
3803    this._bindEvents();
3804  }
3805
3806  onError(callback) {
3807    this._onErrorCallback = callback;
3808  }
3809
3810  _query(role) {
3811    return this.root.querySelector('[data-role="' + role + '"]');
3812  }
3813
3814  _isDisabled() {
3815    var fileInput = this._query("file-upload-input");
3816    return fileInput ? fileInput.disabled : true;
3817  }
3818
3819  _getOrCreateFormText(id) {
3820    var existing = this.root.querySelector("p.form-text");
3821    if (existing) {
3822      existing.id = id;
3823      return existing;
3824    }
3825    var p = document.createElement("p");
3826    p.className = "form-text";
3827    p.id = id;
3828    p.setAttribute("role", "alert");
3829    p.setAttribute("aria-live", "assertive");
3830    var fileInput = this._query("file-upload-input");
3831    fileInput.insertAdjacentElement("afterend", p);
3832    return p;
3833  }
3834
3835  _removeFormText() {
3836    var p = this.root.querySelector("p.form-text");
3837    if (p) p.remove();
3838  }
3839
3840  _syncInput() {
3841    var fileInput = this._query("file-upload-input");
3842    var dt = new DataTransfer();
3843    for (var i = 0; i < this.files.length; i++) {
3844      dt.items.add(this.files[i]);
3845    }
3846    fileInput.files = dt.files;
3847  }
3848
3849  _bindEvents() {
3850    if (this._eventsBound) return;
3851    this._eventsBound = true;
3852
3853    var self = this;
3854    var dropZone = this._query("file-upload-drop-zone");
3855    var fileInput = this._query("file-upload-input");
3856    var btnSelect = this._query("file-upload-btn-select");
3857
3858    this._handlers.onBtnSelectClick = function (e) {
3859      if (self._isDisabled()) return;
3860      e.stopPropagation();
3861      fileInput.click();
3862    };
3863
3864    this._handlers.onDropZoneClick = function (e) {
3865      if (self._isDisabled()) return;
3866      if (e.target !== btnSelect && !btnSelect.contains(e.target)) {
3867        fileInput.click();
3868      }
3869    };
3870
3871    this._handlers.onInputChange = function () {
3872      if (self._isDisabled()) return;
3873      if (fileInput.files.length) {
3874        self._addFiles(Array.from(fileInput.files));
3875      }
3876    };
3877
3878    this._handlers.onDragEnter = function (e) {
3879      if (self._isDisabled()) return;
3880      e.preventDefault();
3881      self._dragCounter++;
3882      dropZone.classList.add("drag-over");
3883    };
3884
3885    this._handlers.onDragOver = function (e) {
3886      if (self._isDisabled()) return;
3887      e.preventDefault();
3888      e.dataTransfer.dropEffect = "copy";
3889    };
3890
3891    this._handlers.onDragLeave = function () {
3892      if (self._isDisabled()) return;
3893      if (--self._dragCounter <= 0) {
3894        self._dragCounter = 0;
3895        dropZone.classList.remove("drag-over");
3896      }
3897    };
3898
3899    this._handlers.onDrop = function (e) {
3900      if (self._isDisabled()) return;
3901      e.preventDefault();
3902      self._dragCounter = 0;
3903      dropZone.classList.remove("drag-over");
3904
3905      if (e.dataTransfer.files.length) {
3906        self._addFiles(Array.from(e.dataTransfer.files));
3907      }
3908    };
3909
3910    btnSelect.addEventListener("click", this._handlers.onBtnSelectClick);
3911    dropZone.addEventListener("click", this._handlers.onDropZoneClick);
3912    fileInput.addEventListener("change", this._handlers.onInputChange);
3913    dropZone.addEventListener("dragenter", this._handlers.onDragEnter);
3914    dropZone.addEventListener("dragover", this._handlers.onDragOver);
3915    dropZone.addEventListener("dragleave", this._handlers.onDragLeave);
3916    dropZone.addEventListener("drop", this._handlers.onDrop);
3917  }
3918
3919  destroy() {
3920    var dropZone = this._query("file-upload-drop-zone");
3921    var fileInput = this._query("file-upload-input");
3922    var btnSelect = this._query("file-upload-btn-select");
3923
3924    if (btnSelect) btnSelect.removeEventListener("click", this._handlers.onBtnSelectClick);
3925    if (dropZone) {
3926      dropZone.removeEventListener("click", this._handlers.onDropZoneClick);
3927      dropZone.removeEventListener("dragenter", this._handlers.onDragEnter);
3928      dropZone.removeEventListener("dragover", this._handlers.onDragOver);
3929      dropZone.removeEventListener("dragleave", this._handlers.onDragLeave);
3930      dropZone.removeEventListener("drop", this._handlers.onDrop);
3931    }
3932    if (fileInput) fileInput.removeEventListener("change", this._handlers.onInputChange);
3933
3934    this._eventsBound = false;
3935  }
3936
3937  _addFiles(newFiles) {
3938    var fileInput = this._query("file-upload-input");
3939    var allowedExts = fileInput.accept
3940      ? fileInput.accept.split(",").map(function (s) {
3941          return s.trim().toLowerCase();
3942        })
3943      : [];
3944
3945    var oversizedFiles = [];
3946    var validSizeFiles = [];
3947
3948    // 1. Kontrola maxSize
3949    for (var i = 0; i < newFiles.length; i++) {
3950      var f = newFiles[i];
3951
3952      if (allowedExts.length > 0) {
3953        var ext = f.name.substring(f.name.lastIndexOf(".")).toLowerCase();
3954        if (allowedExts.indexOf(ext) === -1) {
3955          continue;
3956        }
3957      }
3958
3959      if (f.size > this.maxSize) {
3960        oversizedFiles.push(f);
3961      } else {
3962        validSizeFiles.push(f);
3963      }
3964    }
3965
3966    // Chyba maxSize → zavolej callback a ZASTAV
3967    if (oversizedFiles.length > 0) {
3968      this.files = [];
3969      this._syncInput();
3970      this._render(); // překresli
3971
3972      if (this.onFiles) {
3973        this.onFiles([]);
3974      }
3975
3976      if (this._onErrorCallback) {
3977        this._onErrorCallback("maxSize", oversizedFiles);
3978      }
3979      return;
3980    }
3981
3982    if (validSizeFiles.length === 0) return;
3983
3984    // 2. Kontrola maxFiles
3985    var filesToAdd = [];
3986    var rejectedFiles = [];
3987
3988    if (!this.isMulti) {
3989      filesToAdd = [validSizeFiles[0]];
3990    } else {
3991      var uniqueNewFiles = [];
3992      for (var j = 0; j < validSizeFiles.length; j++) {
3993        var nf = validSizeFiles[j];
3994        var exists = this.files.some(function (e) {
3995          return e.name === nf.name && e.size === nf.size;
3996        });
3997        if (!exists) {
3998          uniqueNewFiles.push(nf);
3999        }
4000      }
4001
4002      var currentCount = this.files.length;
4003      for (var k = 0; k < uniqueNewFiles.length; k++) {
4004        if (currentCount + filesToAdd.length < this.maxFiles) {
4005          filesToAdd.push(uniqueNewFiles[k]);
4006        } else {
4007          rejectedFiles.push(uniqueNewFiles[k]);
4008        }
4009      }
4010    }
4011
4012    // Chyba maxFiles → zavolej callback a ZASTAV
4013
4014    if (rejectedFiles.length > 0) {
4015      this.files = [];
4016      this._syncInput();
4017      this._render();
4018      překresli;
4019
4020      if (this.onFiles) {
4021        this.onFiles([]);
4022      }
4023
4024      if (this._onErrorCallback) {
4025        this._onErrorCallback("maxFiles", rejectedFiles);
4026      }
4027      return;
4028    }
4029
4030    // Zápis schválených souborů do stavu
4031    if (!this.isMulti) {
4032      if (filesToAdd.length > 0) {
4033        this.files = [filesToAdd[0]];
4034      }
4035    } else {
4036      this.files = this.files.concat(filesToAdd);
4037    }
4038
4039    this._syncInput();
4040    this._render();
4041
4042    if (this.onFiles) {
4043      this.onFiles(this.getFiles());
4044    }
4045  }
4046
4047  _formatSize(bytes) {
4048    if (bytes < 1024) return bytes + " B";
4049    if (bytes < 1048576) return Math.round(bytes / 1024) + " KB";
4050    return (bytes / 1048576).toFixed(1) + " MB";
4051  }
4052
4053  _render() {
4054    var self = this;
4055    var fileList = this._query("file-upload-list");
4056    var fileSection = this._query("file-upload-section");
4057
4058    fileList.innerHTML = "";
4059
4060    if (this.files.length === 0) {
4061      fileSection.style.display = "none";
4062      return;
4063    }
4064
4065    fileSection.style.display = "block";
4066
4067    var row = document.createElement("div");
4068    row.className = "file-upload-summary";
4069
4070    var icon = document.createElement("span");
4071    icon.className = "file-upload-summary-icon";
4072    icon.setAttribute("aria-hidden", "true");
4073    icon.innerHTML = this._icons.file;
4074
4075    var text = document.createElement("p");
4076    text.className = "file-upload-summary-text";
4077    text.textContent = "Počet nahraných souborů: " + this.files.length;
4078
4079    const isCzech = (Liferay?.ThemeDisplay?.getLanguageId?.() ?? "cs_CZ") === "cs_CZ";
4080    var remove = document.createElement("button");
4081    remove.className = "file-upload-btn-remove";
4082    remove.type = "button";
4083    remove.setAttribute("aria-label", isCzech ? "Odebrat všechny soubory" : "Remove all files");
4084    remove.innerHTML = this._icons.x;
4085
4086    remove.addEventListener("click", function () {
4087      self.files = [];
4088      self._syncInput();
4089      self._render();
4090
4091      if (self.onFiles) {
4092        self.onFiles([]);
4093      }
4094    });
4095
4096    row.appendChild(icon);
4097    row.appendChild(text);
4098    row.appendChild(remove);
4099
4100    fileList.appendChild(row);
4101  }
4102
4103  getFiles() {
4104    return this.files.slice();
4105  }
4106
4107  clear() {
4108    this.files = [];
4109    this._syncInput();
4110    this._render();
4111  }
4112
4113  set(prop, value) {
4114    var fileInput = this._query("file-upload-input");
4115    var btnSelect = this._query("file-upload-btn-select");
4116    var formGroup = this.root;
4117
4118    if (prop === "disabled") {
4119      var isDisabled = Boolean(value);
4120      fileInput.disabled = isDisabled;
4121      if (btnSelect) btnSelect.disabled = isDisabled;
4122      formGroup.classList.toggle("disabled", isDisabled);
4123      return;
4124    }
4125
4126    if (prop === "error" || prop === "message") {
4127      var inputId = fileInput.id || (this.root.id ? this.root.id + "-input" : "file-upload-input");
4128      var pId = "error-" + inputId;
4129      var ariaTarget = btnSelect;
4130
4131      if (value && value !== "") {
4132        if (prop === "error") {
4133          ariaTarget.setAttribute("aria-invalid", "true");
4134          formGroup.classList.add("has-error");
4135        }
4136        ariaTarget.setAttribute("aria-describedby", pId);
4137        this._getOrCreateFormText(pId).textContent = value;
4138      } else {
4139        if (prop === "error") {
4140          ariaTarget.removeAttribute("aria-invalid");
4141          formGroup.classList.remove("has-error");
4142        }
4143        ariaTarget.removeAttribute("aria-describedby");
4144        this._removeFormText();
4145      }
4146      return;
4147    }
4148
4149    console.warn('FileUpload.set: neznámá vlastnost "' + prop + '"');
4150  }
4151}

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.