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.