UNPKG

@humanspeak/svelte-motion

Version:

Framer Motion for Svelte 5. Declarative motion.<tag> components with AnimatePresence exit animations, gestures (hover, tap, drag, focus, in-view), variants, FLIP layout animations, shared-layout transitions, spring physics, and scroll-linked motion values

110 lines (109 loc) 3.28 kB
import { cancelMicrotask, microtask } from 'motion-dom'; import { resolveElement } from './dom.js'; /** * Builds a subscriber-refcounted DOM-attachment primitive. Both `useScroll` * and `useInView` use this to defer observer setup until a subscriber arrives, * poll for `bind:this` element resolution, and tear down on the last * unsubscribe. * * @param config Attachment configuration: refs to resolve, an `onAttach` * callback that returns a cleanup function, and an optional `isLatched` * short-circuit. * @returns An `Attachable` exposing `subscribe()` for use inside Svelte * `readable(..., start)` callbacks. * @example * ```ts * const attachable = createAttachable({ * refs: { target }, * onAttach: ({ target }) => { * const stopObserving = startObserver(target!, ...) * return stopObserving * } * }) * return readable(initial, (set) => { * const release = attachable.subscribe() * return release * }) * ``` */ export const createAttachable = (config) => { let cleanup; let pollScheduled = false; let subscriberCount = 0; // When stop() runs synchronously inside onAttach, cleanup hasn't been // assigned yet. Defer the teardown so the just-returned disposer still // gets invoked. let attaching = false; let stopRequestedDuringAttach = false; const pollTick = () => { pollScheduled = false; tryAttach(); }; const cancelPoll = () => { if (pollScheduled) { cancelMicrotask(pollTick); pollScheduled = false; } }; const stop = () => { cancelPoll(); if (attaching) { stopRequestedDuringAttach = true; return; } if (cleanup) { const fn = cleanup; cleanup = undefined; fn(); } }; const tryAttach = () => { if (cleanup || config.isLatched?.()) return; const els = {}; let needsPoll = false; for (const key of Object.keys(config.refs)) { const ref = config.refs[key]; const el = resolveElement(ref); if (ref && !el) needsPoll = true; els[key] = el; } if (needsPoll) { // Microtask schedule (matches useScroll's pattern) — one frame // faster than rAF for refs that hydrate the same tick. if (!pollScheduled) { pollScheduled = true; microtask.read(pollTick); } return; } attaching = true; let result; try { result = config.onAttach(els, stop); } finally { attaching = false; } if (typeof result === 'function') cleanup = result; if (stopRequestedDuringAttach) { stopRequestedDuringAttach = false; stop(); } }; return { subscribe: () => { subscriberCount++; tryAttach(); return () => { if (subscriberCount > 0) subscriberCount--; if (subscriberCount > 0) return; stop(); }; } }; };