@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
JavaScript
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();
};
}
};
};