UNPKG

@shoelace-style/shoelace

Version:

A forward-thinking library of web components.

559 lines (550 loc) 18.8 kB
import { clamp } from "./chunk.HF7GESMZ.js"; import { AutoplayController } from "./chunk.F4VGSDIW.js"; import { carousel_styles_default } from "./chunk.HUJPN4KF.js"; import { waitForEvent } from "./chunk.B4BZKR24.js"; import { prefersReducedMotion } from "./chunk.RV6ECJES.js"; import { LocalizeController } from "./chunk.NH3SRVOC.js"; import { SlIcon } from "./chunk.F24SGBDC.js"; import { e } from "./chunk.UZVKBFXH.js"; import { watch } from "./chunk.SJGTYGCD.js"; import { component_styles_default } from "./chunk.K23QWHWK.js"; import { ShoelaceElement, e as e2, n, r, t } from "./chunk.QAHCGRIS.js"; import { x } from "./chunk.CXZZ2LVK.js"; import { __decorateClass } from "./chunk.625AWUY7.js"; // node_modules/lit-html/directives/map.js function* o(o3, f) { if (void 0 !== o3) { let i = 0; for (const t2 of o3) yield f(t2, i++); } } // node_modules/lit-html/directives/range.js function* o2(o3, t2, e3 = 1) { const i = void 0 === t2 ? 0 : o3; t2 != null ? t2 : t2 = o3; for (let o4 = i; e3 > 0 ? o4 < t2 : t2 < o4; o4 += e3) yield o4; } // src/components/carousel/carousel.component.ts var SlCarousel = class extends ShoelaceElement { constructor() { super(...arguments); this.loop = false; this.navigation = false; this.pagination = false; this.autoplay = false; this.autoplayInterval = 3e3; this.slidesPerPage = 1; this.slidesPerMove = 1; this.orientation = "horizontal"; this.mouseDragging = false; this.activeSlide = 0; this.scrolling = false; this.dragging = false; this.autoplayController = new AutoplayController(this, () => this.next()); this.localize = new LocalizeController(this); this.pendingSlideChange = false; this.handleMouseDrag = (event) => { if (!this.dragging) { this.scrollContainer.style.setProperty("scroll-snap-type", "none"); this.dragging = true; } this.scrollContainer.scrollBy({ left: -event.movementX, top: -event.movementY, behavior: "instant" }); }; this.handleMouseDragEnd = () => { const scrollContainer = this.scrollContainer; document.removeEventListener("pointermove", this.handleMouseDrag, { capture: true }); const startLeft = scrollContainer.scrollLeft; const startTop = scrollContainer.scrollTop; scrollContainer.style.removeProperty("scroll-snap-type"); scrollContainer.style.setProperty("overflow", "hidden"); const finalLeft = scrollContainer.scrollLeft; const finalTop = scrollContainer.scrollTop; scrollContainer.style.removeProperty("overflow"); scrollContainer.style.setProperty("scroll-snap-type", "none"); scrollContainer.scrollTo({ left: startLeft, top: startTop, behavior: "instant" }); requestAnimationFrame(async () => { if (startLeft !== finalLeft || startTop !== finalTop) { scrollContainer.scrollTo({ left: finalLeft, top: finalTop, behavior: prefersReducedMotion() ? "auto" : "smooth" }); await waitForEvent(scrollContainer, "scrollend"); } scrollContainer.style.removeProperty("scroll-snap-type"); this.dragging = false; this.handleScrollEnd(); }); }; this.handleSlotChange = (mutations) => { const needsInitialization = mutations.some( (mutation) => [...mutation.addedNodes, ...mutation.removedNodes].some( (el) => this.isCarouselItem(el) && !el.hasAttribute("data-clone") ) ); if (needsInitialization) { this.initializeSlides(); } this.requestUpdate(); }; } connectedCallback() { super.connectedCallback(); this.setAttribute("role", "region"); this.setAttribute("aria-label", this.localize.term("carousel")); } disconnectedCallback() { var _a; super.disconnectedCallback(); (_a = this.mutationObserver) == null ? void 0 : _a.disconnect(); } firstUpdated() { this.initializeSlides(); this.mutationObserver = new MutationObserver(this.handleSlotChange); this.mutationObserver.observe(this, { childList: true, subtree: true }); } willUpdate(changedProperties) { if (changedProperties.has("slidesPerMove") || changedProperties.has("slidesPerPage")) { this.slidesPerMove = Math.min(this.slidesPerMove, this.slidesPerPage); } } getPageCount() { const slidesCount = this.getSlides().length; const { slidesPerPage, slidesPerMove, loop } = this; const pages = loop ? slidesCount / slidesPerMove : (slidesCount - slidesPerPage) / slidesPerMove + 1; return Math.ceil(pages); } getCurrentPage() { return Math.ceil(this.activeSlide / this.slidesPerMove); } canScrollNext() { return this.loop || this.getCurrentPage() < this.getPageCount() - 1; } canScrollPrev() { return this.loop || this.getCurrentPage() > 0; } /** @internal Gets all carousel items. */ getSlides({ excludeClones = true } = {}) { return [...this.children].filter( (el) => this.isCarouselItem(el) && (!excludeClones || !el.hasAttribute("data-clone")) ); } handleKeyDown(event) { if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) { const target = event.target; const isRtl = this.localize.dir() === "rtl"; const isFocusInPagination = target.closest('[part~="pagination-item"]') !== null; const isNext = event.key === "ArrowDown" || !isRtl && event.key === "ArrowRight" || isRtl && event.key === "ArrowLeft"; const isPrevious = event.key === "ArrowUp" || !isRtl && event.key === "ArrowLeft" || isRtl && event.key === "ArrowRight"; event.preventDefault(); if (isPrevious) { this.previous(); } if (isNext) { this.next(); } if (event.key === "Home") { this.goToSlide(0); } if (event.key === "End") { this.goToSlide(this.getSlides().length - 1); } if (isFocusInPagination) { this.updateComplete.then(() => { var _a; const activePaginationItem = (_a = this.shadowRoot) == null ? void 0 : _a.querySelector( '[part~="pagination-item--active"]' ); if (activePaginationItem) { activePaginationItem.focus(); } }); } } } handleMouseDragStart(event) { const canDrag = this.mouseDragging && event.button === 0; if (canDrag) { event.preventDefault(); document.addEventListener("pointermove", this.handleMouseDrag, { capture: true, passive: true }); document.addEventListener("pointerup", this.handleMouseDragEnd, { capture: true, once: true }); } } handleScroll() { this.scrolling = true; if (!this.pendingSlideChange) { this.synchronizeSlides(); } } /** @internal Synchronizes the slides with the IntersectionObserver API. */ synchronizeSlides() { const io = new IntersectionObserver( (entries) => { io.disconnect(); for (const entry of entries) { const slide = entry.target; slide.toggleAttribute("inert", !entry.isIntersecting); slide.classList.toggle("--in-view", entry.isIntersecting); slide.setAttribute("aria-hidden", entry.isIntersecting ? "false" : "true"); } const firstIntersecting = entries.find((entry) => entry.isIntersecting); if (!firstIntersecting) { return; } const slidesWithClones = this.getSlides({ excludeClones: false }); const slidesCount = this.getSlides().length; const slideIndex = slidesWithClones.indexOf(firstIntersecting.target); const normalizedIndex = this.loop ? slideIndex - this.slidesPerPage : slideIndex; this.activeSlide = (Math.ceil(normalizedIndex / this.slidesPerMove) * this.slidesPerMove + slidesCount) % slidesCount; if (!this.scrolling) { if (this.loop && firstIntersecting.target.hasAttribute("data-clone")) { const clonePosition = Number(firstIntersecting.target.getAttribute("data-clone")); this.goToSlide(clonePosition, "instant"); } } }, { root: this.scrollContainer, threshold: 0.6 } ); this.getSlides({ excludeClones: false }).forEach((slide) => { io.observe(slide); }); } handleScrollEnd() { if (!this.scrolling || this.dragging) return; this.scrolling = false; this.pendingSlideChange = false; this.synchronizeSlides(); } isCarouselItem(node) { return node instanceof Element && node.tagName.toLowerCase() === "sl-carousel-item"; } initializeSlides() { this.getSlides({ excludeClones: false }).forEach((slide, index) => { slide.classList.remove("--in-view"); slide.classList.remove("--is-active"); slide.setAttribute("aria-label", this.localize.term("slideNum", index + 1)); if (slide.hasAttribute("data-clone")) { slide.remove(); } }); this.updateSlidesSnap(); if (this.loop) { this.createClones(); } this.synchronizeSlides(); this.goToSlide(this.activeSlide, "auto"); } createClones() { const slides = this.getSlides(); const slidesPerPage = this.slidesPerPage; const lastSlides = slides.slice(-slidesPerPage); const firstSlides = slides.slice(0, slidesPerPage); lastSlides.reverse().forEach((slide, i) => { const clone = slide.cloneNode(true); clone.setAttribute("data-clone", String(slides.length - i - 1)); this.prepend(clone); }); firstSlides.forEach((slide, i) => { const clone = slide.cloneNode(true); clone.setAttribute("data-clone", String(i)); this.append(clone); }); } handleSlideChange() { const slides = this.getSlides(); slides.forEach((slide, i) => { slide.classList.toggle("--is-active", i === this.activeSlide); }); if (this.hasUpdated) { this.emit("sl-slide-change", { detail: { index: this.activeSlide, slide: slides[this.activeSlide] } }); } } updateSlidesSnap() { const slides = this.getSlides(); const slidesPerMove = this.slidesPerMove; slides.forEach((slide, i) => { const shouldSnap = (i + slidesPerMove) % slidesPerMove === 0; if (shouldSnap) { slide.style.removeProperty("scroll-snap-align"); } else { slide.style.setProperty("scroll-snap-align", "none"); } }); } handleAutoplayChange() { this.autoplayController.stop(); if (this.autoplay) { this.autoplayController.start(this.autoplayInterval); } } /** * Move the carousel backward by `slides-per-move` slides. * * @param behavior - The behavior used for scrolling. */ previous(behavior = "smooth") { this.goToSlide(this.activeSlide - this.slidesPerMove, behavior); } /** * Move the carousel forward by `slides-per-move` slides. * * @param behavior - The behavior used for scrolling. */ next(behavior = "smooth") { this.goToSlide(this.activeSlide + this.slidesPerMove, behavior); } /** * Scrolls the carousel to the slide specified by `index`. * * @param index - The slide index. * @param behavior - The behavior used for scrolling. */ goToSlide(index, behavior = "smooth") { const { slidesPerPage, loop } = this; const slides = this.getSlides(); const slidesWithClones = this.getSlides({ excludeClones: false }); if (!slides.length) { return; } const newActiveSlide = loop ? (index + slides.length) % slides.length : clamp(index, 0, slides.length - slidesPerPage); this.activeSlide = newActiveSlide; const isRtl = this.localize.dir() === "rtl"; const nextSlideIndex = clamp( index + (loop ? slidesPerPage : 0) + (isRtl ? slidesPerPage - 1 : 0), 0, slidesWithClones.length - 1 ); const nextSlide = slidesWithClones[nextSlideIndex]; this.scrollToSlide(nextSlide, prefersReducedMotion() ? "auto" : behavior); } scrollToSlide(slide, behavior = "smooth") { const scrollContainer = this.scrollContainer; const scrollContainerRect = scrollContainer.getBoundingClientRect(); const nextSlideRect = slide.getBoundingClientRect(); const nextLeft = nextSlideRect.left - scrollContainerRect.left; const nextTop = nextSlideRect.top - scrollContainerRect.top; if (nextLeft || nextTop) { this.pendingSlideChange = true; scrollContainer.scrollTo({ left: nextLeft + scrollContainer.scrollLeft, top: nextTop + scrollContainer.scrollTop, behavior }); } } render() { const { slidesPerMove, scrolling } = this; const pagesCount = this.getPageCount(); const currentPage = this.getCurrentPage(); const prevEnabled = this.canScrollPrev(); const nextEnabled = this.canScrollNext(); const isLtr = this.localize.dir() === "rtl"; return x` <div part="base" class="carousel"> <div id="scroll-container" part="scroll-container" class="${e({ carousel__slides: true, "carousel__slides--horizontal": this.orientation === "horizontal", "carousel__slides--vertical": this.orientation === "vertical", "carousel__slides--dragging": this.dragging })}" style="--slides-per-page: ${this.slidesPerPage};" aria-busy="${scrolling ? "true" : "false"}" aria-atomic="true" tabindex="0" @keydown=${this.handleKeyDown} @mousedown="${this.handleMouseDragStart}" @scroll="${this.handleScroll}" @scrollend=${this.handleScrollEnd} > <slot></slot> </div> ${this.navigation ? x` <div part="navigation" class="carousel__navigation"> <button part="navigation-button navigation-button--previous" class="${e({ "carousel__navigation-button": true, "carousel__navigation-button--previous": true, "carousel__navigation-button--disabled": !prevEnabled })}" aria-label="${this.localize.term("previousSlide")}" aria-controls="scroll-container" aria-disabled="${prevEnabled ? "false" : "true"}" @click=${prevEnabled ? () => this.previous() : null} > <slot name="previous-icon"> <sl-icon library="system" name="${isLtr ? "chevron-left" : "chevron-right"}"></sl-icon> </slot> </button> <button part="navigation-button navigation-button--next" class=${e({ "carousel__navigation-button": true, "carousel__navigation-button--next": true, "carousel__navigation-button--disabled": !nextEnabled })} aria-label="${this.localize.term("nextSlide")}" aria-controls="scroll-container" aria-disabled="${nextEnabled ? "false" : "true"}" @click=${nextEnabled ? () => this.next() : null} > <slot name="next-icon"> <sl-icon library="system" name="${isLtr ? "chevron-right" : "chevron-left"}"></sl-icon> </slot> </button> </div> ` : ""} ${this.pagination ? x` <div part="pagination" role="tablist" class="carousel__pagination" aria-controls="scroll-container"> ${o(o2(pagesCount), (index) => { const isActive = index === currentPage; return x` <button part="pagination-item ${isActive ? "pagination-item--active" : ""}" class="${e({ "carousel__pagination-item": true, "carousel__pagination-item--active": isActive })}" role="tab" aria-selected="${isActive ? "true" : "false"}" aria-label="${this.localize.term("goToSlide", index + 1, pagesCount)}" tabindex=${isActive ? "0" : "-1"} @click=${() => this.goToSlide(index * slidesPerMove)} @keydown=${this.handleKeyDown} ></button> `; })} </div> ` : ""} </div> `; } }; SlCarousel.styles = [component_styles_default, carousel_styles_default]; SlCarousel.dependencies = { "sl-icon": SlIcon }; __decorateClass([ n({ type: Boolean, reflect: true }) ], SlCarousel.prototype, "loop", 2); __decorateClass([ n({ type: Boolean, reflect: true }) ], SlCarousel.prototype, "navigation", 2); __decorateClass([ n({ type: Boolean, reflect: true }) ], SlCarousel.prototype, "pagination", 2); __decorateClass([ n({ type: Boolean, reflect: true }) ], SlCarousel.prototype, "autoplay", 2); __decorateClass([ n({ type: Number, attribute: "autoplay-interval" }) ], SlCarousel.prototype, "autoplayInterval", 2); __decorateClass([ n({ type: Number, attribute: "slides-per-page" }) ], SlCarousel.prototype, "slidesPerPage", 2); __decorateClass([ n({ type: Number, attribute: "slides-per-move" }) ], SlCarousel.prototype, "slidesPerMove", 2); __decorateClass([ n() ], SlCarousel.prototype, "orientation", 2); __decorateClass([ n({ type: Boolean, reflect: true, attribute: "mouse-dragging" }) ], SlCarousel.prototype, "mouseDragging", 2); __decorateClass([ e2(".carousel__slides") ], SlCarousel.prototype, "scrollContainer", 2); __decorateClass([ e2(".carousel__pagination") ], SlCarousel.prototype, "paginationContainer", 2); __decorateClass([ r() ], SlCarousel.prototype, "activeSlide", 2); __decorateClass([ r() ], SlCarousel.prototype, "scrolling", 2); __decorateClass([ r() ], SlCarousel.prototype, "dragging", 2); __decorateClass([ t({ passive: true }) ], SlCarousel.prototype, "handleScroll", 1); __decorateClass([ watch("loop", { waitUntilFirstUpdate: true }), watch("slidesPerPage", { waitUntilFirstUpdate: true }) ], SlCarousel.prototype, "initializeSlides", 1); __decorateClass([ watch("activeSlide") ], SlCarousel.prototype, "handleSlideChange", 1); __decorateClass([ watch("slidesPerMove") ], SlCarousel.prototype, "updateSlidesSnap", 1); __decorateClass([ watch("autoplay") ], SlCarousel.prototype, "handleAutoplayChange", 1); export { SlCarousel }; /*! Bundled license information: lit-html/directives/map.js: (** * @license * Copyright 2021 Google LLC * SPDX-License-Identifier: BSD-3-Clause *) lit-html/directives/range.js: (** * @license * Copyright 2021 Google LLC * SPDX-License-Identifier: BSD-3-Clause *) */