192 lines
6.2 KiB
JavaScript
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 },
|
|
);
|
|
}
|