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

149 lines (148 loc) 5.62 kB
import { animate } from 'motion'; import { hover } from 'motion-dom'; /** * Determine whether the current environment supports true hover. * * Uses `(hover: hover)` and `(pointer: fine)` media queries to avoid sticky * hover states on touch devices. * * @param win Window object (useful for testing/mocking). * @return Whether the device supports hover with a fine pointer. */ export const isHoverCapable = (win = window) => { try { const mqHover = win.matchMedia('(hover: hover)'); const mqPointerFine = win.matchMedia('(pointer: fine)'); return mqHover.matches && mqPointerFine.matches; } catch { return false; } }; /** * Split a hover definition into keyframes and an optional nested transition. * * @param def While-hover record that may include a nested `transition`. * @return Object with `keyframes` (no `transition`) and optional `transition`. */ export const splitHoverDefinition = (def) => { const { transition, ...rest } = (def ?? {}); return { keyframes: rest, transition }; }; /** * Compute the baseline values to restore to on hover end. * * Preference order per key: `animate` → `initial` → neutral transform defaults * → computed style value if present. * * @param el Target element. * @param opts Source records for baseline computation. * @return Minimal baseline record to restore on hover end. */ export const computeHoverBaseline = (el, opts) => { const baseline = {}; const initialRecord = (opts.initial ?? {}); const animateRecord = (opts.animate ?? {}); const whileHoverRecordRaw = (opts.whileHover ?? {}); const whileHoverRecord = { ...whileHoverRecordRaw }; delete whileHoverRecord.transition; const neutralTransformDefaults = { x: 0, y: 0, translateX: 0, translateY: 0, scale: 1, scaleX: 1, scaleY: 1, rotate: 0, rotateX: 0, rotateY: 0, rotateZ: 0, skewX: 0, skewY: 0, opacity: 1 }; const cs = getComputedStyle(el); const inlineStyle = el.getAttribute('style') || ''; // Helper to escape regex metacharacters to prevent ReDoS and ensure literal matching const escapeRegExp = (str) => { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }; // Helper to extract CSS function (var, calc, min, max, etc.) from inline style if present const getInlineStyleValue = (propName) => { const kebabCase = propName.replace(/([A-Z])/g, '-$1').toLowerCase(); const escapedKebabCase = escapeRegExp(kebabCase); // Match property name at start of string or after semicolon const regex = new RegExp(`(?:^|;)\\s*${escapedKebabCase}\\s*:\\s*([^;]+)`, 'i'); const match = inlineStyle.match(regex); if (match) { const value = match[1].trim(); // Preserve CSS functions: var(), calc(), min(), max(), clamp(), rgb(), hsl(), url(), etc. if (/\b(var|calc|min|max|clamp|rgb|rgba|hsl|hsla|url)\s*\(/.test(value)) { return value; } } return null; }; for (const key of Object.keys(whileHoverRecord)) { if (Object.prototype.hasOwnProperty.call(animateRecord, key)) { baseline[key] = animateRecord[key]; } else if (Object.prototype.hasOwnProperty.call(initialRecord, key)) { baseline[key] = initialRecord[key]; } else if (key in neutralTransformDefaults) { baseline[key] = neutralTransformDefaults[key]; } else { // Check if inline style has a CSS variable for this property const inlineValue = getInlineStyleValue(key); if (inlineValue) { baseline[key] = inlineValue; } else if (key in cs) { baseline[key] = cs[key]; } } } return baseline; }; /** * Attach whileHover interactions to an element with capability gating. * * Uses motion-dom's hover function which filters out touch events and interoperates * with drag gestures. On pointer enter, animates to `whileHover` (using nested `transition` * if provided). On leave, restores changed keys to the baseline using the merged * root/component transition. * * @param el Target element. * @param whileHover While-hover definition. * @param mergedTransition Root/component merged transition. * @param callbacks Optional lifecycle callbacks for hover start/end. * @param baselineSources Optional sources used to compute baseline. * @return Cleanup function to remove hover listeners. */ export const attachWhileHover = (el, whileHover, mergedTransition, callbacks, baselineSources) => { if (!whileHover) return () => { }; let hoverBaseline = null; return hover(el, () => { // Hover start: compute baseline and animate to whileHover values hoverBaseline = computeHoverBaseline(el, { initial: baselineSources?.initial, animate: baselineSources?.animate, whileHover }); callbacks?.onStart?.(); const { keyframes, transition } = splitHoverDefinition(whileHover); animate(el, keyframes, (transition ?? mergedTransition)); // Return cleanup function for hover end return () => { // Hover end: restore baseline values if (hoverBaseline && Object.keys(hoverBaseline).length > 0) { animate(el, hoverBaseline, mergedTransition); } callbacks?.onEnd?.(); }; }); };