@shoelace-style/shoelace
Version:
A forward-thinking library of web components.
559 lines (550 loc) • 18.8 kB
JavaScript
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"
=${this.handleKeyDown}
="${this.handleMouseDragStart}"
="${this.handleScroll}"
=${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"}"
=${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"}"
=${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"}
=${() => this.goToSlide(index * slidesPerMove)}
=${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
*)
*/