import { Component } from "/js/components/component.js";
import { html, render } from "/js/lib/lit-html.js";
import { isSafari } from "/js/utils.js";
import { cdnImageUrl } from "/js/dataHelpers.js";
import "/js/components/lightbox-image-group.js";
import "/js/components/app-icon.js";
const MIN_ASPECT_RATIO = 2 / 3;
const MAX_ASPECT_RATIO = 3 / 2;
const ITEM_GAP_PX = 8;
function clampAspectRatio(rawRatio) {
if (!Number.isFinite(rawRatio) || rawRatio <= 0) {
return { ratio: 1, isCropped: false };
}
const ratio = Math.min(
MAX_ASPECT_RATIO,
Math.max(MIN_ASPECT_RATIO, rawRatio),
);
return { ratio, isCropped: ratio !== rawRatio };
}
export class ImageCarousel extends Component {
connectedCallback() {
if (this._initialized) return;
this._initialized = true;
this.currentIndex = 0;
if (!Array.isArray(this._images)) this._images = [];
this.render();
this._scroller = this.querySelector(".carousel-scroller");
this._disposers = [];
if (this._scroller) {
this._disposers.push(this._bindScroll(), this._bindKeyboard());
if (isSafari()) {
this._disposers.push(this._bindWheel());
}
}
}
disconnectedCallback() {
this._disposers?.forEach((dispose) => dispose());
this._disposers = [];
}
set images(value) {
this._images = Array.isArray(value) ? value : [];
if (this._initialized) {
this.currentIndex = 0;
this.render();
}
}
get images() {
return this._images ?? [];
}
get _slides() {
return this.querySelectorAll('[data-testid="carousel-slide"]');
}
render() {
const numImages = this.images.length;
render(
html`
`,
this,
);
}
_openLightbox(index) {
const images = Array.from(this.querySelectorAll("img.carousel-image"));
const lightboxDialog = document.createElement("lightbox-dialog");
lightboxDialog.images = images;
lightboxDialog.currentIndex = index;
lightboxDialog.addEventListener("close", () => {
lightboxDialog.remove();
});
document.body.appendChild(lightboxDialog);
lightboxDialog.open();
}
_getSlideWidths() {
return Array.from(this._slides, (slide) => slide.offsetWidth);
}
_getOffsetForIndex(index) {
const widths = this._getSlideWidths();
let offset = 0;
for (let i = 0; i < index; i += 1) {
offset += widths[i] + ITEM_GAP_PX;
}
return offset;
}
_getActiveIndexFromScroll() {
const widths = this._getSlideWidths();
const scrollLeft = this._scroller.scrollLeft;
let cumulative = 0;
let bestIndex = 0;
let bestDistance = Infinity;
for (let i = 0; i < widths.length; i += 1) {
const distance = Math.abs(cumulative - scrollLeft);
if (distance < bestDistance) {
bestDistance = distance;
bestIndex = i;
}
cumulative += widths[i] + ITEM_GAP_PX;
}
return bestIndex;
}
_updateCurrentIndex(index) {
if (index === this.currentIndex) return;
this.currentIndex = index;
this.render();
}
_bindScroll() {
const onScrollEnd = () => {
const index = this._getActiveIndexFromScroll();
this._updateCurrentIndex(index);
};
this._scroller.addEventListener("scrollend", onScrollEnd);
return () => this._scroller.removeEventListener("scrollend", onScrollEnd);
}
_bindKeyboard() {
const onKeyDown = (event) => {
if (event.key !== "ArrowRight" && event.key !== "ArrowLeft") return;
if (!this.contains(document.activeElement)) return;
event.preventDefault();
const offset = event.key === "ArrowRight" ? 1 : -1;
const nextIndex = Math.max(
0,
Math.min(this.images.length - 1, this.currentIndex + offset),
);
if (nextIndex === this.currentIndex) return;
this._updateCurrentIndex(nextIndex);
this._scroller.scrollTo({
left: this._getOffsetForIndex(nextIndex),
behavior: "smooth",
});
const slide = this._slides[nextIndex];
if (slide) slide.focus({ preventScroll: true });
};
this.addEventListener("keydown", onKeyDown);
return () => this.removeEventListener("keydown", onKeyDown);
}
_bindWheel() {
const onWheel = (event) => {
if (Math.abs(event.deltaX) <= Math.abs(event.deltaY)) return;
event.preventDefault();
this._scroller.scrollLeft += event.deltaX;
this._updateCurrentIndex(this._getActiveIndexFromScroll());
};
this._scroller.addEventListener("wheel", onWheel, { passive: false });
return () => this._scroller.removeEventListener("wheel", onWheel);
}
}
ImageCarousel.register();