@blossom-carousel/web
Version:
A native-scroll-first carousel component for the web.
405 lines (404 loc) • 13.3 kB
JavaScript
function pe() {
return {
scroller: null,
end: 300,
isDragging: !1,
scrollerScrollWidth: 300,
scrollerWidth: 300,
scrollerScrollHeight: 300,
scrollerHeight: 300,
padding: { start: 0, end: 0 },
scrollPadding: { start: 0, end: 0 },
slidePositions: [],
hasSnap: !1,
dir: 1
};
}
function xe(e, r, t) {
return (1 - t) * e + t * r;
}
function S(e, r, t, l) {
return xe(e, r, 1 - Math.exp(Math.log(1 - t) * (l / (1e3 / 60))));
}
function N(e, r, t) {
return Math.max(r, Math.min(t, e));
}
function T(e, r = 0) {
const t = Math.pow(10, r);
return Math.round(e * t) / t;
}
function ve(e, r, t) {
return e + r / (1 - t);
}
const v = 0.72, k = 0.12, Z = new Event("scrollend", {
bubbles: !0,
cancelable: !0
}), B = (e, r, t) => {
const l = t ? new CustomEvent(r, { bubbles: !0, cancelable: !0, detail: t }) : new Event(r, { bubbles: !0, cancelable: !0 });
return e?.dispatchEvent(l), l;
}, C = (e, r) => B(e, "overscroll", r), ye = (e) => (e?.dispatchEvent(Z), Z), we = (e, r) => B(e, "scrollsnapchange", r), K = (e, r) => B(e, "scrollsnapchanging", r);
function me() {
return {
positions: [],
activePosition: { target: null, x: 0, y: 0 }
};
}
function be(e, r, t) {
let l = [], c = 0;
const i = (d) => {
if (c++, c > 100) return;
const h = window.getComputedStyle(d).scrollSnapAlign;
if (h !== "none") {
l.push({
align: h,
el: d
});
return;
}
const g = d.children;
if (g.length !== 0)
for (let y of Array.from(g))
i(y);
};
i(e);
const a = e.getBoundingClientRect(), u = l.map(({ el: d, align: h }, g) => {
const y = d.getBoundingClientRect(), b = d.clientWidth, w = y.left - a.left + e.scrollLeft;
switch (h) {
case "start":
return {
target: d,
x: w - r.scrollPadding.start,
y: 0
};
case "end":
return {
target: d,
x: w + b - r.scrollerWidth + r.scrollPadding.end,
y: 0
};
case "center":
return {
target: d,
x: w + b * 0.5 - r.scrollerWidth / 2,
y: 0
};
default:
return null;
}
}).filter((d) => d !== null).reduce((d, h) => ((d.length === 0 || d[d.length - 1].x !== h.x) && d.push(h), d), []);
t.positions = u;
}
function Pe(e, r, t, l, c) {
const i = ee(
e,
r,
t,
l,
c
);
return i.x !== c.activePosition.x && K(l.scroller, {
snapTargetInline: (i || c.activePosition).target,
snapTargetBlock: (i || c.activePosition).target
}), c.activePosition = i, (N(
i.x,
Math.min((l.scrollerScrollWidth - l.scrollerWidth) * l.dir, 0),
Math.max((l.scrollerScrollWidth - l.scrollerWidth) * l.dir, 0)
) - e) * (1 - v) * (1 / v);
}
function ee(e, r, t, l, c) {
const i = ve(e, r, t);
return c.positions.length ? c.positions.reduce(
(a, u) => Math.abs(u.x - i) < Math.abs(a.x - i) ? u : a
) : {
target: null,
x: N(i, Math.min(l.end, 0), Math.max(l.end, 0)),
y: 0
};
}
function Ee(e, r) {
we(e.scroller, {
snapTargetInline: r.activePosition.target,
snapTargetBlock: r.activePosition.target
});
}
function We(e, r, t, l, c) {
const i = ee(
e,
r,
t,
l,
c
);
i.x !== c.activePosition.x && (c.activePosition = i, K(l.scroller, {
snapTargetInline: (i || c.activePosition).target,
snapTargetBlock: (i || c.activePosition).target
}));
}
const Le = {
center: (e, r, t) => e + r * 0.5 - t.scrollerWidth / 2,
end: (e, r, t) => e + r - t.scrollerWidth + t.scrollPadding.end,
start: (e, r, t) => e - t.scrollPadding.start
};
function _(e, r, t, l) {
if (l.hasSnap) return e;
const c = Le[t || "start"];
return c(e, r, l);
}
function te(e, r, t, l) {
if (!t.scroller) return 0;
const c = t.scroller.scrollLeft, i = l.positions.length ? l.positions : t.slidePositions;
if (e === "prev")
for (let a = i.length - 1; a >= 0; a--) {
const u = _(
i[a].x,
i[a].width || 0,
r,
t
);
if (u < c - 1) return u;
}
else
for (let a = 0; a < i.length; a++) {
const u = _(
i[a].x,
i[a].width || 0,
r,
t
);
if (u > c + 1) return u;
}
return 0;
}
function Se(e, r, { align: t } = {}) {
const l = te("prev", t, e, r);
l && e.scroller.scrollTo({ left: l, behavior: "smooth" });
}
function Ae(e, r, { align: t } = {}) {
const l = te("next", t, e, r);
l && e.scroller.scrollTo({ left: l, behavior: "smooth" });
}
const Ie = (e, r) => {
const t = pe(), l = me();
t.scroller = e;
let c = !0;
const i = { x: 0, y: 0 }, a = { x: 0, y: 0 }, u = { x: 0, y: 0 }, d = new Proxy(
{ x: 0, y: 0 },
{
set(n, o, s) {
return n[o] === s || (n[o] = s, (n.x >= 10 || n.y >= 10) && (x.value = !0)), !0;
}
}
), h = new Proxy(
{ x: !1, y: !1 },
{
set(n, o, s) {
return n[o] === s || (n[o] = s, n.x || n.y ? (e.setAttribute("has-overflow", "true"), e.addEventListener("pointerdown", F), e.addEventListener("wheel", O, { passive: !1 })) : (e.removeAttribute("has-overflow"), e.removeEventListener("pointerdown", F), e.removeEventListener("wheel", O))), !0;
}
}
);
let g = null, y = null, b = null, w = null, H;
function ne() {
e?.setAttribute("blossom-carousel", "true"), y = e?.querySelectorAll("a[href]") || null, y?.forEach((s) => {
s.addEventListener("click", R);
}), window.addEventListener("keydown", $), e.addEventListener("scroll", X), b = new ResizeObserver(V), b.observe(e), w = new MutationObserver(le), w.observe(e, {
attributes: !1,
childList: !0,
subtree: !1
});
const n = window.matchMedia(
"(hover: hover) and (pointer: fine)"
).matches;
t.dir = e.closest('[dir="rtl"]') ? -1 : 1;
const { scrollSnapType: o } = window.getComputedStyle(e);
t.hasSnap = o !== "none", e.style.setProperty("--snap-type", o), n && e.style.setProperty("scroll-snap-type", "none"), e.setAttribute("has-snap", c ? "true" : "false"), e.setAttribute("has-repeat", r?.repeat ? "true" : "false"), H = ae((s) => {
(s === e || e.contains(s)) && (x.value = !1);
});
}
function re() {
e.removeAttribute("blossom-carousel"), b?.disconnect(), w?.disconnect(), g && cancelAnimationFrame(g), window.removeEventListener("keydown", $), e.removeEventListener("scroll", X), y?.forEach((n) => {
n.removeEventListener("click", R);
}), H?.();
}
function R(n) {
d.x > 10 && n.preventDefault();
}
function V() {
if (!e) return;
const n = "ontouchmove" in window;
t.scrollerScrollWidth = e.scrollWidth, t.scrollerWidth = e.clientWidth, t.scrollerScrollHeight = e.scrollHeight, t.scrollerHeight = e.clientHeight;
const o = window.getComputedStyle(e);
h.x = !n && t.scrollerScrollWidth > t.scrollerWidth && ["auto", "scroll"].includes(o.getPropertyValue("overflow-x")), h.y = !n && t.scrollerScrollHeight > t.scrollerHeight && ["auto", "scroll"].includes(o.getPropertyValue("overflow-y")), t.padding.end = parseInt(o.paddingInlineEnd) || 0, t.padding.start = parseInt(o.paddingInlineStart) || 0, t.scrollPadding.start = parseInt(o.scrollPaddingInlineStart) || 0, t.scrollPadding.end = parseInt(o.scrollPaddingInlineEnd) || 0, t.dir = e.closest('[dir="rtl"]') ? -1 : 1, t.end = (t.scrollerScrollWidth - t.scrollerWidth - 4) * t.dir, t.hasSnap ? be(e, t, l) : t.slidePositions = Array.from(e.children).map((s) => {
const p = s.getBoundingClientRect(), M = e.getBoundingClientRect(), m = p.left - M.left + e.scrollLeft;
return {
target: s,
x: m - t.scrollPadding.start,
y: 0,
width: p.width,
height: p.height
};
}), r?.repeat && A(null, null);
}
function le() {
V();
}
function X() {
if (r?.repeat) {
A(null, null);
return;
}
if (t.isDragging || !e) return;
const n = e.scrollLeft;
n < 0 ? C(e, { left: n * -1 }) : n > t.scrollerScrollWidth - t.scrollerWidth && C(e, {
left: n * -1 + t.scrollerScrollWidth - t.scrollerWidth
});
}
const f = {
x: 0,
y: 0
};
function F(n) {
e && (h.x && (f.x = e.scrollLeft, i.x = n.clientX, u.x = 0), h.y && (f.y = e.scrollTop, i.y = n.clientY, u.y = 0), d.x = 0, t.isDragging = !0, window.addEventListener("pointermove", Y), window.addEventListener("pointerup", q));
}
function Y(n) {
if (n.preventDefault(), h.x) {
const o = i.x - n.clientX;
a.x += o, u.x += o, i.x = n.clientX, d.x += Math.abs(o);
}
if (h.y) {
const o = i.y - n.clientY;
a.y += o, u.y += o, i.y = n.clientY, d.y += Math.abs(o);
}
}
function q() {
window.removeEventListener("pointermove", Y), window.removeEventListener("pointerup", q), t.isDragging = !1, !(d.x <= 10) && (h.x && (u.x *= 2), h.y && (u.y *= 2), u.x = Pe(a.x, u.x, v, t, l), ce());
}
function O(n) {
if (Math.abs(n.deltaX) > Math.abs(n.deltaY)) {
if (x.value = !1, t.isDragging || !e) return;
h.x && (f.x = e.scrollLeft), h.y && (f.y = e.scrollTop);
}
}
function $(n) {
["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(n.key) && (x.value = !1);
}
function A(n, o) {
if (!e) return;
const s = o ?? e.scrollLeft, p = t.padding.start - s, M = s - (t.scrollerScrollWidth - t.scrollerWidth - t.padding.end), m = Array.from(e.children), j = (ue, J, de, D) => {
let Q = 0;
const he = D ? -1 : 1, fe = D ? -(t.scrollerScrollWidth - t.scrollerWidth) : t.scrollerScrollWidth - t.scrollerWidth;
for (let L = ue; D ? L >= J : L < J; L += he) {
const ge = Q > de;
m[L].style.translate = `${ge ? 0 : fe}px 0`, Q += m[L].clientWidth;
}
};
if (j(
m.length - 1,
m.length / 2,
p,
!0
), j(0, m.length / 2, M, !1), t.isDragging) return;
const G = s > t.end ? 4 : s < 4 ? t.end : null;
G && (W = !0, e.scrollTo({
left: G,
behavior: "instant"
}));
}
function z(n) {
x.value && n.stopPropagation();
}
const x = new Proxy(
{ value: !1 },
{
set(n, o, s) {
return c = !s, n[o] === s ? !0 : e ? (e.setAttribute("has-snap", c ? "true" : "false"), s && !x.value ? (I = performance.now(), h.x && (a.x = e.scrollLeft), h.y && (a.y = e.scrollTop), e.addEventListener("scrollend", z, {
capture: !0,
passive: !1
}), g || (g = requestAnimationFrame(U))) : s || (g && cancelAnimationFrame(g), g = null, e.removeEventListener("scrollend", z)), n[o] = s, !0) : !1;
}
}
);
let P = 0, I = 0;
function U(n) {
g = requestAnimationFrame(U), P = n - I, e && (h.x && (u.x *= v, t.isDragging ? f.x = S(f.x, a.x, v, P) : (a.x += u.x, f.x = S(f.x, a.x, k, P))), h.y && (u.y *= v, t.isDragging ? f.y = S(f.y, a.y, v, P) : (a.y += u.y, f.y = S(f.y, a.y, k, P))), r?.repeat && (f.x > t.end && (f.x = a.x = 4), f.x < 4 && (f.x = a.x = t.end)), W = !0, e.scrollTo({
left: f.x,
top: f.y,
behavior: "instant"
}), t.isDragging && t.hasSnap && We(a.x, u.x, v, t, l), !t.isDragging && T(u.x, 12) === 0 && (x.value = !1, ye(e), t.hasSnap && Ee(t, l)), r?.repeat ? A(null, f.x) : oe(T(f.x, 2)), I = n);
}
let E = 0;
function oe(n) {
if (!e) return;
const o = t.end;
let s = 0;
if (n * t.dir <= 0 ? s = t.isDragging ? n * -0.2 : 0 : n * t.dir > o * t.dir && (s = t.isDragging ? (n - o) * -0.2 : 0), E = S(
E,
s,
t.isDragging ? 0.8 : k,
P
), Math.abs(E) > 0.01) {
if (C(e, { left: E }).defaultPrevented) return;
e.style.transform = `translateX(${T(E, 3)}px)`;
return;
}
e.style.transform = "", E = 0;
}
let W = !1;
const se = e.scrollTo.bind(e);
e.scrollTo = function(...n) {
W === !0 || (x.value = !1), W = !1, se(...n);
};
const ie = e.scrollBy.bind(e);
e.scrollBy = function(...n) {
W === !0 || (x.value = !1), W = !1, ie(...n);
};
function ae(n) {
const o = [], s = Element.prototype.scrollIntoView;
return s && (Element.prototype.scrollIntoView = function(p) {
return n(this, "scrollIntoView", [p]), s.call(this, p);
}, o.push(() => {
Element.prototype.scrollIntoView = s;
})), () => o.forEach((p) => p());
}
function ce() {
const n = (o) => {
o.preventDefault(), o.stopPropagation(), window.removeEventListener("click", n, !0);
};
window.addEventListener("click", n, !0);
}
return {
snap: c,
hasOverflow: h,
init: ne,
destroy: re,
prev: (n) => Se(t, l, n),
next: (n) => Ae(t, l, n)
};
};
class Me extends HTMLElement {
carouselInstance;
constructor() {
super();
const r = this.attachShadow({ mode: "open" });
this.setAttribute("blossom-carousel", "true");
const t = document.createElement("slot");
r.appendChild(t);
}
connectedCallback() {
this.carouselInstance = Ie(this, {
repeat: this.hasAttribute("repeat")
}), this.carouselInstance.init();
}
disconnectedCallback() {
this.carouselInstance.destroy();
}
prev(r) {
this.carouselInstance.prev(r);
}
next(r) {
this.carouselInstance.next(r);
}
}
customElements.define("blossom-carousel", Me);
export {
Me as BlossomCarousel
};