import { resolveMotionProps } from "../utils/resolve-motion-props.mjs"; import { layoutGroupInjectionKey, motionInjectionKey } from "../components/context.mjs"; import { defaultConfig, motionConfigInjectionKey } from "../components/motion-config/context.mjs"; import { animatePresenceInjectionKey } from "../components/animate-presence/presence.mjs"; import { createSVGStyles, createStyles } from "../state/style.mjs"; import { isSVGElement, resolveInitialValues } from "../state/utils.mjs"; import { MotionState, mountedStates } from "../state/motion-state.mjs"; import { createVisualElement } from "../state/create-visual-element.mjs"; import { domMax } from "../features/dom-max.mjs"; import { warning } from "hey-listen"; //#region src/directive/index.ts /** * Extract props from VNode and merge with binding value. * VNode props take priority over binding value for the same key. * * Supports both syntaxes: * - `
` (props on element) * - `
` (binding value) * - Mix of both (VNode props override binding value) */ function extractMotionProps(vnode, bindingValue) { const vnodeProps = vnode?.props; if (!vnodeProps) return bindingValue || {}; return { ...bindingValue || {}, ...vnodeProps }; } /** * Clean up DOM side-effects from Vue's VNode prop patching on native elements. * * Vue checks `key in el` before patching — if true, sets as property (`el[key] = value`), * which creates an own property shadowing the prototype method (e.g. Element.prototype.animate). * `delete el[key]` removes the own property, restoring the prototype chain. */ function cleanVNodeProps(el, vnodeProps) { if (!vnodeProps) return; for (const key in vnodeProps) { const value = vnodeProps[key]; if (typeof value !== "function" && key in Element.prototype) delete el[key]; if (value != null && typeof value === "object" && key !== "style") el.removeAttribute(key); } } function resolveTag(source) { if (typeof Element !== "undefined" && source instanceof Element) return source.tagName.toLowerCase(); const vnode = source; return typeof vnode.type === "string" ? vnode.type : "div"; } /** * Compute CSS styles (and SVG attrs) from motion values. * Shared by SSR and client-side initial style application. */ function computeStyles(values, tag, styleProp) { if (isSVGElement(tag)) { const { attrs, style } = createSVGStyles({ ...values }, tag, styleProp); return { styles: createStyles(style), attrs }; } return { styles: createStyles({ ...styleProp, ...values }) }; } function resolveSSRStyles(options) { if (!options) return null; const latestValues = resolveInitialValues(options); if (Object.keys(latestValues).length === 0) return null; return computeStyles(latestValues, options.as || "div", options.style).styles; } function applyInitialStyles(el, state) { const { styles, attrs } = computeStyles(state.latestValues, resolveTag(el), state.options.style); if (attrs) for (const key in attrs) el.setAttribute(key, String(attrs[key])); if (styles) for (const key in styles) el.style[key] = styles[key]; } function findComponentParent(vnode, root) { const stack = /* @__PURE__ */ new Set(); const walk = (children) => { for (const child of children) { if (!child) continue; if (child === vnode || child.el && vnode.el && child.el === vnode.el) return true; stack.add(child); let result; if (child.suspense) result = walk([child.ssContent]); else if (Array.isArray(child.children)) result = walk(child.children); else if (child.component?.vnode) result = walk([child.component?.subTree]); if (result) return result; stack.delete(child); } return false; }; if (!walk([root.subTree])) { warning(false, "Could not find original vnode, component will not inherit provides"); return root; } const result = Array.from(stack).reverse(); for (const child of result) if (child.component) return child.component; return root; } /** * Resolve the provides object from the vnode's component tree. */ function resolveProvides(vnode, binding) { return (vnode.ctx === binding.instance.$ ? findComponentParent(vnode, binding.instance.$)?.provides : vnode.ctx?.provides) ?? binding.instance.$.provides; } /** * Extract context values from the provides chain and build * a full options object matching useMotionState behavior. */ function buildMotionOptions(motionProps, provides, tag) { const parentState = provides[motionInjectionKey] ?? null; const layoutGroup = provides[layoutGroupInjectionKey] ?? {}; const presenceContext = provides[animatePresenceInjectionKey] ?? {}; const config = (provides[motionConfigInjectionKey] ?? null)?.value ?? defaultConfig; return { parentState, options: resolveMotionProps({ ...motionProps, as: tag }, { layoutGroup, presenceContext, config }) }; } /** * Create a v-motion directive with the given feature bundle. * If no bundle is provided, defaults to domMax (all features). * * @param featureBundle - Optional feature bundle (domAnimation or domMax) * @param defaultOptions - Optional default motion props merged under user props */ function createMotionDirective(featureBundle, defaultOptions) { const renderer = featureBundle?.renderer ?? createVisualElement; const features = featureBundle?.features ?? domMax.features; function mergeMotionProps(vnode, bindingValue) { const userProps = extractMotionProps(vnode, bindingValue); return defaultOptions ? { ...defaultOptions, ...userProps } : userProps; } /** * Build fresh motion options from the current vnode + binding, * resolving context through the component tree. */ function resolveStateOptions(el, binding, vnode) { const provides = resolveProvides(vnode, binding); return buildMotionOptions(mergeMotionProps(vnode, binding.value), provides, resolveTag(el)); } return { created(el, binding, vnode) { const { options, parentState } = resolveStateOptions(el, binding, vnode); const state = new MotionState(options, parentState, { renderer, features }); mountedStates.set(el, state); }, mounted(el, _binding, vnode) { const state = mountedStates.get(el); if (!state) return; cleanVNodeProps(el, vnode.props); applyInitialStyles(el, state); state.mount(el); }, beforeUpdate(el, binding, vnode) { const state = mountedStates.get(el); if (!state) return; state.updateOptions(resolveStateOptions(el, binding, vnode).options); state.getSnapshot(true); }, updated(el, _binding, vnode) { const state = mountedStates.get(el); if (!state) return; cleanVNodeProps(el, vnode.props); state.update(); }, beforeUnmount(el, binding, vnode) { const state = mountedStates.get(el); if (!state) return; state.updateOptions(resolveStateOptions(el, binding, vnode).options); state.getSnapshot(false); }, unmounted(el) { const state = mountedStates.get(el); if (!state) return; if (!el.isConnected) state.unmount(); }, getSSRProps(binding, vnode) { const motionProps = mergeMotionProps(vnode, binding.value); const tag = vnode ? resolveTag(vnode) : "div"; const ssrStyles = resolveSSRStyles({ ...motionProps, as: tag }); if (!ssrStyles) return {}; return { style: ssrStyles }; } }; } /** * Create a preset directive with default motion options baked in. * Users can still override any option via binding value or VNode props. * * @example * ```ts * const vFadeIn = createPresetDirective({ * initial: { opacity: 0 }, * animate: { opacity: 1 }, * }) * app.directive('fade-in', vFadeIn) * ``` */ function createPresetDirective(preset, featureBundle) { return createMotionDirective(featureBundle ?? domMax, preset); } var vMotion = createMotionDirective(domMax); /** * Vue plugin for global v-motion directive registration. * * @example * ```ts * import { MotionPlugin } from 'motion-v' * app.use(MotionPlugin) * * // With custom presets * app.use(MotionPlugin, { * presets: { * 'fade-in': { initial: { opacity: 0 }, animate: { opacity: 1 } }, * }, * }) * ``` */ var MotionPlugin = { install(app, options) { app.directive("motion", vMotion); if (options?.presets) for (const [name, preset] of Object.entries(options.presets)) app.directive(name, createPresetDirective(preset)); } }; //#endregion export { MotionPlugin, createMotionDirective, createPresetDirective, vMotion };