Files

192 lines
6.2 KiB
JavaScript

export function initProjectsCarousel() {
const root = document.querySelector("[data-projects-carousel]");
if (!root) return;
const viewport = root.querySelector("[data-projects-viewport]");
const prevBtn = root.parentElement?.querySelector("[data-projects-prev]");
const nextBtn = root.parentElement?.querySelector("[data-projects-next]");
if (!viewport || !prevBtn || !nextBtn) return;
const edgePrevBtn = root.querySelector("[data-projects-edge-prev]");
const edgeNextBtn = root.querySelector("[data-projects-edge-next]");
let dragState = null;
let suppressClick = false;
function syncViewportWidthVar() {
const visibleWidth = document.documentElement.clientWidth || window.innerWidth;
root.style.setProperty("--projects-viewport-width", `${Math.max(visibleWidth, 0)}px`);
}
function getEdgeSpace() {
const raw = getComputedStyle(root).getPropertyValue("--projects-edge-space");
const value = Number.parseFloat(raw);
return Number.isFinite(value) ? Math.max(value, 0) : 0;
}
function getMinScroll() {
return getEdgeSpace();
}
function getMaxScroll() {
const maxNative = Math.max(viewport.scrollWidth - viewport.clientWidth, 0);
return Math.max(maxNative - getEdgeSpace(), getMinScroll());
}
function clampScroll(left) {
return Math.min(Math.max(left, getMinScroll()), getMaxScroll());
}
function getStep() {
const firstItem = viewport.querySelector(".projects-carousel__item");
if (!(firstItem instanceof HTMLElement)) return viewport.clientWidth;
const track = viewport.querySelector(".projects-carousel__track");
const gapRaw = track ? getComputedStyle(track).columnGap : "0";
const gap = Number.parseFloat(gapRaw) || 0;
return firstItem.getBoundingClientRect().width + gap;
}
function getSnapIndex() {
const step = getStep();
if (!step) return 0;
const min = getMinScroll();
return Math.round((viewport.scrollLeft - min) / step);
}
function scrollToIndex(index, behavior = "smooth") {
const step = getStep();
const min = getMinScroll();
const max = getMaxScroll();
if (!step) {
viewport.scrollTo({ left: clampScroll(viewport.scrollLeft), behavior });
return;
}
const maxIndex = Math.max(Math.round((max - min) / step), 0);
const nextIndex = Math.min(Math.max(index, 0), maxIndex);
const target = clampScroll(min + nextIndex * step);
viewport.scrollTo({ left: target, behavior });
}
function snapToNearest(behavior = "smooth") {
scrollToIndex(getSnapIndex(), behavior);
}
function updateControls() {
const x = viewport.scrollLeft;
const min = getMinScroll();
const max = getMaxScroll();
prevBtn.disabled = x <= min + 2;
nextBtn.disabled = x >= max - 2;
}
function scrollByStep(direction) {
const currentIndex = getSnapIndex();
scrollToIndex(currentIndex + direction, "smooth");
}
function getEdgeHitZone() {
const sectionInner = root.closest(".section__inner");
const sectionInnerWidth = sectionInner instanceof HTMLElement ? sectionInner.getBoundingClientRect().width : viewport.clientWidth;
const viewportWidthWithScrollbar = window.innerWidth || document.documentElement.clientWidth || viewport.clientWidth;
const track = viewport.querySelector(".projects-carousel__track");
const gapRaw = track ? getComputedStyle(track).columnGap : "0";
const gap = Number.parseFloat(gapRaw) || 0;
const zone = (viewportWidthWithScrollbar - sectionInnerWidth) / 2 - gap;
return Math.max(zone, 0);
}
prevBtn.addEventListener("click", () => scrollByStep(-1));
nextBtn.addEventListener("click", () => scrollByStep(1));
edgePrevBtn?.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
scrollByStep(-1);
});
edgeNextBtn?.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
scrollByStep(1);
});
viewport.addEventListener("scroll", updateControls, { passive: true });
window.addEventListener("resize", updateControls, { passive: true });
viewport.querySelectorAll("a, img").forEach((el) => {
el.setAttribute("draggable", "false");
});
viewport.addEventListener("dragstart", (e) => {
e.preventDefault();
});
viewport.addEventListener("pointerdown", (e) => {
if (e.button !== 0) return;
if (!(e.target instanceof Element)) return;
suppressClick = false;
dragState = {
pointerId: e.pointerId,
startX: e.clientX,
startScrollLeft: viewport.scrollLeft,
moved: false,
};
viewport.classList.add("is-dragging");
});
viewport.addEventListener("pointermove", (e) => {
if (!dragState || dragState.pointerId !== e.pointerId) return;
const dx = e.clientX - dragState.startX;
if (Math.abs(dx) > 8) dragState.moved = true;
if (dragState.moved) e.preventDefault();
viewport.scrollLeft = clampScroll(dragState.startScrollLeft - dx);
});
function finishDrag(e) {
if (!dragState || dragState.pointerId !== e.pointerId) return;
if (dragState.moved) {
e.preventDefault();
suppressClick = true;
snapToNearest("smooth");
} else {
suppressClick = false;
}
viewport.classList.remove("is-dragging");
dragState = null;
}
viewport.addEventListener("pointerup", finishDrag);
viewport.addEventListener("pointercancel", finishDrag);
viewport.addEventListener("pointerleave", (e) => {
if (!dragState) return;
finishDrag(e);
});
viewport.addEventListener(
"click",
(e) => {
if (suppressClick) {
e.preventDefault();
e.stopPropagation();
suppressClick = false;
}
},
true,
);
function alignToStart() {
syncViewportWidthVar();
const edgeZone = `${getEdgeHitZone()}px`;
root.style.setProperty("--projects-edge-hit-zone", edgeZone);
viewport.scrollLeft = getMinScroll();
updateControls();
}
alignToStart();
window.addEventListener("load", alignToStart, { once: true });
window.addEventListener(
"resize",
() => {
syncViewportWidthVar();
snapToNearest("auto");
updateControls();
},
{ passive: true },
);
}