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