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