UNPKG

@blossom-carousel/svelte

Version:

A native-scroll-first carousel component for Svelte.

98 lines (97 loc) 3.85 kB
import { pageScroll } from "./scroll.js"; import { getMarkerTargets } from "./markers.js"; import { resolveInlineAlign } from "./snap.js"; import { getSnapCache } from "./cache.js"; const COMMAND_NAMESPACE = "--blossom-"; export const COMMANDS = { prev: `${COMMAND_NAMESPACE}prev`, next: `${COMMAND_NAMESPACE}next`, gotoPrefix: `${COMMAND_NAMESPACE}goto-`, }; const REGISTRY = Symbol.for("blossom-carousel.navigation.registry"); function runCommand(scroller, command) { if (command === COMMANDS.prev) { pageScroll(scroller, "prev"); } else if (command === COMMANDS.next) { pageScroll(scroller, "next"); } else if (command.startsWith(COMMANDS.gotoPrefix)) { const index = Number.parseInt(command.slice(COMMANDS.gotoPrefix.length), 10); if (Number.isNaN(index) || index < 0) return; // Prefer the cache, but ignore a stale entry whose element has been detached // (a slide removed before the cache refresh runs) and read live instead. const cached = getSnapCache(scroller)?.markers[index]; const fresh = cached?.el.isConnected ? cached : undefined; const target = fresh?.el ?? getMarkerTargets(scroller)[index]; if (!target) return; const inline = fresh?.align ?? resolveInlineAlign(getComputedStyle(target).scrollSnapAlign); target.scrollIntoView({ block: "nearest", inline, behavior: "smooth" }); } } /** * Attaches a single `command` listener to a scroller that handles the Blossom * navigation commands using native scroll APIs only (no Blossom instance). * * The native `command` event does not bubble, so the listener must live on the * target element itself. Registration is idempotent and ref-counted so multiple * controls pointing at the same scroller share one handler; the returned cleanup * detaches the handler once the last control releases it. */ export function registerCommands(scroller) { installFallback(); const el = scroller; const existing = el[REGISTRY]; if (existing) { existing.count++; return () => release(el); } const handler = (event) => { const command = event.command ?? event.detail?.command; if (typeof command === "string" && command.startsWith(COMMAND_NAMESPACE)) { runCommand(scroller, command); } }; scroller.addEventListener("command", handler); el[REGISTRY] = { count: 1, handler }; return () => release(el); } function release(el) { const registration = el[REGISTRY]; if (!registration) return; registration.count--; if (registration.count <= 0) { el.removeEventListener("command", registration.handler); delete el[REGISTRY]; } } /** * Manual fallback for engines without the Invoker Commands API: delegate clicks * on `button[commandfor]` and dispatch a synthetic `command` event on the * target, matching the native event shape. Installed once, only when needed. */ let fallbackInstalled = false; function installFallback() { if (fallbackInstalled || typeof document === "undefined") return; fallbackInstalled = true; const supportsInvokers = typeof HTMLButtonElement !== "undefined" && "commandForElement" in HTMLButtonElement.prototype; if (supportsInvokers) return; document.addEventListener("click", (event) => { const button = event.target?.closest?.("button[commandfor]"); if (!button) return; const id = button.getAttribute("commandfor"); const command = button.getAttribute("command"); if (!id || !command) return; const target = document.getElementById(id); target?.dispatchEvent(new CustomEvent("command", { bubbles: false, detail: { command } })); }); }