UNPKG

@blossom-carousel/web

Version:

A native-scroll-first carousel component for the web.

405 lines (404 loc) 13.3 kB
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 };