UNPKG

@blossom-carousel/web

Version:

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

385 lines (384 loc) 13 kB
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 };