build: ship pre-built API bundle; images copy output instead of installing
- API: `pnpm build` bundles dist/ into apps/api/.output (esbuild), with Linux musl native packages for sharp and argon2 kept alongside - docker/api.Dockerfile copies that output; no pnpm install on the server - add dist/migrate.js (prisma migrate deploy equivalent, same _prisma_migrations table and lock) so the Prisma CLI is not needed at runtime - pnpm-workspace: supportedArchitectures for linux/x64/musl - admin: apps/admin/.env.production sets VITE_SITE_URL for the production build - drop NPM_REGISTRY / SITE_URL build args from compose and .env.example - gitignore: anchor node_modules rules so pre-built node_modules are committed - README: describe the pre-built deploy flow Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 5.5
parent
ee8258556d
commit
10e453e4be
Generated
Vendored
+68
@@ -0,0 +1,68 @@
|
||||
import { animateVisualElement, setTarget } from "motion-dom";
|
||||
import { invariant } from "hey-listen";
|
||||
//#region src/animation/hooks/animation-controls.ts
|
||||
function stopAnimation(visualElement) {
|
||||
visualElement.values.forEach((value) => value.stop());
|
||||
}
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
function animationControls() {
|
||||
/**
|
||||
* Track whether the host component has mounted.
|
||||
*/
|
||||
let hasMounted = false;
|
||||
/**
|
||||
* A collection of linked component animation controls.
|
||||
*/
|
||||
const subscribers = /* @__PURE__ */ new Set();
|
||||
const controls = {
|
||||
subscribe(state) {
|
||||
subscribers.add(state);
|
||||
return () => void subscribers.delete(state);
|
||||
},
|
||||
start(definition, transitionOverride) {
|
||||
invariant(hasMounted, "controls.start() should only be called after a component has mounted. Consider calling within a useEffect hook.");
|
||||
const animations = [];
|
||||
subscribers.forEach((visualElement) => {
|
||||
animations.push(animateVisualElement(visualElement, definition, { transitionOverride }));
|
||||
});
|
||||
return Promise.all(animations);
|
||||
},
|
||||
set(definition) {
|
||||
invariant(hasMounted, "controls.set() should only be called after a component has mounted. Consider calling within a useEffect hook.");
|
||||
return subscribers.forEach((visualElement) => {
|
||||
setValues(visualElement, definition);
|
||||
});
|
||||
},
|
||||
stop() {
|
||||
subscribers.forEach((visualElement) => {
|
||||
stopAnimation(visualElement);
|
||||
});
|
||||
},
|
||||
mount() {
|
||||
hasMounted = true;
|
||||
return () => {
|
||||
hasMounted = false;
|
||||
controls.stop();
|
||||
};
|
||||
}
|
||||
};
|
||||
return controls;
|
||||
}
|
||||
function setValues(visualElement, definition) {
|
||||
if (Array.isArray(definition)) return setVariants(visualElement, definition);
|
||||
else if (typeof definition === "string") return setVariants(visualElement, [definition]);
|
||||
else setTarget(visualElement, definition);
|
||||
}
|
||||
function setVariants(visualElement, variantLabels) {
|
||||
[...variantLabels].reverse().forEach((key) => {
|
||||
const variant = visualElement.getVariant(key);
|
||||
variant && setTarget(visualElement, variant);
|
||||
if (visualElement.variantChildren) visualElement.variantChildren.forEach((child) => {
|
||||
setVariants(child, variantLabels);
|
||||
});
|
||||
});
|
||||
}
|
||||
//#endregion
|
||||
export { animationControls, setValues };
|
||||
Generated
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
import { useMotionConfig } from "../../components/motion-config/context.mjs";
|
||||
import { createScopedAnimate } from "framer-motion/dom";
|
||||
import { computed, onUnmounted, ref } from "vue";
|
||||
//#region src/animation/hooks/use-animate.ts
|
||||
function useAnimate() {
|
||||
const dom = ref(null);
|
||||
const domProxy = new Proxy(dom, {
|
||||
get(target, key) {
|
||||
if (typeof key === "string" || typeof key === "symbol") {
|
||||
if (key === "current") return Reflect.get(target, "value");
|
||||
return Reflect.get(target, key);
|
||||
}
|
||||
},
|
||||
set(target, key, value) {
|
||||
if (key === "value") return Reflect.set(target, key, value?.$el || value);
|
||||
if (key === "animations") return Reflect.set(target, key, value);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
domProxy.animations = [];
|
||||
const config = useMotionConfig();
|
||||
const scopedAnimate = computed(() => createScopedAnimate({
|
||||
scope: domProxy,
|
||||
skipAnimations: config.value.skipAnimations
|
||||
}));
|
||||
const animate = ((...args) => scopedAnimate.value(...args));
|
||||
onUnmounted(() => {
|
||||
domProxy.animations.forEach((animation) => animation.stop());
|
||||
});
|
||||
return [domProxy, animate];
|
||||
}
|
||||
//#endregion
|
||||
export { useAnimate };
|
||||
Generated
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
import { animationControls } from "./animation-controls.mjs";
|
||||
import { onMounted, onUnmounted } from "vue";
|
||||
//#region src/animation/hooks/use-animation-controls.ts
|
||||
/**
|
||||
* Creates `AnimationControls`, which can be used to manually start, stop
|
||||
* and sequence animations on one or more components.
|
||||
*
|
||||
* The returned `AnimationControls` should be passed to the `animate` property
|
||||
* of the components you want to animate.
|
||||
*
|
||||
* These components can then be animated with the `start` method.
|
||||
*
|
||||
* ```jsx
|
||||
* import { motion, useAnimationControls } from 'motion-v'
|
||||
*
|
||||
* export default defineComponent({
|
||||
* setup() {
|
||||
* const controls = useAnimationControls()
|
||||
*
|
||||
* controls.start({
|
||||
* x: 100,
|
||||
* transition: { duration: 0.5 },
|
||||
* })
|
||||
*
|
||||
* return () => (
|
||||
* <motion.div animate={controls} />
|
||||
* )
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @returns Animation controller with `start`, `stop`, `set` and `mount` methods
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
function useAnimationControls() {
|
||||
const controls = animationControls();
|
||||
let unmount;
|
||||
onMounted(() => {
|
||||
unmount = controls.mount();
|
||||
});
|
||||
onUnmounted(() => {
|
||||
unmount();
|
||||
});
|
||||
return controls;
|
||||
}
|
||||
//#endregion
|
||||
export { useAnimationControls };
|
||||
Generated
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
import { computed } from "vue";
|
||||
import { useMediaQuery } from "@vueuse/core";
|
||||
//#region src/animation/hooks/use-reduced-motion.ts
|
||||
/**
|
||||
* Reactive prefers-reduced-motion.
|
||||
*/
|
||||
function useReducedMotion(options = {}) {
|
||||
const reducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)", options);
|
||||
return computed(() => reducedMotion.value);
|
||||
}
|
||||
//#endregion
|
||||
export { useReducedMotion };
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import LayoutGroup_vue_vue_type_script_setup_true_lang_default from "./LayoutGroup.vue_vue_type_script_setup_true_lang.mjs";
|
||||
//#region src/components/LayoutGroup.vue
|
||||
var LayoutGroup_default = LayoutGroup_vue_vue_type_script_setup_true_lang_default;
|
||||
//#endregion
|
||||
export { LayoutGroup_default as default };
|
||||
Generated
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
import { useLayoutGroupProvider } from "./use-layout-group.mjs";
|
||||
import { defineComponent, renderSlot, unref } from "vue";
|
||||
//#region src/components/LayoutGroup.vue?vue&type=script&setup=true&lang.ts
|
||||
var LayoutGroup_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
|
||||
__name: "LayoutGroup",
|
||||
props: {
|
||||
id: {},
|
||||
inherit: {
|
||||
type: [Boolean, String],
|
||||
default: true
|
||||
}
|
||||
},
|
||||
setup(__props) {
|
||||
const { forceRender, key } = useLayoutGroupProvider(__props);
|
||||
return (_ctx, _cache) => {
|
||||
return renderSlot(_ctx.$slots, "default", {
|
||||
renderKey: unref(key),
|
||||
forceRender: unref(forceRender)
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
export { LayoutGroup_vue_vue_type_script_setup_true_lang_default as default };
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import RowValue_vue_vue_type_script_setup_true_lang_default from "./RowValue.vue_vue_type_script_setup_true_lang.mjs";
|
||||
//#region src/components/RowValue.vue
|
||||
var RowValue_default = RowValue_vue_vue_type_script_setup_true_lang_default;
|
||||
//#endregion
|
||||
export { RowValue_default as default };
|
||||
Generated
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
import { defineComponent, getCurrentInstance, toDisplayString, watchEffect } from "vue";
|
||||
//#region src/components/RowValue.vue?vue&type=script&setup=true&lang.ts
|
||||
var RowValue_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
|
||||
__name: "RowValue",
|
||||
props: { value: {} },
|
||||
setup(__props) {
|
||||
const props = __props;
|
||||
const instance = getCurrentInstance()?.proxy;
|
||||
watchEffect((cleanup) => {
|
||||
cleanup(props.value.on("change", (value) => {
|
||||
if (instance && instance.$el) instance.$el.textContent = value;
|
||||
}));
|
||||
});
|
||||
return (_ctx, _cache) => {
|
||||
return toDisplayString(__props.value.get());
|
||||
};
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
export { RowValue_vue_vue_type_script_setup_true_lang_default as default };
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
import AnimatePresence_vue_vue_type_script_setup_true_lang_default from "./AnimatePresence.vue_vue_type_script_setup_true_lang.mjs";
|
||||
//#region src/components/animate-presence/AnimatePresence.vue
|
||||
var AnimatePresence_default = AnimatePresence_vue_vue_type_script_setup_true_lang_default;
|
||||
//#endregion
|
||||
export { AnimatePresence_default as default };
|
||||
Generated
Vendored
+40
@@ -0,0 +1,40 @@
|
||||
import { usePresenceContainer } from "./use-presence-container.mjs";
|
||||
import { Transition, TransitionGroup, computed, createBlock, defineComponent, mergeProps, openBlock, renderSlot, resolveDynamicComponent, unref, withCtx } from "vue";
|
||||
//#region src/components/animate-presence/AnimatePresence.vue?vue&type=script&setup=true&lang.ts
|
||||
var AnimatePresence_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
|
||||
name: "AnimatePresence",
|
||||
inheritAttrs: true,
|
||||
__name: "AnimatePresence",
|
||||
props: {
|
||||
mode: { default: "sync" },
|
||||
initial: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
as: {},
|
||||
custom: {},
|
||||
onExitComplete: {},
|
||||
anchorX: { default: "left" }
|
||||
},
|
||||
setup(__props) {
|
||||
const props = __props;
|
||||
const { enter, exit } = usePresenceContainer(props);
|
||||
const transitionProps = computed(() => {
|
||||
if (props.mode !== "wait") return { tag: props.as };
|
||||
return { mode: props.mode === "wait" ? "out-in" : void 0 };
|
||||
});
|
||||
return (_ctx, _cache) => {
|
||||
return openBlock(), createBlock(resolveDynamicComponent(__props.mode === "wait" ? Transition : TransitionGroup), mergeProps(transitionProps.value, {
|
||||
appear: "",
|
||||
css: false,
|
||||
onLeave: unref(exit),
|
||||
onEnter: unref(enter)
|
||||
}), {
|
||||
default: withCtx(() => [renderSlot(_ctx.$slots, "default")]),
|
||||
_: 3
|
||||
}, 16, ["onLeave", "onEnter"]);
|
||||
};
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
export { AnimatePresence_vue_vue_type_script_setup_true_lang_default as default };
|
||||
Generated
Vendored
+103
@@ -0,0 +1,103 @@
|
||||
import { frame } from "motion-dom";
|
||||
//#region src/components/animate-presence/exit-session.ts
|
||||
var popId = 0;
|
||||
/**
|
||||
* Bookkeeping unit for AnimatePresence exits: one session per exiting
|
||||
* container element. Owns pop-layout styling and the fan-in toward
|
||||
* `onAllComplete`. `abort` cancels a session whose element re-entered
|
||||
* mid-exit; its states' exit promises then resolve silently.
|
||||
*/
|
||||
function createExitSession(config) {
|
||||
const sessions = /* @__PURE__ */ new Map();
|
||||
function addPopStyle(session) {
|
||||
if (config.props.mode !== "popLayout") return;
|
||||
const element = session.el;
|
||||
const parent = element.offsetParent;
|
||||
const parentWidth = parent instanceof HTMLElement ? parent.offsetWidth || 0 : 0;
|
||||
const size = {
|
||||
height: element.offsetHeight || 0,
|
||||
width: element.offsetWidth || 0,
|
||||
top: element.offsetTop,
|
||||
left: element.offsetLeft,
|
||||
right: 0
|
||||
};
|
||||
size.right = parentWidth - size.width - size.left;
|
||||
const x = config.props.anchorX === "left" ? `left: ${size.left}px` : `right: ${size.right}px`;
|
||||
const elementPopId = `pop-${popId++}`;
|
||||
element.dataset.motionPopId = elementPopId;
|
||||
const doc = element.ownerDocument;
|
||||
const style = doc.createElement("style");
|
||||
const nonce = config.getNonce();
|
||||
if (nonce) style.nonce = nonce;
|
||||
session.popStyle = style;
|
||||
doc.head.appendChild(style);
|
||||
if (style.sheet) style.sheet.insertRule(`
|
||||
[data-motion-pop-id="${elementPopId}"] {
|
||||
position: absolute !important;
|
||||
width: ${size.width}px !important;
|
||||
height: ${size.height}px !important;
|
||||
top: ${size.top}px !important;
|
||||
${x} !important;
|
||||
}
|
||||
`);
|
||||
}
|
||||
function removePopStyle(session) {
|
||||
const style = session.popStyle;
|
||||
if (!style) return;
|
||||
session.popStyle = void 0;
|
||||
frame.render(() => {
|
||||
style.parentNode?.removeChild(style);
|
||||
});
|
||||
}
|
||||
function track(el, states, done, handlers = []) {
|
||||
const session = {
|
||||
el,
|
||||
states,
|
||||
handlers,
|
||||
done,
|
||||
aborted: false
|
||||
};
|
||||
sessions.set(el, session);
|
||||
addPopStyle(session);
|
||||
const completions = states.map((state) => state.getFeature("exit")?.exit() ?? Promise.resolve());
|
||||
for (const handler of handlers) completions.push(Promise.resolve(handler.onExit()));
|
||||
Promise.all(completions).then(() => {
|
||||
if (!session.aborted) finalize(session);
|
||||
});
|
||||
}
|
||||
function abort(el) {
|
||||
const session = sessions.get(el);
|
||||
if (!session) return;
|
||||
session.aborted = true;
|
||||
sessions.delete(el);
|
||||
removePopStyle(session);
|
||||
session.handlers.forEach((handler) => handler.onEnter?.());
|
||||
}
|
||||
function finalize(session) {
|
||||
sessions.delete(session.el);
|
||||
removePopStyle(session);
|
||||
session.states.forEach((state) => {
|
||||
state.getSnapshot(false);
|
||||
});
|
||||
session.done();
|
||||
if (!session.el.isConnected) session.states.forEach((state) => {
|
||||
state.unmount();
|
||||
});
|
||||
config.onAllComplete();
|
||||
}
|
||||
function dispose() {
|
||||
sessions.forEach((session) => {
|
||||
session.states.forEach((state) => {
|
||||
state.unmount();
|
||||
});
|
||||
});
|
||||
sessions.clear();
|
||||
}
|
||||
return {
|
||||
track,
|
||||
abort,
|
||||
dispose
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
export { createExitSession };
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
import { createContext } from "../../utils/createContext.mjs";
|
||||
//#region src/components/animate-presence/presence.ts
|
||||
var [injectAnimatePresence, provideAnimatePresence, animatePresenceInjectionKey] = createContext("AnimatePresenceContext");
|
||||
//#endregion
|
||||
export { animatePresenceInjectionKey, injectAnimatePresence, provideAnimatePresence };
|
||||
Generated
Vendored
+74
@@ -0,0 +1,74 @@
|
||||
import { useMotionConfig } from "../motion-config/context.mjs";
|
||||
import { provideAnimatePresence } from "./presence.mjs";
|
||||
import { createExitSession } from "./exit-session.mjs";
|
||||
import { onMounted, onUnmounted } from "vue";
|
||||
//#region src/components/animate-presence/use-presence-container.ts
|
||||
function usePresenceContainer(props) {
|
||||
const motionConfig = useMotionConfig();
|
||||
const sessions = createExitSession({
|
||||
props,
|
||||
getNonce: () => motionConfig.value.nonce,
|
||||
onAllComplete: () => props.onExitComplete?.()
|
||||
});
|
||||
const registered = /* @__PURE__ */ new Set();
|
||||
const handlers = /* @__PURE__ */ new Set();
|
||||
let hasMounted = false;
|
||||
provideAnimatePresence({
|
||||
get initial() {
|
||||
return hasMounted ? void 0 : props.initial;
|
||||
},
|
||||
get custom() {
|
||||
return props.custom;
|
||||
},
|
||||
register: (state) => registered.add(state),
|
||||
unregister: (state) => registered.delete(state),
|
||||
registerHandler: (handler) => handlers.add(handler),
|
||||
unregisterHandler: (handler) => handlers.delete(handler)
|
||||
});
|
||||
onMounted(() => {
|
||||
hasMounted = true;
|
||||
});
|
||||
function findMotionStates(container) {
|
||||
const states = [];
|
||||
for (const state of registered) {
|
||||
const el = state.element;
|
||||
if (el && (el === container || container.contains(el))) states.push(state);
|
||||
}
|
||||
return states;
|
||||
}
|
||||
function findHandlers(container) {
|
||||
const found = [];
|
||||
for (const handler of handlers) {
|
||||
const el = handler.element();
|
||||
if (el && (el === container || container.contains(el))) found.push(handler);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
function enter(el, done) {
|
||||
sessions.abort(el);
|
||||
findMotionStates(el).forEach((state) => {
|
||||
state.getFeature("exit")?.reenter();
|
||||
});
|
||||
done();
|
||||
}
|
||||
function exit(el, done) {
|
||||
const container = el;
|
||||
const states = findMotionStates(container);
|
||||
const exiting = findHandlers(container);
|
||||
if (states.length === 0 && exiting.length === 0) {
|
||||
done();
|
||||
props.onExitComplete?.();
|
||||
return;
|
||||
}
|
||||
sessions.track(container, states, done, exiting);
|
||||
}
|
||||
onUnmounted(() => {
|
||||
sessions.dispose();
|
||||
});
|
||||
return {
|
||||
enter,
|
||||
exit
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
export { usePresenceContainer };
|
||||
Generated
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
import { injectAnimatePresence } from "./presence.mjs";
|
||||
import { onMounted, onUnmounted, onUpdated } from "vue";
|
||||
//#region src/components/animate-presence/use-presence.ts
|
||||
/**
|
||||
* Lets a component run its own exit when an `AnimatePresence` removes it,
|
||||
* for components that animate out without a `motion` element's `exit`.
|
||||
*
|
||||
* Removal waits for the promise `onExit` returns. Vue runs the component's
|
||||
* `onUnmounted` hooks before that, so teardown the exit needs (an engine,
|
||||
* listeners) should wait for the same promise. `onEnter` runs if the
|
||||
* element comes back while its exit is running.
|
||||
*
|
||||
* Outside an `AnimatePresence` it does nothing.
|
||||
*/
|
||||
function usePresence(handler) {
|
||||
const presence = injectAnimatePresence({});
|
||||
if (!presence.registerHandler) return;
|
||||
let element;
|
||||
const read = () => {
|
||||
element = handler.element() ?? element;
|
||||
};
|
||||
const registered = {
|
||||
...handler,
|
||||
element: () => element
|
||||
};
|
||||
onMounted(() => {
|
||||
read();
|
||||
presence.registerHandler(registered);
|
||||
});
|
||||
onUpdated(read);
|
||||
onUnmounted(() => presence.unregisterHandler(registered));
|
||||
}
|
||||
//#endregion
|
||||
export { usePresence };
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
import AnimateView_vue_vue_type_script_setup_true_lang_default from "./AnimateView.vue_vue_type_script_setup_true_lang.mjs";
|
||||
//#region src/components/animate-view/AnimateView.vue
|
||||
var AnimateView_default = AnimateView_vue_vue_type_script_setup_true_lang_default;
|
||||
//#endregion
|
||||
export { AnimateView_default as default };
|
||||
Generated
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
import { useAnimateView } from "./use-animate-view.mjs";
|
||||
import { defineComponent, renderSlot } from "vue";
|
||||
//#region src/components/animate-view/AnimateView.vue?vue&type=script&setup=true&lang.ts
|
||||
/**
|
||||
* Animate enter, exit, update and shared element transitions using the
|
||||
* browser's View Transitions API. Adds no wrapper DOM element.
|
||||
*
|
||||
* State updates must be wrapped in `startTransition`:
|
||||
*
|
||||
* ```vue
|
||||
* <button @click="startTransition(() => show = !show)">Toggle</button>
|
||||
* <AnimateView v-if="show"><div class="box" /></AnimateView>
|
||||
* ```
|
||||
*/
|
||||
var AnimateView_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
|
||||
name: "AnimateView",
|
||||
__name: "AnimateView",
|
||||
props: {
|
||||
name: {},
|
||||
transition: {},
|
||||
enter: { type: [Object, Function] },
|
||||
exit: { type: [Object, Function] },
|
||||
share: { type: [Object, Function] },
|
||||
update: { type: [Object, Function] },
|
||||
onAnimationStart: { type: Function },
|
||||
onAnimationComplete: { type: Function }
|
||||
},
|
||||
setup(__props) {
|
||||
useAnimateView(__props);
|
||||
return (_ctx, _cache) => {
|
||||
return renderSlot(_ctx.$slots, "default");
|
||||
};
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
export { AnimateView_vue_vue_type_script_setup_true_lang_default as default };
|
||||
Generated
Vendored
+92
@@ -0,0 +1,92 @@
|
||||
import { GroupAnimation, NativeAnimation, NativeAnimationWrapper, applyGeneratorOptions, getValueTransition, getViewAnimationLayerInfo, getViewAnimations, mapEasingToNativeEasing } from "motion-dom";
|
||||
import { secondsToMilliseconds } from "motion-utils";
|
||||
//#region src/components/animate-view/animate-view-layers.ts
|
||||
/**
|
||||
* Animate the pseudo-element layers the browser has generated for a named
|
||||
* view transition boundary, either retiming the browser's animations or
|
||||
* replacing its crossfade with custom keyframes.
|
||||
*
|
||||
* Ported from framer-motion's AnimateView (`animate-view-layers.mjs`).
|
||||
*/
|
||||
function animateViewLayers(name, animationType, { transition, onAnimationStart, onAnimationComplete, ...props }, types) {
|
||||
const layerAnimations = [];
|
||||
const definition = props[animationType];
|
||||
const { transition: typeTransition, ...values } = (typeof definition === "function" ? definition(types) : definition) || {};
|
||||
const hasValues = Object.keys(values).length > 0;
|
||||
let hasBrowserLayerAnimation = false;
|
||||
for (const viewAnimation of getViewAnimations()) {
|
||||
if (viewAnimation.playState === "finished") continue;
|
||||
const { effect } = viewAnimation;
|
||||
if (typeof KeyframeEffect === "undefined" || !(effect instanceof KeyframeEffect) || !effect.pseudoElement) continue;
|
||||
const info = getViewAnimationLayerInfo(effect.pseudoElement);
|
||||
if (!info || info.layer !== name) continue;
|
||||
hasBrowserLayerAnimation = true;
|
||||
/**
|
||||
* Custom values replace the browser's crossfade (old/new layers).
|
||||
* The group layer carries the position/size morph, so keep it running
|
||||
* with Motion's timing.
|
||||
*/
|
||||
if (hasValues && info.type !== "group") viewAnimation.cancel();
|
||||
else {
|
||||
const transitionName = info.type === "group" ? "layout" : "";
|
||||
let options = {
|
||||
...getValueTransition(transition, transitionName),
|
||||
...getValueTransition(typeTransition, transitionName)
|
||||
};
|
||||
options.duration = secondsToMilliseconds(options.duration ?? .3);
|
||||
options = applyGeneratorOptions(options);
|
||||
effect.updateTiming({
|
||||
delay: secondsToMilliseconds(options.delay ?? 0),
|
||||
duration: options.duration,
|
||||
easing: mapEasingToNativeEasing(options.ease, options.duration)
|
||||
});
|
||||
layerAnimations.push(new NativeAnimationWrapper(viewAnimation));
|
||||
}
|
||||
}
|
||||
if (hasValues && hasBrowserLayerAnimation) layerAnimations.push(...createLayerAnimations(name, animationType, values, transition, typeTransition));
|
||||
const animation = new GroupAnimation(layerAnimations);
|
||||
let active = true;
|
||||
onAnimationStart?.(animation, animationType);
|
||||
if (onAnimationComplete) animation.finished.then(() => {
|
||||
if (active) onAnimationComplete(animationType);
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
animation.cancel();
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Run each custom value as a `NativeAnimation` against the snapshot
|
||||
* pseudo-element that stays visible for this animation type: the `new`
|
||||
* snapshot when entering, the `old` snapshot when exiting or sharing.
|
||||
* The browser's own old/new crossfade has already been cancelled by the
|
||||
* caller; the group layer (size/position morph) is untouched.
|
||||
*/
|
||||
function createLayerAnimations(layerName, animationType, values, baseTransition, typeTransition) {
|
||||
const animations = [];
|
||||
for (const [name, value] of Object.entries(values)) {
|
||||
let keyframes = value;
|
||||
const options = {
|
||||
...getValueTransition(baseTransition, name),
|
||||
...getValueTransition(typeTransition, name)
|
||||
};
|
||||
options.duration && (options.duration = secondsToMilliseconds(options.duration));
|
||||
options.delay && (options.delay = secondsToMilliseconds(options.delay));
|
||||
/**
|
||||
* A single opacity value is a target, not a keyframe pair — backfill
|
||||
* the starting frame from the snapshot's natural state: entering
|
||||
* elements fade in from 0, exiting/shared ones fade out from 1.
|
||||
*/
|
||||
if (name === "opacity" && !Array.isArray(keyframes)) keyframes = [animationType === "enter" ? 0 : 1, keyframes];
|
||||
animations.push(new NativeAnimation({
|
||||
...options,
|
||||
element: document.documentElement,
|
||||
name,
|
||||
pseudoElement: `::view-transition-${animationType === "enter" ? "new" : "old"}(${layerName})`,
|
||||
keyframes
|
||||
}));
|
||||
}
|
||||
return animations;
|
||||
}
|
||||
//#endregion
|
||||
export { animateViewLayers };
|
||||
Generated
Vendored
+102
@@ -0,0 +1,102 @@
|
||||
//#region src/components/animate-view/context.ts
|
||||
var instances = /* @__PURE__ */ new Set();
|
||||
/**
|
||||
* The transition currently collecting participants. Registration closes as
|
||||
* soon as the update has flushed — NOT when the animation finishes playing
|
||||
* (which can take seconds), so unrelated mounts/updates during playback are
|
||||
* never misregistered.
|
||||
*/
|
||||
var activeTransition = null;
|
||||
/**
|
||||
* The most recently started transition. The browser skips an interrupted
|
||||
* transition when a new one begins, and the skipped one's `finished` settles
|
||||
* too — this guard stops a stale transition from removing names the newer
|
||||
* transition has applied.
|
||||
*/
|
||||
var latestTransition = null;
|
||||
/** Cancel functions returned by `animateViewLayers`, keyed by instance. */
|
||||
var animationCleanups = /* @__PURE__ */ new Map();
|
||||
function registerAnimateView(handle) {
|
||||
instances.add(handle);
|
||||
}
|
||||
function unregisterAnimateView(handle) {
|
||||
instances.delete(handle);
|
||||
}
|
||||
function getAnimateViews() {
|
||||
return instances;
|
||||
}
|
||||
function getActiveViewTransition() {
|
||||
return activeTransition;
|
||||
}
|
||||
function beginViewTransition(types) {
|
||||
activeTransition = {
|
||||
types,
|
||||
participants: []
|
||||
};
|
||||
latestTransition = activeTransition;
|
||||
return activeTransition;
|
||||
}
|
||||
/**
|
||||
* Close participant registration once the transition's DOM update has
|
||||
* flushed. The transition's pseudo-element snapshot is taken after this
|
||||
* point, so no further lifecycle events can belong to it.
|
||||
*/
|
||||
function closeViewTransitionRegistration(transition) {
|
||||
if (activeTransition === transition) activeTransition = null;
|
||||
}
|
||||
function endViewTransition(transition) {
|
||||
if (latestTransition !== transition) return;
|
||||
latestTransition = null;
|
||||
for (const instance of instances) instance.removeLayerNames();
|
||||
if (activeTransition === transition) activeTransition = null;
|
||||
/**
|
||||
* Destroy every layer animation, not just forget them. Custom-value
|
||||
* animations are WAAPI animations attached to pseudo-elements BY NAME —
|
||||
* left lingering, they re-attach and replay when the same name reappears
|
||||
* in a later transition (wrap-around carousels, repeated shared-element
|
||||
* opens), and their accumulation crashes the Chrome renderer on rapid
|
||||
* navigation.
|
||||
*/
|
||||
for (const cleanup of animationCleanups.values()) cleanup();
|
||||
animationCleanups.clear();
|
||||
}
|
||||
/**
|
||||
* Store the cancel function for a participant's layer animation(s).
|
||||
* Multi-root participants produce one call per layer name — compose them.
|
||||
*/
|
||||
function setViewTransitionCleanup(handle, cleanup) {
|
||||
const existing = animationCleanups.get(handle);
|
||||
animationCleanups.set(handle, existing ? () => {
|
||||
existing();
|
||||
cleanup();
|
||||
} : cleanup);
|
||||
}
|
||||
/**
|
||||
* Cancel a layer animation still playing for an unmounted instance. Also
|
||||
* suppresses its `onAnimationComplete` via the cleanup's `active` guard.
|
||||
*/
|
||||
function cancelViewTransitionAnimation(handle) {
|
||||
const cleanup = animationCleanups.get(handle);
|
||||
if (cleanup) {
|
||||
animationCleanups.delete(handle);
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Record that an instance is participating in the active transition.
|
||||
* The first recorded type wins within a single transition — an entering
|
||||
* element that also re-renders stays an "enter", not an "update".
|
||||
*/
|
||||
function recordParticipant(type, handle, transition = activeTransition) {
|
||||
if (!transition) return;
|
||||
if (transition.participants.some((p) => p.handle === handle)) return;
|
||||
transition.participants.push({
|
||||
handle,
|
||||
layerNames: [...handle.layerNames],
|
||||
type,
|
||||
props: handle.getProps(),
|
||||
shareName: handle.getShareName()
|
||||
});
|
||||
}
|
||||
//#endregion
|
||||
export { beginViewTransition, cancelViewTransitionAnimation, closeViewTransitionRegistration, endViewTransition, getActiveViewTransition, getAnimateViews, recordParticipant, registerAnimateView, setViewTransitionCleanup, unregisterAnimateView };
|
||||
Generated
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
import { isSSR } from "../../utils/is.mjs";
|
||||
//#region src/components/animate-view/reset-styles.ts
|
||||
var hasInsertedTransitionResetStyle = false;
|
||||
/**
|
||||
* Force all view transition pseudo-elements to linear timing so Motion
|
||||
* transitions fully control the easing via `updateTiming`.
|
||||
*/
|
||||
function injectViewTransitionResetStyles() {
|
||||
if (hasInsertedTransitionResetStyle || isSSR) return;
|
||||
hasInsertedTransitionResetStyle = true;
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) {
|
||||
animation-timing-function: linear !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
//#endregion
|
||||
export { injectViewTransitionResetStyles };
|
||||
Generated
Vendored
+187
@@ -0,0 +1,187 @@
|
||||
import { beginViewTransition, closeViewTransitionRegistration, endViewTransition, getAnimateViews, setViewTransitionCleanup } from "./context.mjs";
|
||||
import { animateViewLayers } from "./animate-view-layers.mjs";
|
||||
import { nextTick } from "vue";
|
||||
import { noop, warnOnce } from "motion-utils";
|
||||
//#region src/components/animate-view/start-transition.ts
|
||||
/**
|
||||
* View transitions are non-interruptible: starting one while another is
|
||||
* playing makes the browser skip the running transition, and its mid-flight
|
||||
* pseudo-element images leak into the new transition's old snapshot (visual
|
||||
* ghost stacking). React serializes transitions instead — updates committed
|
||||
* during playback wait, then flush as a single batched transition. This
|
||||
* queue mirrors that behavior.
|
||||
*/
|
||||
var isTransitionRunning = false;
|
||||
var updateQueue = [];
|
||||
/**
|
||||
* Run a state update inside a native View Transition. Any `<AnimateView>`
|
||||
* component that enters, exits, updates or shares an element during the
|
||||
* update will animate with its configured animations.
|
||||
*
|
||||
* ```ts
|
||||
* startTransition(() => { show.value = !show.value }, { types: ['toggle'] })
|
||||
* ```
|
||||
*
|
||||
* If a transition is already playing, the update is queued and flushed —
|
||||
* batched with any others — once it finishes. Queued calls return
|
||||
* `undefined` and their state changes apply only when the queue drains
|
||||
* (React `startTransition` semantics).
|
||||
*
|
||||
* Returns the native `ViewTransition`, or `undefined` when queued, or when
|
||||
* the View Transitions API is unavailable (SSR or unsupported browsers —
|
||||
* the update still runs, without animation).
|
||||
*/
|
||||
function startTransition(update, options = {}) {
|
||||
if (!(typeof document !== "undefined" && typeof document.startViewTransition === "function")) {
|
||||
if (process.env.NODE_ENV !== "production" && typeof document !== "undefined") warnOnce(true, "startTransition: the View Transitions API is not supported in this browser. The update ran without animation.");
|
||||
update();
|
||||
return;
|
||||
}
|
||||
updateQueue.push({
|
||||
update,
|
||||
types: options.types ?? [],
|
||||
root: options.root
|
||||
});
|
||||
if (isTransitionRunning) return void 0;
|
||||
return flushUpdateQueue();
|
||||
}
|
||||
/**
|
||||
* Drain the queue as a single batched transition. Calls itself recursively
|
||||
* from `finished` to flush updates that arrived during playback; with an
|
||||
* empty queue it just marks the runner idle.
|
||||
*/
|
||||
function flushUpdateQueue() {
|
||||
const batch = updateQueue.splice(0);
|
||||
if (batch.length === 0) {
|
||||
isTransitionRunning = false;
|
||||
return;
|
||||
}
|
||||
isTransitionRunning = true;
|
||||
const types = [...new Set(batch.flatMap((item) => item.types))];
|
||||
const root = batch.some((item) => item.root);
|
||||
const transition = beginViewTransition(types);
|
||||
/**
|
||||
* Apply layer names on every mounted instance *before* the update runs,
|
||||
* so elements that are about to exit already carry their
|
||||
* `view-transition-name` when the browser captures the old snapshot.
|
||||
*/
|
||||
for (const instance of getAnimateViews()) instance.applyLayerNames();
|
||||
/**
|
||||
* Suppress the root *before* startViewTransition: the browser captures the
|
||||
* old snapshot synchronously when the transition starts, so doing this in
|
||||
* the update callback would leave the root in the old snapshot — an
|
||||
* old-only layer that fades out as a full-page ghost instead of a hard cut.
|
||||
*/
|
||||
if (!root) suppressRootLayer();
|
||||
const viewTransition = document.startViewTransition(async () => {
|
||||
if (!root) zeroSizeOverlayPassThrough();
|
||||
for (const item of batch) await item.update();
|
||||
await nextTick();
|
||||
/**
|
||||
* The DOM update has flushed, so every mount/update/unmount belonging
|
||||
* to this transition has happened. Close registration now — the window
|
||||
* must not stay open while the animation plays (which can take
|
||||
* seconds), or unrelated lifecycle events would be misregistered.
|
||||
*/
|
||||
closeViewTransitionRegistration(transition);
|
||||
});
|
||||
viewTransition.ready.then(() => startParticipantAnimations(transition)).catch(noop);
|
||||
viewTransition.finished.catch(noop).finally(() => {
|
||||
restoreRootLayer();
|
||||
endViewTransition(transition);
|
||||
flushUpdateQueue();
|
||||
});
|
||||
return viewTransition;
|
||||
}
|
||||
var overlayPassThroughAnimation = null;
|
||||
/**
|
||||
* Zero-size the `::view-transition` overlay so pointer events reach the live
|
||||
* document during the transition; running animation groups keep their size
|
||||
* and still block clicks over their snapshots. WAAPI scoped to this
|
||||
* transition's pseudo tree, not a global stylesheet (React's technique).
|
||||
* Must run inside the update callback: animations created before
|
||||
* `startViewTransition()` never bind to the pseudo tree.
|
||||
*/
|
||||
function zeroSizeOverlayPassThrough() {
|
||||
const animation = document.documentElement.animate({
|
||||
width: [0, 0],
|
||||
height: [0, 0]
|
||||
}, {
|
||||
duration: 0,
|
||||
fill: "forwards",
|
||||
pseudoElement: "::view-transition"
|
||||
});
|
||||
if (animation.effect?.pseudoElement !== "::view-transition") {
|
||||
animation.cancel();
|
||||
return;
|
||||
}
|
||||
overlayPassThroughAnimation = animation;
|
||||
}
|
||||
/**
|
||||
* Exclude the document root from the transition (React
|
||||
* `cancelRootViewTransitionName` parity): `view-transition-name: none`
|
||||
* before the transition starts skips the root in both snapshots, so unnamed
|
||||
* content changes hard-cut instead of crossfading the whole page.
|
||||
*/
|
||||
function suppressRootLayer() {
|
||||
const root = document.documentElement;
|
||||
if (root.style.getPropertyValue("view-transition-name") !== "") return;
|
||||
root.style.setProperty("view-transition-name", "none");
|
||||
}
|
||||
/** Undo `suppressRootLayer`/`zeroSizeOverlayPassThrough` after the transition. */
|
||||
function restoreRootLayer() {
|
||||
const root = document.documentElement;
|
||||
if (root.style.getPropertyValue("view-transition-name") === "none") root.style.removeProperty("view-transition-name");
|
||||
overlayPassThroughAnimation?.cancel();
|
||||
overlayPassThroughAnimation = null;
|
||||
}
|
||||
/**
|
||||
* Pair shared-element participants, then take over the browser's
|
||||
* pseudo-element animations for every participant's layer. Called once the
|
||||
* transition is `ready` — the pseudo tree now exists and its animations can
|
||||
* be retimed or replaced.
|
||||
*/
|
||||
function startParticipantAnimations(transition) {
|
||||
pairSharedParticipants(transition);
|
||||
const seen = /* @__PURE__ */ new Set();
|
||||
for (const participant of transition.participants) {
|
||||
if (participant.consumed) continue;
|
||||
for (const layerName of participant.layerNames) {
|
||||
if (seen.has(layerName)) continue;
|
||||
seen.add(layerName);
|
||||
/**
|
||||
* Keep the cancel function so an instance unmounted mid-playback can
|
||||
* cancel its layer animation (and suppress `onAnimationComplete`),
|
||||
* mirroring the cleanup React's `<ViewTransition>` performs.
|
||||
*/
|
||||
const cleanup = animateViewLayers(layerName, participant.type, participant.props, transition.types);
|
||||
setViewTransitionCleanup(participant.handle, cleanup);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Share pairing: an exiting and an entering instance with the same `name`
|
||||
* within this transition animate as a shared element, configured by the
|
||||
* entering side's props. The exiting participant is marked `consumed` so
|
||||
* the run loop skips it — the browser has already paired both sides under
|
||||
* one layer name.
|
||||
*
|
||||
* Note: components that neither enter, exit nor re-render are not detected
|
||||
* as participants even if the browser still morphs their position (e.g.
|
||||
* list reorder without a re-render) — their group layer plays with the
|
||||
* browser's default timing. Unlike React's `<ViewTransition>`, Vue has no
|
||||
* reconciler-level signal for this.
|
||||
*/
|
||||
function pairSharedParticipants(transition) {
|
||||
const exits = transition.participants.filter((p) => p.type === "exit" && p.shareName);
|
||||
for (const participant of transition.participants) {
|
||||
if (participant.type !== "enter" || !participant.shareName) continue;
|
||||
const exit = exits.find((p) => p.shareName === participant.shareName);
|
||||
if (exit) {
|
||||
participant.type = "share";
|
||||
exit.consumed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
export { startTransition };
|
||||
Generated
Vendored
+132
@@ -0,0 +1,132 @@
|
||||
import { cancelViewTransitionAnimation, getActiveViewTransition, recordParticipant, registerAnimateView, unregisterAnimateView } from "./context.mjs";
|
||||
import { injectViewTransitionResetStyles } from "./reset-styles.mjs";
|
||||
import { getCurrentInstance, onBeforeUnmount, onMounted, onUnmounted, onUpdated } from "vue";
|
||||
//#region src/components/animate-view/use-animate-view.ts
|
||||
var uidCounter = 0;
|
||||
/** charCode of 'a' — suffix base for multi-root layer names (`uid-a`, `uid-b`, …) */
|
||||
var SUFFIX_A = 97;
|
||||
/**
|
||||
* Collect the nearest host DOM nodes of a component instance.
|
||||
*
|
||||
* How the traversal stays inside this component's DOM range:
|
||||
*
|
||||
* Vue maintains two pointers on every vnode that delimit its DOM output:
|
||||
*
|
||||
* - `subTree.el` — the first DOM node this component rendered
|
||||
* - `subTree.anchor` — the last DOM node (only set on Fragment vnodes,
|
||||
* i.e. multi-root components)
|
||||
*
|
||||
* Vue guarantees a fragment's children are one contiguous run of siblings
|
||||
* between `el` and `anchor` (its own patch/move logic relies on this), so
|
||||
* walking `nextSibling` from `el` to `anchor` covers exactly this
|
||||
* component's top-level DOM — never nodes before or after it.
|
||||
*
|
||||
* Shapes this produces:
|
||||
*
|
||||
* - Single root (`<div/>`): `anchor` is absent, falls back to `el`, the
|
||||
* loop stops after one node.
|
||||
* - Multi-root fragment (`<div/><div/>`): every top-level sibling in the
|
||||
* `el → anchor` range is collected (they get suffixed names later,
|
||||
* mirroring React's `name-a`/`name-b` behavior).
|
||||
* - Root is a child component: the child vnode's `el` is its own root DOM
|
||||
* node, so we land on the "nearest host node" without recursing into the
|
||||
* child's internals (we follow `nextSibling`, never `childNodes`).
|
||||
* - `<Teleport>` content renders elsewhere in the document, so it is
|
||||
* structurally outside the `el → anchor` range and never collected.
|
||||
* - Text and comment nodes (Vue uses comments as `v-if` placeholders) are
|
||||
* filtered out by the `nodeType === 1` check.
|
||||
*/
|
||||
function getRootElements(instance) {
|
||||
const subTree = instance.subTree;
|
||||
if (!subTree || !subTree.el) return [];
|
||||
const elements = [];
|
||||
let node = subTree.el;
|
||||
const anchor = subTree.anchor ?? subTree.el;
|
||||
const parent = subTree.el.parentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === 1) elements.push(node);
|
||||
if (node === anchor) break;
|
||||
node = node.nextSibling;
|
||||
/**
|
||||
* Defensive bound: `el` and `anchor` must live in the same parent.
|
||||
* If `anchor` were ever unreachable as a following sibling (a broken
|
||||
* fragment invariant or a mid-patch intermediate state), stop instead
|
||||
* of walking past the component's range into unrelated siblings.
|
||||
*/
|
||||
if (node && node.parentNode !== parent) break;
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
/**
|
||||
* Register the current `<AnimateView>` instance with the view transition
|
||||
* registry and wire up lifecycle-based participation detection.
|
||||
*
|
||||
* Names are applied lazily: `startTransition` applies layer names on every
|
||||
* mounted instance right before the DOM update, and entering instances apply
|
||||
* their own on mount. Outside a transition no `view-transition-name` is
|
||||
* written, so idle instances never interfere with other transitions.
|
||||
*/
|
||||
function useAnimateView(props) {
|
||||
const instance = getCurrentInstance();
|
||||
const uid = `motion-view-${++uidCounter}`;
|
||||
let layerNames = [];
|
||||
let layerElements = [];
|
||||
function computeLayerNames(count) {
|
||||
const name = props.name ?? uid;
|
||||
if (count <= 1) return [name];
|
||||
return Array.from({ length: count }, (_, i) => `${name}-${String.fromCharCode(SUFFIX_A + i)}`);
|
||||
}
|
||||
function applyLayerNames() {
|
||||
removeLayerNames();
|
||||
layerElements = getRootElements(instance);
|
||||
layerNames = computeLayerNames(layerElements.length);
|
||||
layerElements.forEach((el, i) => {
|
||||
el.style.setProperty("view-transition-name", layerNames[i]);
|
||||
});
|
||||
}
|
||||
function removeLayerNames() {
|
||||
for (const el of layerElements) el.style.removeProperty("view-transition-name");
|
||||
layerElements = [];
|
||||
layerNames = [];
|
||||
}
|
||||
/**
|
||||
* Spread, don't enumerate: any prop added to `AnimateViewProps` later is
|
||||
* captured automatically, so exit-time snapshots can't silently drop it.
|
||||
*/
|
||||
function getPropsSnapshot() {
|
||||
return { ...props };
|
||||
}
|
||||
const handle = {
|
||||
uid,
|
||||
get layerNames() {
|
||||
return layerNames;
|
||||
},
|
||||
applyLayerNames,
|
||||
removeLayerNames,
|
||||
getProps: getPropsSnapshot,
|
||||
getShareName: () => props.name
|
||||
};
|
||||
onMounted(() => {
|
||||
injectViewTransitionResetStyles();
|
||||
registerAnimateView(handle);
|
||||
if (getActiveViewTransition()) {
|
||||
applyLayerNames();
|
||||
recordParticipant("enter", handle);
|
||||
}
|
||||
});
|
||||
onUpdated(() => {
|
||||
if (!getActiveViewTransition()) return;
|
||||
applyLayerNames();
|
||||
recordParticipant("update", handle);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
if (getActiveViewTransition()) recordParticipant("exit", handle);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
unregisterAnimateView(handle);
|
||||
cancelViewTransitionAnimation(handle);
|
||||
removeLayerNames();
|
||||
});
|
||||
}
|
||||
//#endregion
|
||||
export { useAnimateView };
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import { createContext } from "../utils/createContext.mjs";
|
||||
//#region src/components/context.ts
|
||||
var [injectMotion, provideMotion, motionInjectionKey] = createContext("Motion");
|
||||
var [injectLayoutGroup, provideLayoutGroup, layoutGroupInjectionKey] = createContext("LayoutGroup");
|
||||
//#endregion
|
||||
export { injectLayoutGroup, injectMotion, layoutGroupInjectionKey, motionInjectionKey, provideLayoutGroup, provideMotion };
|
||||
Generated
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
import "vue";
|
||||
import { unrefElement } from "@vueuse/core";
|
||||
//#region src/components/hooks/use-motion-elm.ts
|
||||
/**
|
||||
* Get the actual motion element, skipping text and comment nodes
|
||||
* @param el - The HTML element to check
|
||||
* @returns The first non-text/comment element
|
||||
*/
|
||||
function getMotionElement(el) {
|
||||
if (!el) return void 0;
|
||||
if (el.nodeType === 3 || el.nodeType === 8) return getMotionElement(el.nextSibling);
|
||||
return el;
|
||||
}
|
||||
/**
|
||||
* Get the actual element, skipping text and comment nodes
|
||||
* @param target - The element to check
|
||||
* @returns The first non-text/comment element
|
||||
*/
|
||||
function getElement(target) {
|
||||
return getMotionElement(unrefElement(target));
|
||||
}
|
||||
//#endregion
|
||||
export { getElement, getMotionElement };
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
import { createContext } from "../../utils/createContext.mjs";
|
||||
//#region src/components/lazy-motion/context.ts
|
||||
var [useLazyMotionContext, lazyMotionContextProvider] = createContext("LazyMotionContext");
|
||||
//#endregion
|
||||
export { lazyMotionContextProvider, useLazyMotionContext };
|
||||
Generated
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
import { lazyMotionContextProvider } from "./context.mjs";
|
||||
import { defineComponent, ref } from "vue";
|
||||
//#region src/components/lazy-motion/index.ts
|
||||
var LazyMotion = defineComponent({
|
||||
name: "LazyMotion",
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
features: {
|
||||
type: [Object, Function],
|
||||
required: true
|
||||
},
|
||||
strict: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
const features = ref({});
|
||||
if (typeof props.features === "object" && "renderer" in props.features) features.value = props.features;
|
||||
else if (typeof props.features === "function") props.features().then((mod) => {
|
||||
features.value = mod;
|
||||
});
|
||||
else if (props.features instanceof Promise) props.features.then((mod) => {
|
||||
features.value = mod;
|
||||
});
|
||||
lazyMotionContextProvider({
|
||||
features,
|
||||
strict: props.strict
|
||||
});
|
||||
return () => {
|
||||
return slots.default?.();
|
||||
};
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
export { LazyMotion };
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
import MotionConfig_vue_vue_type_script_setup_true_lang_default from "./MotionConfig.vue_vue_type_script_setup_true_lang.mjs";
|
||||
//#region src/components/motion-config/MotionConfig.vue
|
||||
var MotionConfig_default = MotionConfig_vue_vue_type_script_setup_true_lang_default;
|
||||
//#endregion
|
||||
export { MotionConfig_default as default };
|
||||
Generated
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
import { defaultConfig, provideMotionConfig, useMotionConfig } from "./context.mjs";
|
||||
import { computed, defineComponent, renderSlot } from "vue";
|
||||
import { isDef } from "@vueuse/core";
|
||||
import { warning } from "hey-listen";
|
||||
//#region src/components/motion-config/MotionConfig.vue?vue&type=script&setup=true&lang.ts
|
||||
var MotionConfig_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
|
||||
name: "MotionConfig",
|
||||
inheritAttrs: false,
|
||||
__name: "MotionConfig",
|
||||
props: {
|
||||
transition: {},
|
||||
reduceMotion: {},
|
||||
reducedMotion: { default: ({ reduceMotion }) => {
|
||||
if (isDef(reduceMotion)) {
|
||||
warning(false, "`reduceMotion` is deprecated. Use `reducedMotion` instead.");
|
||||
return reduceMotion;
|
||||
}
|
||||
return defaultConfig.reducedMotion;
|
||||
} },
|
||||
skipAnimations: { type: Boolean },
|
||||
nonce: {},
|
||||
inViewOptions: {}
|
||||
},
|
||||
setup(__props) {
|
||||
const props = __props;
|
||||
const parentConfig = useMotionConfig();
|
||||
const config = computed(() => ({
|
||||
transition: props.transition ?? parentConfig.value.transition,
|
||||
reducedMotion: props.reducedMotion ?? parentConfig.value.reducedMotion,
|
||||
skipAnimations: props.skipAnimations ?? parentConfig.value.skipAnimations,
|
||||
nonce: props.nonce ?? parentConfig.value.nonce,
|
||||
inViewOptions: props.inViewOptions ?? parentConfig.value.inViewOptions
|
||||
}));
|
||||
provideMotionConfig(config);
|
||||
return (_ctx, _cache) => {
|
||||
return renderSlot(_ctx.$slots, "default");
|
||||
};
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
export { MotionConfig_vue_vue_type_script_setup_true_lang_default as default };
|
||||
Generated
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
import { createContext } from "../../utils/createContext.mjs";
|
||||
import { computed } from "vue";
|
||||
//#region src/components/motion-config/context.ts
|
||||
/**
|
||||
* Default motion configuration
|
||||
*/
|
||||
var defaultConfig = {
|
||||
reducedMotion: "never",
|
||||
transition: void 0,
|
||||
nonce: void 0
|
||||
};
|
||||
/**
|
||||
* Context for sharing motion configuration with child components
|
||||
*/
|
||||
var [injectMotionConfig, provideMotionConfig, motionConfigInjectionKey] = createContext("MotionConfig");
|
||||
function useMotionConfig() {
|
||||
return injectMotionConfig(computed(() => defaultConfig));
|
||||
}
|
||||
//#endregion
|
||||
export { defaultConfig, injectMotionConfig, motionConfigInjectionKey, provideMotionConfig, useMotionConfig };
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
import { createMotionComponentWithFeatures } from "./utils.mjs";
|
||||
import { domMax } from "../../features/dom-max.mjs";
|
||||
//#region src/components/motion/index.ts
|
||||
var motion = createMotionComponentWithFeatures(domMax);
|
||||
var Motion = motion.create("div");
|
||||
//#endregion
|
||||
export { Motion, motion };
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import { createMotionComponentWithFeatures } from "./utils.mjs";
|
||||
//#region src/components/motion/m.ts
|
||||
var m = createMotionComponentWithFeatures();
|
||||
var M = m.create("div");
|
||||
//#endregion
|
||||
export { M, m };
|
||||
Generated
Vendored
+148
@@ -0,0 +1,148 @@
|
||||
//#region src/components/motion/props.ts
|
||||
var MotionComponentProps = {
|
||||
"ignoreStrict": { type: Boolean },
|
||||
"forwardMotionProps": {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
"asChild": {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
"whileDrag": { type: [
|
||||
String,
|
||||
Array,
|
||||
Object
|
||||
] },
|
||||
"whileHover": { type: [
|
||||
String,
|
||||
Array,
|
||||
Object
|
||||
] },
|
||||
"whilePress": { type: [
|
||||
String,
|
||||
Array,
|
||||
Object
|
||||
] },
|
||||
"whileInView": { type: [
|
||||
String,
|
||||
Array,
|
||||
Object
|
||||
] },
|
||||
"whileFocus": { type: [
|
||||
String,
|
||||
Array,
|
||||
Object
|
||||
] },
|
||||
"custom": { type: [
|
||||
String,
|
||||
Number,
|
||||
Object,
|
||||
Array
|
||||
] },
|
||||
"initial": {
|
||||
type: [
|
||||
String,
|
||||
Array,
|
||||
Object,
|
||||
Boolean
|
||||
],
|
||||
default: void 0
|
||||
},
|
||||
"animate": {
|
||||
type: [
|
||||
String,
|
||||
Array,
|
||||
Object
|
||||
],
|
||||
default: void 0
|
||||
},
|
||||
"exit": { type: [
|
||||
String,
|
||||
Array,
|
||||
Object
|
||||
] },
|
||||
"variants": { type: Object },
|
||||
"inherit": {
|
||||
type: Boolean,
|
||||
default: void 0
|
||||
},
|
||||
"style": { type: Object },
|
||||
"transformTemplate": { type: Function },
|
||||
"transition": { type: Object },
|
||||
"onAnimationComplete": { type: Function },
|
||||
"onUpdate": { type: Function },
|
||||
"layout": {
|
||||
type: [Boolean, String],
|
||||
default: false
|
||||
},
|
||||
"layoutId": {
|
||||
type: String,
|
||||
default: void 0
|
||||
},
|
||||
"layoutScroll": {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
"layoutRoot": {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
"data-framer-portal-id": { type: String },
|
||||
"crossfade": {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
"layoutDependency": {
|
||||
type: null,
|
||||
default: void 0
|
||||
},
|
||||
"onBeforeLayoutMeasure": { type: Function },
|
||||
"onLayoutMeasure": { type: Function },
|
||||
"onLayoutAnimationStart": { type: Function },
|
||||
"onLayoutAnimationComplete": { type: Function },
|
||||
"globalPressTarget": { type: Boolean },
|
||||
"onPressStart": { type: Function },
|
||||
"onPress": { type: Function },
|
||||
"onPressCancel": { type: Function },
|
||||
"onHoverStart": { type: Function },
|
||||
"onHoverEnd": { type: Function },
|
||||
"inViewOptions": { type: Object },
|
||||
"onViewportEnter": { type: Function },
|
||||
"onViewportLeave": { type: Function },
|
||||
"drag": { type: [Boolean, String] },
|
||||
"dragSnapToOrigin": { type: [Boolean, String] },
|
||||
"dragDirectionLock": { type: Boolean },
|
||||
"dragPropagation": { type: Boolean },
|
||||
"dragConstraints": { type: [Boolean, Object] },
|
||||
"dragElastic": {
|
||||
type: [
|
||||
Boolean,
|
||||
Number,
|
||||
Object
|
||||
],
|
||||
default: .5
|
||||
},
|
||||
"dragMomentum": {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
"dragTransition": { type: Object },
|
||||
"dragListener": {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
"dragControls": { type: Object },
|
||||
"onDragStart": { type: Function },
|
||||
"onDragEnd": { type: Function },
|
||||
"onDrag": { type: Function },
|
||||
"onDirectionLock": { type: Function },
|
||||
"onDragTransitionEnd": { type: Function },
|
||||
"onMeasureDragConstraints": { type: Function },
|
||||
"onPanSessionStart": { type: Function },
|
||||
"onPanStart": { type: Function },
|
||||
"onPan": { type: Function },
|
||||
"onPanEnd": { type: Function }
|
||||
};
|
||||
//#endregion
|
||||
export { MotionComponentProps };
|
||||
Generated
Vendored
+115
@@ -0,0 +1,115 @@
|
||||
import { resolveMotionProps } from "../../utils/resolve-motion-props.mjs";
|
||||
import { getMotionElement } from "../hooks/use-motion-elm.mjs";
|
||||
import { injectLayoutGroup, injectMotion, provideMotion } from "../context.mjs";
|
||||
import { useLazyMotionContext } from "../lazy-motion/context.mjs";
|
||||
import { useMotionConfig } from "../motion-config/context.mjs";
|
||||
import { injectAnimatePresence } from "../animate-presence/presence.mjs";
|
||||
import { createSVGStyles, createStyles } from "../../state/style.mjs";
|
||||
import { MotionState } from "../../state/motion-state.mjs";
|
||||
import { isMotionValue } from "motion-dom";
|
||||
import { getCurrentInstance, onActivated, onBeforeUnmount, onBeforeUpdate, onDeactivated, onMounted, onUnmounted, onUpdated, ref, useAttrs, watch } from "vue";
|
||||
import { invariant, warning } from "hey-listen";
|
||||
//#region src/components/motion/use-motion-state.ts
|
||||
function useMotionState(props, bundle = {}) {
|
||||
const parentState = injectMotion(null);
|
||||
const layoutGroup = injectLayoutGroup({});
|
||||
const config = useMotionConfig();
|
||||
const presenceContext = injectAnimatePresence({});
|
||||
const lazyMotionContext = useLazyMotionContext({
|
||||
features: ref({}),
|
||||
strict: false
|
||||
});
|
||||
/**
|
||||
* If we're in development mode, check to make sure we're not rendering a motion component
|
||||
* as a child of LazyMotion, as this will break the file-size benefits of using it.
|
||||
*/
|
||||
if (process.env.NODE_ENV !== "production" && bundle.renderer && lazyMotionContext.strict) {
|
||||
const strictMessage = "You have rendered a `motion` component within a `LazyMotion` component. This will break tree shaking. Import and render a `m` component instead.";
|
||||
props.ignoreStrict ? warning(false, strictMessage) : invariant(false, strictMessage);
|
||||
}
|
||||
const attrs = useAttrs();
|
||||
function getProps() {
|
||||
return resolveMotionProps(props, {
|
||||
layoutGroup,
|
||||
presenceContext,
|
||||
config: config.value
|
||||
});
|
||||
}
|
||||
function getMotionProps() {
|
||||
return {
|
||||
...attrs,
|
||||
...getProps()
|
||||
};
|
||||
}
|
||||
const state = new MotionState(getMotionProps(), parentState, bundle);
|
||||
provideMotion(state);
|
||||
watch(lazyMotionContext.features, (bundle) => {
|
||||
state.setBundle(bundle);
|
||||
}, {
|
||||
immediate: true,
|
||||
flush: "pre"
|
||||
});
|
||||
function getAttrs() {
|
||||
const isSVG = state.type === "svg";
|
||||
const attrsProps = { ...attrs };
|
||||
Object.keys(attrs).forEach((key) => {
|
||||
if (isMotionValue(attrs[key])) attrsProps[key] = attrs[key].get();
|
||||
});
|
||||
const currentValues = state.visualElement?.latestValues || state.latestValues;
|
||||
let styleProps = {
|
||||
...props.style,
|
||||
...isSVG ? {} : currentValues
|
||||
};
|
||||
for (const key in styleProps) if (isMotionValue(styleProps[key])) styleProps[key] = styleProps[key].get();
|
||||
if (isSVG) {
|
||||
const { attrs: svgAttrs, style: svgStyle } = createSVGStyles({
|
||||
...currentValues,
|
||||
...styleProps
|
||||
}, state.options.as, props.style);
|
||||
Object.assign(attrsProps, svgAttrs);
|
||||
styleProps = svgStyle;
|
||||
}
|
||||
if (props.drag && props.dragListener !== false) Object.assign(styleProps, {
|
||||
userSelect: "none",
|
||||
WebkitUserSelect: "none",
|
||||
WebkitTouchCallout: "none",
|
||||
touchAction: props.drag === true ? "none" : `pan-${props.drag === "x" ? "y" : "x"}`
|
||||
});
|
||||
const style = createStyles(styleProps, props.transformTemplate);
|
||||
if (style) attrsProps.style = style;
|
||||
return attrsProps;
|
||||
}
|
||||
const instance = getCurrentInstance()?.proxy;
|
||||
function mountState() {
|
||||
const el = getMotionElement(instance?.$el);
|
||||
state.mount(el);
|
||||
}
|
||||
onBeforeUnmount(() => {
|
||||
state.updateOptions(getMotionProps());
|
||||
state.beforeUnmount();
|
||||
});
|
||||
onMounted(mountState);
|
||||
function unmountIfDetached() {
|
||||
if (!getMotionElement(instance?.$el)?.isConnected) state.unmount();
|
||||
}
|
||||
onUnmounted(unmountIfDetached);
|
||||
onDeactivated(unmountIfDetached);
|
||||
onActivated(() => {
|
||||
if (!state.isMounted()) mountState();
|
||||
});
|
||||
onBeforeUpdate(() => {
|
||||
state.updateOptions(getMotionProps());
|
||||
state.beforeUpdate();
|
||||
});
|
||||
onUpdated(() => {
|
||||
state.update();
|
||||
});
|
||||
return {
|
||||
getProps,
|
||||
getAttrs,
|
||||
layoutGroup,
|
||||
state
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
export { useMotionState };
|
||||
Generated
Vendored
+101
@@ -0,0 +1,101 @@
|
||||
import { useMotionState } from "./use-motion-state.mjs";
|
||||
import { MotionComponentProps } from "./props.mjs";
|
||||
import { Comment, cloneVNode, defineComponent, h, mergeProps } from "vue";
|
||||
//#region src/components/motion/utils.ts
|
||||
var componentMaxCache = /* @__PURE__ */ new Map();
|
||||
var componentMiniCache = /* @__PURE__ */ new Map();
|
||||
function renderSlotFragments(fragments) {
|
||||
if (!Array.isArray(fragments)) return [fragments];
|
||||
const result = [];
|
||||
for (const item of fragments) if (Array.isArray(item)) result.push(...item);
|
||||
else result.push(item);
|
||||
return result;
|
||||
}
|
||||
var SELF_CLOSING_TAGS = [
|
||||
"area",
|
||||
"img",
|
||||
"input"
|
||||
];
|
||||
function handlePrimitiveAndSlot(asTag, allAttrs, slots) {
|
||||
if (typeof asTag === "string" && SELF_CLOSING_TAGS.includes(asTag)) return h(asTag, allAttrs);
|
||||
if (asTag === "template") {
|
||||
if (!slots.default) return null;
|
||||
const children = renderSlotFragments(slots.default());
|
||||
const firstNonCommentIndex = children.findIndex((child) => child.type !== Comment);
|
||||
if (firstNonCommentIndex === -1) return children;
|
||||
const firstNonComment = children[firstNonCommentIndex];
|
||||
delete firstNonComment.props?.ref;
|
||||
const mergedProps = firstNonComment.props ? mergeProps(allAttrs, firstNonComment.props) : allAttrs;
|
||||
if (allAttrs.class && firstNonComment.props?.class) delete firstNonComment.props.class;
|
||||
const cloned = cloneVNode(firstNonComment, mergedProps);
|
||||
for (const prop in mergedProps) if (prop.startsWith("on")) {
|
||||
cloned.props ||= {};
|
||||
cloned.props[prop] = mergedProps[prop];
|
||||
}
|
||||
if (children.length === 1) return cloned;
|
||||
children[firstNonCommentIndex] = cloned;
|
||||
return children;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Creates a motion component from a base component or HTML tag
|
||||
* Caches string-based components for reuse
|
||||
*/
|
||||
function createMotionComponent(component, options = {}) {
|
||||
const isString = typeof component === "string";
|
||||
const name = isString ? component : component.name || "";
|
||||
const componentCache = options.renderer ? componentMaxCache : componentMiniCache;
|
||||
if (isString && componentCache.has(component)) return componentCache.get(component);
|
||||
const motionComponent = defineComponent({
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
...MotionComponentProps,
|
||||
as: {
|
||||
type: [String, Object],
|
||||
default: component || "div"
|
||||
}
|
||||
},
|
||||
name: name ? `motion.${name}` : "Motion",
|
||||
setup(props, { slots }) {
|
||||
const { getProps, getAttrs, state } = useMotionState(props, options);
|
||||
/**
|
||||
* Vue reapplies all styles every render, include style properties and calculated initially styles get reapplied every render.
|
||||
* To prevent this, reapply the current motion state styles in vnode updated lifecycle
|
||||
*/
|
||||
function onVnodeUpdated() {
|
||||
const el = state.element;
|
||||
if ((!(typeof props.as === "object") || props.asChild) && el) {
|
||||
const { style } = getAttrs();
|
||||
if (style) for (const [key, val] of Object.entries(style)) el.style[key] = val;
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
const motionProps = getProps();
|
||||
const motionAttrs = getAttrs();
|
||||
const asTag = props.asChild ? "template" : props.as;
|
||||
const allAttrs = {
|
||||
...props.forwardMotionProps ? motionProps : {},
|
||||
...motionAttrs,
|
||||
onVnodeUpdated
|
||||
};
|
||||
const primitiveOrSlotResult = handlePrimitiveAndSlot(asTag, allAttrs, slots);
|
||||
if (primitiveOrSlotResult !== null) return primitiveOrSlotResult;
|
||||
return h(asTag, { ...allAttrs }, slots);
|
||||
};
|
||||
}
|
||||
});
|
||||
if (isString) componentCache.set(component, motionComponent);
|
||||
return motionComponent;
|
||||
}
|
||||
function createMotionComponentWithFeatures(featureBundle) {
|
||||
return new Proxy({}, { get(_, prop) {
|
||||
if (prop === "create") return (component, options) => createMotionComponent(component, {
|
||||
...options,
|
||||
...featureBundle
|
||||
});
|
||||
return createMotionComponent(prop, { ...featureBundle });
|
||||
} });
|
||||
}
|
||||
//#endregion
|
||||
export { createMotionComponent, createMotionComponentWithFeatures };
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
import Group_vue_vue_type_script_setup_true_lang_default from "./Group.vue_vue_type_script_setup_true_lang.mjs";
|
||||
//#region src/components/reorder/Group.vue
|
||||
var Group_default = Group_vue_vue_type_script_setup_true_lang_default;
|
||||
//#endregion
|
||||
export { Group_default as default };
|
||||
Generated
Vendored
+207
@@ -0,0 +1,207 @@
|
||||
import { useDomRef } from "../../utils/use-dom-ref.mjs";
|
||||
import { Motion } from "../motion/index.mjs";
|
||||
import { reorderContextProvider } from "./context.mjs";
|
||||
import { checkReorder, detectAxis } from "./utils.mjs";
|
||||
import { computed, createBlock, createTextVNode, defineComponent, mergeProps, onUpdated, openBlock, ref, renderSlot, toDisplayString, unref, useAttrs, withCtx } from "vue";
|
||||
import { invariant } from "hey-listen";
|
||||
//#region src/components/reorder/Group.vue?vue&type=script&setup=true&lang.ts
|
||||
var Group_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
|
||||
name: "ReorderGroup",
|
||||
inheritAttrs: false,
|
||||
__name: "Group",
|
||||
props: {
|
||||
axis: {},
|
||||
"onUpdate:values": {},
|
||||
values: {},
|
||||
as: { default: "ul" },
|
||||
asChild: { type: Boolean },
|
||||
whileDrag: {},
|
||||
whileHover: {},
|
||||
whilePress: {},
|
||||
whileInView: {},
|
||||
whileFocus: {},
|
||||
forwardMotionProps: { type: Boolean },
|
||||
ignoreStrict: { type: Boolean },
|
||||
custom: {},
|
||||
initial: {
|
||||
type: [
|
||||
String,
|
||||
Array,
|
||||
Object,
|
||||
Boolean
|
||||
],
|
||||
default: void 0
|
||||
},
|
||||
animate: {},
|
||||
exit: {},
|
||||
variants: {},
|
||||
inherit: {
|
||||
type: Boolean,
|
||||
default: void 0
|
||||
},
|
||||
style: {},
|
||||
transformTemplate: {},
|
||||
transition: {},
|
||||
onAnimationComplete: {},
|
||||
onUpdate: {},
|
||||
onAnimationStart: {},
|
||||
layout: { type: [Boolean, String] },
|
||||
layoutId: {},
|
||||
layoutScroll: { type: Boolean },
|
||||
layoutRoot: { type: Boolean },
|
||||
"data-framer-portal-id": {},
|
||||
crossfade: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
layoutDependency: {},
|
||||
onBeforeLayoutMeasure: {},
|
||||
onLayoutMeasure: {},
|
||||
onLayoutAnimationStart: {},
|
||||
onLayoutAnimationComplete: {},
|
||||
globalPressTarget: { type: Boolean },
|
||||
onPressStart: {},
|
||||
onPress: {},
|
||||
onPressCancel: {},
|
||||
onHoverStart: {},
|
||||
onHoverEnd: {},
|
||||
inViewOptions: {},
|
||||
inView: {},
|
||||
onViewportEnter: {},
|
||||
onViewportLeave: {},
|
||||
drag: { type: [Boolean, String] },
|
||||
dragSnapToOrigin: { type: [Boolean, String] },
|
||||
dragDirectionLock: { type: Boolean },
|
||||
dragPropagation: { type: Boolean },
|
||||
dragConstraints: { type: [Boolean, Object] },
|
||||
dragElastic: {
|
||||
type: [
|
||||
Boolean,
|
||||
Number,
|
||||
Object
|
||||
],
|
||||
default: .5
|
||||
},
|
||||
dragMomentum: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
dragTransition: {},
|
||||
dragListener: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
dragControls: {},
|
||||
onDragStart: {},
|
||||
onDragEnd: {},
|
||||
onDrag: {},
|
||||
onDirectionLock: {},
|
||||
onDragTransitionEnd: {},
|
||||
onMeasureDragConstraints: {},
|
||||
onPanSessionStart: {},
|
||||
onPanStart: {},
|
||||
onPan: {},
|
||||
onPanEnd: {},
|
||||
onFocus: {},
|
||||
onBlur: {}
|
||||
},
|
||||
setup(__props) {
|
||||
const props = __props;
|
||||
const itemLayouts = /* @__PURE__ */ new Map();
|
||||
const detectedAxis = ref("y");
|
||||
let isReordering = false;
|
||||
const hasReordered = ref(false);
|
||||
const axis = computed(() => props.axis || detectedAxis.value);
|
||||
function warning() {
|
||||
invariant(Boolean(props.values), "Reorder.Group must be provided a values prop");
|
||||
}
|
||||
onUpdated(() => {
|
||||
isReordering = false;
|
||||
});
|
||||
const groupRef = useDomRef();
|
||||
reorderContextProvider({
|
||||
groupRef,
|
||||
axis,
|
||||
hasReordered,
|
||||
registerItem: (value, layout) => {
|
||||
const valuesSet = new Set(props.values);
|
||||
itemLayouts.forEach((_, itemValue) => {
|
||||
if (!valuesSet.has(itemValue)) itemLayouts.delete(itemValue);
|
||||
});
|
||||
itemLayouts.set(value, layout);
|
||||
if (!props.axis) {
|
||||
const nextAxis = detectAxis(props.values.flatMap((itemValue) => {
|
||||
const itemLayout = itemLayouts.get(itemValue);
|
||||
return itemLayout ? [itemLayout] : [];
|
||||
}));
|
||||
if (nextAxis !== detectedAxis.value) detectedAxis.value = nextAxis;
|
||||
}
|
||||
},
|
||||
updateOrder: (item, offset, velocity) => {
|
||||
if (isReordering) return;
|
||||
const order = props.values.flatMap((value) => {
|
||||
const layout = itemLayouts.get(value);
|
||||
return layout ? [{
|
||||
value,
|
||||
layout
|
||||
}] : [];
|
||||
});
|
||||
const element = groupRef.value;
|
||||
const direction = element?.ownerDocument.defaultView?.getComputedStyle(element).direction === "rtl" ? "rtl" : "ltr";
|
||||
const newOrder = checkReorder(order, item, offset, velocity, axis.value, direction);
|
||||
if (order !== newOrder) {
|
||||
isReordering = true;
|
||||
hasReordered.value = true;
|
||||
const newValues = [...props.values];
|
||||
const measuredIndexes = order.map(({ value }) => props.values.indexOf(value));
|
||||
newOrder.forEach(({ value }, index) => {
|
||||
newValues[measuredIndexes[index]] = value;
|
||||
});
|
||||
props["onUpdate:values"]?.(newValues);
|
||||
}
|
||||
}
|
||||
});
|
||||
const attrs = useAttrs();
|
||||
/**
|
||||
* A release that ends a reordering drag isn't a click. Browsers only click an
|
||||
* element holding both the press and release targets, so swallow it here,
|
||||
* above every item (#2682). The flag is cleared when the drag ends — a frame
|
||||
* after the release's click — or on the next press. Clicks with no pointer
|
||||
* press behind them (detail 0, e.g. keyboard) are left alone.
|
||||
*/
|
||||
function handleClickCapture(event) {
|
||||
if (hasReordered.value && event.detail) {
|
||||
hasReordered.value = false;
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
function handlePointerdownCapture() {
|
||||
hasReordered.value = false;
|
||||
}
|
||||
function bindProps() {
|
||||
const { axis, values, "onUpdate:values": onUpdateValues, ...rest } = props;
|
||||
return {
|
||||
...attrs,
|
||||
...rest,
|
||||
style: {
|
||||
overflowAnchor: "none",
|
||||
...rest.style
|
||||
}
|
||||
};
|
||||
}
|
||||
return (_ctx, _cache) => {
|
||||
return openBlock(), createBlock(unref(Motion), mergeProps(bindProps(), {
|
||||
ref_key: "groupRef",
|
||||
ref: groupRef,
|
||||
onPointerdownCapture: handlePointerdownCapture,
|
||||
onClickCapture: handleClickCapture
|
||||
}), {
|
||||
default: withCtx(() => [renderSlot(_ctx.$slots, "default"), createTextVNode(" " + toDisplayString(warning()), 1)]),
|
||||
_: 3
|
||||
}, 16);
|
||||
};
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
export { Group_vue_vue_type_script_setup_true_lang_default as default };
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
import Item_vue_vue_type_script_setup_true_lang_default from "./Item.vue_vue_type_script_setup_true_lang.mjs";
|
||||
//#region src/components/reorder/Item.vue
|
||||
var Item_default = Item_vue_vue_type_script_setup_true_lang_default;
|
||||
//#endregion
|
||||
export { Item_default as default };
|
||||
Generated
Vendored
+191
@@ -0,0 +1,191 @@
|
||||
import { Motion } from "../motion/index.mjs";
|
||||
import { useReorderContext } from "./context.mjs";
|
||||
import { useDefaultMotionValue } from "./utils.mjs";
|
||||
import { useTransform } from "../../value/use-transform.mjs";
|
||||
import { autoScrollIfNeeded, resetAutoScrollState } from "./auto-scroll.mjs";
|
||||
import { computed, createBlock, defineComponent, mergeProps, openBlock, ref, renderSlot, toRefs, unref, useAttrs, withCtx } from "vue";
|
||||
import { invariant } from "hey-listen";
|
||||
//#region src/components/reorder/Item.vue?vue&type=script&setup=true&lang.ts
|
||||
var Item_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
|
||||
name: "ReorderItem",
|
||||
inheritAttrs: false,
|
||||
__name: "Item",
|
||||
props: {
|
||||
value: {},
|
||||
layout: {
|
||||
type: [Boolean, String],
|
||||
default: true
|
||||
},
|
||||
as: { default: "li" },
|
||||
asChild: { type: Boolean },
|
||||
whileDrag: { default: void 0 },
|
||||
whileHover: {},
|
||||
whilePress: {},
|
||||
whileInView: {},
|
||||
whileFocus: {},
|
||||
forwardMotionProps: { type: Boolean },
|
||||
ignoreStrict: { type: Boolean },
|
||||
custom: {},
|
||||
initial: {
|
||||
type: [
|
||||
String,
|
||||
Array,
|
||||
Object,
|
||||
Boolean
|
||||
],
|
||||
default: void 0
|
||||
},
|
||||
animate: { default: void 0 },
|
||||
exit: {},
|
||||
variants: {},
|
||||
inherit: {
|
||||
type: Boolean,
|
||||
default: void 0
|
||||
},
|
||||
style: {},
|
||||
transformTemplate: {},
|
||||
transition: {},
|
||||
onAnimationComplete: {},
|
||||
onUpdate: {},
|
||||
onAnimationStart: {},
|
||||
layoutId: { default: void 0 },
|
||||
layoutScroll: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
layoutRoot: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
"data-framer-portal-id": {},
|
||||
crossfade: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
layoutDependency: {},
|
||||
onBeforeLayoutMeasure: {},
|
||||
onLayoutMeasure: {},
|
||||
onLayoutAnimationStart: {},
|
||||
onLayoutAnimationComplete: {},
|
||||
globalPressTarget: { type: Boolean },
|
||||
onPressStart: {},
|
||||
onPress: {},
|
||||
onPressCancel: {},
|
||||
onHoverStart: {},
|
||||
onHoverEnd: {},
|
||||
inViewOptions: {},
|
||||
inView: { default: void 0 },
|
||||
onViewportEnter: {},
|
||||
onViewportLeave: {},
|
||||
drag: {
|
||||
type: [Boolean, String],
|
||||
default: void 0
|
||||
},
|
||||
dragSnapToOrigin: { type: [Boolean, String] },
|
||||
dragDirectionLock: { type: Boolean },
|
||||
dragPropagation: { type: Boolean },
|
||||
dragConstraints: { type: [Boolean, Object] },
|
||||
dragElastic: {
|
||||
type: [
|
||||
Boolean,
|
||||
Number,
|
||||
Object
|
||||
],
|
||||
default: .5
|
||||
},
|
||||
dragMomentum: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
dragTransition: {},
|
||||
dragListener: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
dragControls: {},
|
||||
onDragStart: {},
|
||||
onDragEnd: {},
|
||||
onDrag: {},
|
||||
onDirectionLock: {},
|
||||
onDragTransitionEnd: {},
|
||||
onMeasureDragConstraints: {},
|
||||
onPanSessionStart: {},
|
||||
onPanStart: {},
|
||||
onPan: {},
|
||||
onPanEnd: {},
|
||||
onFocus: {},
|
||||
onBlur: {}
|
||||
},
|
||||
setup(__props) {
|
||||
const props = __props;
|
||||
const { style } = toRefs(props);
|
||||
const context = useReorderContext();
|
||||
const point = {
|
||||
x: useDefaultMotionValue(style.value?.x),
|
||||
y: useDefaultMotionValue(style.value?.y)
|
||||
};
|
||||
const zIndex = useTransform([point.x, point.y], ([latestX, latestY]) => latestX || latestY ? 1 : "unset");
|
||||
function warning() {
|
||||
invariant(Boolean(context), "Reorder.Item must be a descendant of Reorder.Group");
|
||||
}
|
||||
warning();
|
||||
const { axis, registerItem, updateOrder, groupRef, hasReordered } = context;
|
||||
const attrs = useAttrs();
|
||||
function bindProps() {
|
||||
const { value, onDragStart, onDragEnd, onDrag, ...rest } = props;
|
||||
return {
|
||||
...attrs,
|
||||
...rest,
|
||||
style: {
|
||||
...style.value,
|
||||
x: point.x,
|
||||
y: point.y,
|
||||
zIndex
|
||||
}
|
||||
};
|
||||
}
|
||||
const drag = computed(() => {
|
||||
if (props.drag !== void 0) return props.drag;
|
||||
return axis?.value === "xy" ? true : axis?.value;
|
||||
});
|
||||
const isDragging = ref(false);
|
||||
function handleDrag(event, gesturePoint) {
|
||||
const { velocity, point: pointerPoint } = gesturePoint;
|
||||
const offset = {
|
||||
x: point.x.get(),
|
||||
y: point.y.get()
|
||||
};
|
||||
updateOrder?.(props.value, offset, velocity);
|
||||
const scrollAxis = axis?.value === "xy" ? Math.abs(velocity.x) > Math.abs(velocity.y) ? "x" : "y" : axis?.value;
|
||||
autoScrollIfNeeded(groupRef?.value, pointerPoint?.[scrollAxis], scrollAxis, velocity?.[scrollAxis]);
|
||||
if (!isDragging.value) isDragging.value = true;
|
||||
props.onDrag?.(event, gesturePoint);
|
||||
}
|
||||
function handleDragEnd(event, gesturePoint) {
|
||||
if (hasReordered) hasReordered.value = false;
|
||||
isDragging.value = false;
|
||||
resetAutoScrollState();
|
||||
props.onDragEnd?.(event, gesturePoint);
|
||||
}
|
||||
function handleDragStart(event, gesturePoint) {
|
||||
props.onDragStart?.(event, gesturePoint);
|
||||
}
|
||||
return (_ctx, _cache) => {
|
||||
return openBlock(), createBlock(unref(Motion), mergeProps(bindProps(), {
|
||||
drag: drag.value,
|
||||
"drag-snap-to-origin": true,
|
||||
onDrag: handleDrag,
|
||||
onDragEnd: handleDragEnd,
|
||||
onDragStart: handleDragStart,
|
||||
onLayoutMeasure: _cache[0] || (_cache[0] = (measured) => {
|
||||
unref(registerItem)?.(__props.value, measured);
|
||||
})
|
||||
}), {
|
||||
default: withCtx(() => [renderSlot(_ctx.$slots, "default", { isDragging: isDragging.value })]),
|
||||
_: 3
|
||||
}, 16, ["drag"]);
|
||||
};
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
export { Item_vue_vue_type_script_setup_true_lang_default as default };
|
||||
Generated
Vendored
+93
@@ -0,0 +1,93 @@
|
||||
//#region src/components/reorder/auto-scroll.ts
|
||||
var threshold = 50;
|
||||
var maxSpeed = 25;
|
||||
var overflowStyles = /* @__PURE__ */ new Set(["auto", "scroll"]);
|
||||
var initialScrollLimits = /* @__PURE__ */ new WeakMap();
|
||||
var activeScrollEdge = /* @__PURE__ */ new WeakMap();
|
||||
var currentGroupElement = null;
|
||||
function resetAutoScrollState() {
|
||||
if (currentGroupElement) {
|
||||
const scrollableAncestor = findScrollableAncestor(currentGroupElement, "y");
|
||||
if (scrollableAncestor) {
|
||||
activeScrollEdge.delete(scrollableAncestor);
|
||||
initialScrollLimits.delete(scrollableAncestor);
|
||||
}
|
||||
const scrollableAncestorX = findScrollableAncestor(currentGroupElement, "x");
|
||||
if (scrollableAncestorX && scrollableAncestorX !== scrollableAncestor) {
|
||||
activeScrollEdge.delete(scrollableAncestorX);
|
||||
initialScrollLimits.delete(scrollableAncestorX);
|
||||
}
|
||||
currentGroupElement = null;
|
||||
}
|
||||
}
|
||||
function isDocumentScroll(element) {
|
||||
return element === document.body || element === document.documentElement;
|
||||
}
|
||||
function isScrollableElement(element, axis) {
|
||||
const style = getComputedStyle(element);
|
||||
const overflow = axis === "x" ? style.overflowX : style.overflowY;
|
||||
return overflowStyles.has(overflow) || isDocumentScroll(element);
|
||||
}
|
||||
function findScrollableAncestor(element, axis) {
|
||||
let current = element?.parentElement;
|
||||
while (current) {
|
||||
if (isScrollableElement(current, axis)) return current;
|
||||
current = current.parentElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function getScrollAmount(pointerPosition, scrollElement, axis) {
|
||||
const rect = scrollElement.getBoundingClientRect();
|
||||
const start = axis === "x" ? Math.max(0, rect.left) : Math.max(0, rect.top);
|
||||
const end = axis === "x" ? Math.min(window.innerWidth, rect.right) : Math.min(window.innerHeight, rect.bottom);
|
||||
const distanceFromStart = pointerPosition - start;
|
||||
const distanceFromEnd = end - pointerPosition;
|
||||
if (distanceFromStart < threshold) {
|
||||
const intensity = 1 - distanceFromStart / threshold;
|
||||
return {
|
||||
amount: -25 * intensity * intensity,
|
||||
edge: "start"
|
||||
};
|
||||
} else if (distanceFromEnd < threshold) {
|
||||
const intensity = 1 - distanceFromEnd / threshold;
|
||||
return {
|
||||
amount: maxSpeed * intensity * intensity,
|
||||
edge: "end"
|
||||
};
|
||||
}
|
||||
return {
|
||||
amount: 0,
|
||||
edge: null
|
||||
};
|
||||
}
|
||||
function autoScrollIfNeeded(groupElement, pointerPosition, axis, velocity) {
|
||||
if (!groupElement) return;
|
||||
currentGroupElement = groupElement;
|
||||
const scrollableAncestor = findScrollableAncestor(groupElement, axis);
|
||||
if (!scrollableAncestor) return;
|
||||
const { amount: scrollAmount, edge } = getScrollAmount(pointerPosition - (axis === "x" ? window.scrollX : window.scrollY), scrollableAncestor, axis);
|
||||
if (edge === null) {
|
||||
activeScrollEdge.delete(scrollableAncestor);
|
||||
initialScrollLimits.delete(scrollableAncestor);
|
||||
return;
|
||||
}
|
||||
const currentActiveEdge = activeScrollEdge.get(scrollableAncestor);
|
||||
const isDocument = isDocumentScroll(scrollableAncestor);
|
||||
if (currentActiveEdge !== edge) {
|
||||
if (!(edge === "start" && velocity < 0 || edge === "end" && velocity > 0)) return;
|
||||
activeScrollEdge.set(scrollableAncestor, edge);
|
||||
const maxScroll = axis === "x" ? scrollableAncestor.scrollWidth - (isDocument ? window.innerWidth : scrollableAncestor.clientWidth) : scrollableAncestor.scrollHeight - (isDocument ? window.innerHeight : scrollableAncestor.clientHeight);
|
||||
initialScrollLimits.set(scrollableAncestor, maxScroll);
|
||||
}
|
||||
if (scrollAmount > 0) {
|
||||
const initialLimit = initialScrollLimits.get(scrollableAncestor);
|
||||
if ((axis === "x" ? isDocument ? window.scrollX : scrollableAncestor.scrollLeft : isDocument ? window.scrollY : scrollableAncestor.scrollTop) >= initialLimit) return;
|
||||
}
|
||||
if (axis === "x") {
|
||||
if (isDocument) window.scrollBy({ left: scrollAmount });
|
||||
else scrollableAncestor.scrollLeft += scrollAmount;
|
||||
} else if (isDocument) window.scrollBy({ top: scrollAmount });
|
||||
else scrollableAncestor.scrollTop += scrollAmount;
|
||||
}
|
||||
//#endregion
|
||||
export { autoScrollIfNeeded, resetAutoScrollState };
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
import { createContext } from "../../utils/createContext.mjs";
|
||||
//#region src/components/reorder/context.ts
|
||||
var [useReorderContext, reorderContextProvider] = createContext("ReorderContext");
|
||||
//#endregion
|
||||
export { reorderContextProvider, useReorderContext };
|
||||
Generated
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
import Group_default from "./Group.mjs";
|
||||
import Item_default from "./Item.mjs";
|
||||
//#region src/components/reorder/index.ts
|
||||
var ReorderGroup = Group_default;
|
||||
var ReorderItem = Item_default;
|
||||
var Reorder = {
|
||||
Group: Group_default,
|
||||
Item: Item_default
|
||||
};
|
||||
//#endregion
|
||||
export { Reorder, ReorderGroup, ReorderItem };
|
||||
Generated
Vendored
+92
@@ -0,0 +1,92 @@
|
||||
import { isMotionValue, mixNumber, motionValue } from "motion-dom";
|
||||
import { moveItem } from "motion-utils";
|
||||
//#region src/components/reorder/utils.ts
|
||||
function checkReorder(order, value, offset, velocity, axis, direction = "ltr") {
|
||||
const index = order.findIndex((item) => item.value === value);
|
||||
if (index === -1) return order;
|
||||
if (axis === "xy") {
|
||||
const { layout } = order[index];
|
||||
const center = {
|
||||
x: mixNumber(layout.x.min, layout.x.max, .5) + offset.x,
|
||||
y: mixNumber(layout.y.min, layout.y.max, .5) + offset.y
|
||||
};
|
||||
const lines = getLines(order);
|
||||
const sourceLine = lines.find((line) => line.items.includes(order[index]));
|
||||
const targetLine = lines.reduce((closest, line) => distanceToLine(center.y, line) < distanceToLine(center.y, closest) ? line : closest);
|
||||
if (targetLine !== sourceLine) return moveToLine(order, index, center.x, targetLine, direction);
|
||||
const currentDistance = distanceToBox(center, layout);
|
||||
let target = -1;
|
||||
let targetDistance = currentDistance;
|
||||
order.forEach((item, targetIndex) => {
|
||||
if (targetIndex === index) return;
|
||||
const distance = distanceToBox(center, item.layout);
|
||||
if (distance < targetDistance) {
|
||||
target = targetIndex;
|
||||
targetDistance = distance;
|
||||
}
|
||||
});
|
||||
return target === -1 ? order : moveItem(order, index, index + Math.sign(target - index));
|
||||
}
|
||||
if (!velocity[axis]) return order;
|
||||
const nextOffset = velocity[axis] > 0 ? 1 : -1;
|
||||
const nextItem = order[index + nextOffset];
|
||||
if (!nextItem) return order;
|
||||
const itemLayout = order[index].layout[axis];
|
||||
const nextLayout = nextItem.layout[axis];
|
||||
const nextItemCenter = mixNumber(nextLayout.min, nextLayout.max, .5);
|
||||
if (nextOffset === 1 && itemLayout.max + offset[axis] > nextItemCenter || nextOffset === -1 && itemLayout.min + offset[axis] < nextItemCenter) return moveItem(order, index, index + nextOffset);
|
||||
return order;
|
||||
}
|
||||
function getLines(order) {
|
||||
const lines = [];
|
||||
order.forEach((item) => {
|
||||
const { min, max } = item.layout.y;
|
||||
const line = lines[lines.length - 1];
|
||||
if (!line || min >= line.max || max <= line.min) lines.push({
|
||||
items: [item],
|
||||
min,
|
||||
max
|
||||
});
|
||||
else {
|
||||
line.items.push(item);
|
||||
line.min = Math.min(line.min, min);
|
||||
line.max = Math.max(line.max, max);
|
||||
}
|
||||
});
|
||||
return lines;
|
||||
}
|
||||
function distanceToLine(y, line) {
|
||||
return y < line.min ? line.min - y : y > line.max ? y - line.max : 0;
|
||||
}
|
||||
function moveToLine(order, index, x, line, direction) {
|
||||
const remaining = order.filter((_, itemIndex) => itemIndex !== index);
|
||||
const before = line.items.find((item) => {
|
||||
const center = mixNumber(item.layout.x.min, item.layout.x.max, .5);
|
||||
return direction === "ltr" ? x < center : x > center;
|
||||
});
|
||||
const targetIndex = before ? remaining.indexOf(before) : remaining.indexOf(line.items[line.items.length - 1]) + 1;
|
||||
const nextOrder = [...remaining];
|
||||
nextOrder.splice(targetIndex, 0, order[index]);
|
||||
return nextOrder.every((item, itemIndex) => item === order[itemIndex]) ? order : nextOrder;
|
||||
}
|
||||
function distanceToBox(point, box) {
|
||||
const x = Math.max(box.x.min - point.x, 0, point.x - box.x.max);
|
||||
const y = Math.max(box.y.min - point.y, 0, point.y - box.y.max);
|
||||
return x * x + y * y;
|
||||
}
|
||||
var isSeparated = (a, b) => a.max <= b.min || b.max <= a.min;
|
||||
function detectAxis(layouts) {
|
||||
let x = false;
|
||||
let y = false;
|
||||
for (let i = 0; i < layouts.length; i++) for (let j = i + 1; j < layouts.length; j++) {
|
||||
x ||= isSeparated(layouts[i].x, layouts[j].x);
|
||||
y ||= isSeparated(layouts[i].y, layouts[j].y);
|
||||
if (x && y) return "xy";
|
||||
}
|
||||
return x ? "x" : "y";
|
||||
}
|
||||
function useDefaultMotionValue(value, defaultValue = 0) {
|
||||
return isMotionValue(value) ? value : motionValue(defaultValue);
|
||||
}
|
||||
//#endregion
|
||||
export { checkReorder, detectAxis, useDefaultMotionValue };
|
||||
Generated
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
import { ref } from "vue";
|
||||
//#region src/components/use-force-update.ts
|
||||
function useForceUpdate() {
|
||||
const key = ref(0);
|
||||
function forceUpdate() {
|
||||
key.value++;
|
||||
}
|
||||
return [forceUpdate, key];
|
||||
}
|
||||
//#endregion
|
||||
export { useForceUpdate };
|
||||
Generated
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
import { injectLayoutGroup, provideLayoutGroup } from "./context.mjs";
|
||||
import { useForceUpdate } from "./use-force-update.mjs";
|
||||
import { nodeGroup } from "motion-dom";
|
||||
//#region src/components/use-layout-group.ts
|
||||
/**
|
||||
* Hook to create and manage a layout group
|
||||
* Handles group inheritance, force updates, and context management
|
||||
*/
|
||||
function useLayoutGroupProvider(props) {
|
||||
const parentGroup = injectLayoutGroup(null);
|
||||
const [forceRender, key] = useForceUpdate();
|
||||
const context = {
|
||||
id: getGroupId(props, parentGroup),
|
||||
group: getGroup(props, parentGroup),
|
||||
forceRender,
|
||||
key
|
||||
};
|
||||
provideLayoutGroup(context);
|
||||
return context;
|
||||
}
|
||||
function useLayoutGroup() {
|
||||
const { forceRender } = injectLayoutGroup({ forceRender: () => {} });
|
||||
return { forceRender };
|
||||
}
|
||||
/**
|
||||
* Determines the group ID based on inheritance rules
|
||||
*/
|
||||
function getGroupId(props, parentGroup) {
|
||||
const shouldInherit = props.inherit === true || props.inherit === "id";
|
||||
const parentId = parentGroup?.id;
|
||||
if (shouldInherit && parentId) return props.id ? `${parentId}-${props.id}` : parentId;
|
||||
return props.id;
|
||||
}
|
||||
/**
|
||||
* Creates or inherits a node group based on inheritance rules
|
||||
*/
|
||||
function getGroup(props, parentGroup) {
|
||||
return props.inherit === true || props.inherit === "group" ? parentGroup?.group || nodeGroup() : nodeGroup();
|
||||
}
|
||||
//#endregion
|
||||
export { useLayoutGroup, useLayoutGroupProvider };
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
//#region src/config.ts
|
||||
/**
|
||||
* Global configuration for motion-v.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { motionGlobalConfig } from 'motion-v'
|
||||
* motionGlobalConfig.motionAttribute = 'data-mv'
|
||||
* ```
|
||||
*/
|
||||
var motionGlobalConfig = {
|
||||
/**
|
||||
* The data attribute used to identify motion elements in the DOM.
|
||||
* Used for AnimatePresence exit detection and VE parent hierarchy resolution.
|
||||
*/
|
||||
motionAttribute: "data-ap" };
|
||||
//#endregion
|
||||
export { motionGlobalConfig };
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
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:
|
||||
* - `<div v-motion :initial="{...}" :animate="{...}" />` (props on element)
|
||||
* - `<div v-motion="{ initial: {...}, animate: {...} }" />` (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 };
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
import { addPointerInfo } from "./event-info.mjs";
|
||||
import { addDomEvent } from "motion-dom";
|
||||
//#region src/events/add-pointer-event.ts
|
||||
function addPointerEvent(target, eventName, handler, options) {
|
||||
return addDomEvent(target, eventName, addPointerInfo(handler), options);
|
||||
}
|
||||
//#endregion
|
||||
export { addPointerEvent };
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { isPrimaryPointer } from "motion-dom";
|
||||
//#region src/events/event-info.ts
|
||||
function extractEventInfo(event, pointType = "page") {
|
||||
return { point: {
|
||||
x: event[`${pointType}X`],
|
||||
y: event[`${pointType}Y`]
|
||||
} };
|
||||
}
|
||||
function addPointerInfo(handler) {
|
||||
return (event) => isPrimaryPointer(event) && handler(event, extractEventInfo(event));
|
||||
}
|
||||
//#endregion
|
||||
export { addPointerInfo, extractEventInfo };
|
||||
Generated
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
import { Feature } from "../feature.mjs";
|
||||
import { isHidden } from "../../utils/is-hidden.mjs";
|
||||
import { createAnimationState, isAnimationControls } from "motion-dom";
|
||||
//#region src/features/animation/animation.ts
|
||||
var AnimationFeature = class extends Feature {
|
||||
static {
|
||||
this.key = "animation";
|
||||
}
|
||||
constructor(state) {
|
||||
super(state);
|
||||
const ve = state.visualElement;
|
||||
ve.animationState ||= createAnimationState(ve);
|
||||
}
|
||||
updateAnimationControlsSubscription() {
|
||||
const { animate } = this.state.options;
|
||||
if (isAnimationControls(animate)) this.unmountControls = animate.subscribe(this.state.visualElement);
|
||||
}
|
||||
/**
|
||||
* Subscribe any provided AnimationControls to the component's VisualElement
|
||||
*/
|
||||
mount() {
|
||||
if (!!isHidden(this.state.element)) this.state.setActive("exit", true);
|
||||
else this.state.visualElement.animationState?.animateChanges();
|
||||
this.updateAnimationControlsSubscription();
|
||||
}
|
||||
update() {
|
||||
this.state.visualElement.animationState?.animateChanges();
|
||||
const { animate } = this.state.visualElement.getProps();
|
||||
const { animate: prevAnimate } = this.state.visualElement.prevProps || {};
|
||||
if (animate !== prevAnimate) this.updateAnimationControlsSubscription();
|
||||
}
|
||||
unmount() {
|
||||
this.state.visualElement.animationState.reset();
|
||||
this.unmountControls?.();
|
||||
}
|
||||
};
|
||||
//#endregion
|
||||
export { AnimationFeature };
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { AnimationFeature } from "./animation/animation.mjs";
|
||||
import { ExitFeature } from "./exit/exit.mjs";
|
||||
import { createVisualElement } from "../state/create-visual-element.mjs";
|
||||
import { PressGesture } from "./gestures/press/index.mjs";
|
||||
import { HoverGesture } from "./gestures/hover/index.mjs";
|
||||
import { InViewGesture } from "./gestures/in-view/index.mjs";
|
||||
import { FocusGesture } from "./gestures/focus/index.mjs";
|
||||
//#region src/features/dom-animation.ts
|
||||
var domAnimation = {
|
||||
renderer: createVisualElement,
|
||||
features: [
|
||||
AnimationFeature,
|
||||
ExitFeature,
|
||||
PressGesture,
|
||||
HoverGesture,
|
||||
InViewGesture,
|
||||
FocusGesture
|
||||
]
|
||||
};
|
||||
//#endregion
|
||||
export { domAnimation };
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { AnimationFeature } from "./animation/animation.mjs";
|
||||
import { ExitFeature } from "./exit/exit.mjs";
|
||||
import { createVisualElement } from "../state/create-visual-element.mjs";
|
||||
import { PressGesture } from "./gestures/press/index.mjs";
|
||||
import { HoverGesture } from "./gestures/hover/index.mjs";
|
||||
import { InViewGesture } from "./gestures/in-view/index.mjs";
|
||||
import { FocusGesture } from "./gestures/focus/index.mjs";
|
||||
import { ProjectionFeature } from "./layout/projection.mjs";
|
||||
import { PanGesture } from "./gestures/pan/index.mjs";
|
||||
import { DragGesture } from "./gestures/drag/index.mjs";
|
||||
import { LayoutFeature } from "./layout/layout.mjs";
|
||||
//#region src/features/dom-max.ts
|
||||
var domMax = {
|
||||
renderer: createVisualElement,
|
||||
features: [
|
||||
AnimationFeature,
|
||||
ExitFeature,
|
||||
PressGesture,
|
||||
HoverGesture,
|
||||
InViewGesture,
|
||||
FocusGesture,
|
||||
ProjectionFeature,
|
||||
PanGesture,
|
||||
DragGesture,
|
||||
LayoutFeature
|
||||
]
|
||||
};
|
||||
//#endregion
|
||||
export { domMax };
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import { Feature } from "../feature.mjs";
|
||||
import { frame } from "motion-dom";
|
||||
//#region src/features/exit/exit.ts
|
||||
/**
|
||||
* Owns the AnimatePresence exit protocol for a motion state: run the exit
|
||||
* animation, defer completion across the layoutId projection handoff, and
|
||||
* resolve the session's promise exactly once. The `isExiting` flag itself
|
||||
* lives on MotionState; this feature owns the sequence around it.
|
||||
*
|
||||
* The generation counter is bumped by exit() and reenter() — a completion
|
||||
* carrying a stale generation is a silent no-op, which is how mid-exit
|
||||
* reentry and re-entry-then-exit-again sequences stay consistent.
|
||||
*/
|
||||
var ExitFeature = class extends Feature {
|
||||
constructor(..._args) {
|
||||
super(..._args);
|
||||
this.exitGeneration = 0;
|
||||
}
|
||||
static {
|
||||
this.key = "exit";
|
||||
}
|
||||
/**
|
||||
* Run the full exit sequence for AnimatePresence and resolve when this
|
||||
* state's exit completes — including the layoutId projection handoff,
|
||||
* which is signalled back via completeExitFromProjection().
|
||||
*/
|
||||
exit() {
|
||||
const generation = ++this.exitGeneration;
|
||||
const state = this.state;
|
||||
state.isExiting = true;
|
||||
const completion = new Promise((resolve) => {
|
||||
this.pendingExit = { resolve };
|
||||
});
|
||||
const exitAnimation = state.visualElement?.animationState?.setActive("exit", true);
|
||||
if (exitAnimation) exitAnimation.then(() => {
|
||||
if (generation !== this.exitGeneration) return;
|
||||
state.isExiting = false;
|
||||
state.options?.layoutId ? frame.postRender(() => this.tryCompleteExit(generation)) : this.tryCompleteExit(generation);
|
||||
});
|
||||
else {
|
||||
state.isExiting = false;
|
||||
this.tryCompleteExit(generation);
|
||||
}
|
||||
state.getSnapshot(false);
|
||||
return completion;
|
||||
}
|
||||
/** Re-enter while an exit is in flight; the stale exit resolves silently. */
|
||||
reenter() {
|
||||
this.exitGeneration++;
|
||||
this.settleExit();
|
||||
this.state.isExiting = false;
|
||||
this.state.setActive("exit", false);
|
||||
this.state.getSnapshot(true);
|
||||
}
|
||||
/** ProjectionFeature notifies here when a layoutId exit handoff completes. */
|
||||
completeExitFromProjection() {
|
||||
this.tryCompleteExit(this.exitGeneration);
|
||||
}
|
||||
tryCompleteExit(generation) {
|
||||
if (generation !== this.exitGeneration || this.state.isExiting) return;
|
||||
if (this.state.options?.layoutId && this.state.visualElement.projection?.currentAnimation?.state === "running") return;
|
||||
this.settleExit();
|
||||
}
|
||||
settleExit() {
|
||||
this.pendingExit?.resolve();
|
||||
this.pendingExit = void 0;
|
||||
}
|
||||
/**
|
||||
* Register with the nearest AnimatePresence for exit discovery — only
|
||||
* states carrying the exit feature are discoverable, and mount/unmount
|
||||
* pairing keeps the registry exact (KeepAlive remounts re-register).
|
||||
*/
|
||||
mount() {
|
||||
this.state.options.presenceContext?.register?.(this.state);
|
||||
}
|
||||
unmount() {
|
||||
this.state.options.presenceContext?.unregister?.(this.state);
|
||||
this.settleExit();
|
||||
}
|
||||
};
|
||||
//#endregion
|
||||
export { ExitFeature };
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
//#region src/features/feature.ts
|
||||
var Feature = class {
|
||||
constructor(state) {
|
||||
this.isMount = false;
|
||||
this.state = state;
|
||||
}
|
||||
mount() {}
|
||||
beforeUnmount() {}
|
||||
unmount() {}
|
||||
update() {}
|
||||
getSnapshot(isPresent) {}
|
||||
};
|
||||
//#endregion
|
||||
export { Feature };
|
||||
Generated
Vendored
+488
@@ -0,0 +1,488 @@
|
||||
import { getContextWindow } from "../../../utils/get-context-window.mjs";
|
||||
import { addPointerEvent } from "../../../events/add-pointer-event.mjs";
|
||||
import { PanSession } from "../pan/PanSession.mjs";
|
||||
import { applyConstraints, calcOrigin, calcRelativeConstraints, calcViewportConstraints, defaultElastic, rebaseAxisConstraints, resolveDragElastic } from "./utils/constraints.mjs";
|
||||
import { addDomEvent, addValueToWillChange, animateMotionValue, calcLength, convertBoundingBoxToBox, convertBoxToBoundingBox, createBox, eachAxis, frame, isElementTextInput, isHTMLElement, measurePageBox, mixNumber, percent, resize, setDragLock } from "motion-dom";
|
||||
import { invariant } from "hey-listen";
|
||||
//#region src/features/gestures/drag/VisualElementDragControls.ts
|
||||
var elementDragControls = /* @__PURE__ */ new WeakMap();
|
||||
var VisualElementDragControls = class {
|
||||
constructor(state) {
|
||||
this.openGlobalLock = null;
|
||||
this.isDragging = false;
|
||||
this.currentDirection = null;
|
||||
this.originPoint = {
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
this.constraints = false;
|
||||
this.hasMutatedConstraints = false;
|
||||
this.elastic = createBox();
|
||||
this.latestPointerEvent = null;
|
||||
this.latestPanInfo = null;
|
||||
this.state = state;
|
||||
}
|
||||
get visualElement() {
|
||||
return this.state.visualElement;
|
||||
}
|
||||
start(originEvent, { snapToCursor = false, distanceThreshold } = {}) {
|
||||
/**
|
||||
* Don't start dragging if this component is exiting
|
||||
*/
|
||||
if (this.state.isExiting) return;
|
||||
const onSessionStart = (event) => {
|
||||
if (snapToCursor) this.snapToCursor(event);
|
||||
this.stopAnimation();
|
||||
};
|
||||
const onStart = (event, info) => {
|
||||
this.stopAnimation();
|
||||
const { drag, dragPropagation, onDragStart } = this.getProps();
|
||||
if (drag && !dragPropagation) {
|
||||
if (this.openGlobalLock) this.openGlobalLock();
|
||||
this.openGlobalLock = setDragLock(drag);
|
||||
if (!this.openGlobalLock) return;
|
||||
}
|
||||
this.latestPointerEvent = event;
|
||||
this.latestPanInfo = info;
|
||||
this.isDragging = true;
|
||||
this.currentDirection = null;
|
||||
this.resolveConstraints();
|
||||
if (this.visualElement.projection) {
|
||||
this.visualElement.projection.isAnimationBlocked = true;
|
||||
this.visualElement.projection.target = void 0;
|
||||
}
|
||||
/**
|
||||
* Record gesture origin
|
||||
*/
|
||||
eachAxis((axis) => {
|
||||
let current = this.getAxisMotionValue(axis).get() || 0;
|
||||
/**
|
||||
* If the MotionValue is a percentage value convert to px
|
||||
*/
|
||||
if (percent.test(current)) {
|
||||
const { projection } = this.visualElement;
|
||||
if (projection && projection.layout) {
|
||||
const measuredAxis = projection.layout.layoutBox[axis];
|
||||
if (measuredAxis) current = calcLength(measuredAxis) * (parseFloat(current) / 100);
|
||||
}
|
||||
}
|
||||
this.originPoint[axis] = current;
|
||||
});
|
||||
if (onDragStart) frame.update(() => onDragStart(event, info), false, true);
|
||||
addValueToWillChange(this.visualElement, "transform");
|
||||
this.state.setActive("whileDrag", true);
|
||||
};
|
||||
const onMove = (event, info) => {
|
||||
this.latestPointerEvent = event;
|
||||
this.latestPanInfo = info;
|
||||
const { dragPropagation, dragDirectionLock, onDirectionLock, onDrag } = this.getProps();
|
||||
if (!dragPropagation && !this.openGlobalLock) return;
|
||||
const { offset } = info;
|
||||
if (dragDirectionLock && this.currentDirection === null) {
|
||||
this.currentDirection = getCurrentDirection(offset);
|
||||
if (this.currentDirection !== null) onDirectionLock && onDirectionLock(this.currentDirection);
|
||||
return;
|
||||
}
|
||||
this.updateAxis("x", info.point, offset);
|
||||
this.updateAxis("y", info.point, offset);
|
||||
/**
|
||||
* Ideally we would leave the renderer to fire naturally at the end of
|
||||
* this frame but if the element is about to change layout as the result
|
||||
* of a re-render we want to ensure the browser can read the latest
|
||||
* bounding box to ensure the pointer and element don't fall out of sync.
|
||||
*/
|
||||
this.visualElement.render();
|
||||
/**
|
||||
* This must fire after the render call as it might trigger a state
|
||||
* change which itself might trigger a layout update.
|
||||
*/
|
||||
if (onDrag) frame.update(() => onDrag(event, info), false, true);
|
||||
};
|
||||
const onSessionEnd = (event, info) => {
|
||||
this.latestPointerEvent = event;
|
||||
this.latestPanInfo = info;
|
||||
this.stop(event, info);
|
||||
this.latestPointerEvent = null;
|
||||
this.latestPanInfo = null;
|
||||
};
|
||||
const resumeAnimation = () => {
|
||||
const { dragSnapToOrigin: snap } = this.getProps();
|
||||
if (snap || this.constraints) this.startAnimation({
|
||||
x: 0,
|
||||
y: 0
|
||||
});
|
||||
};
|
||||
const { dragSnapToOrigin } = this.getProps();
|
||||
this.panSession = new PanSession(originEvent, {
|
||||
onSessionStart,
|
||||
onStart,
|
||||
onMove,
|
||||
onSessionEnd,
|
||||
resumeAnimation
|
||||
}, {
|
||||
transformPagePoint: this.visualElement.getTransformPagePoint(),
|
||||
dragSnapToOrigin,
|
||||
distanceThreshold,
|
||||
contextWindow: getContextWindow(this.visualElement),
|
||||
element: this.state.element
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
stop(event, panInfo) {
|
||||
const finalEvent = event || this.latestPointerEvent;
|
||||
const finalPanInfo = panInfo || this.latestPanInfo;
|
||||
const isDragging = this.isDragging;
|
||||
this.cancel();
|
||||
if (!isDragging || !finalPanInfo || !finalEvent) return;
|
||||
const { velocity } = finalPanInfo;
|
||||
this.startAnimation(velocity);
|
||||
const { onDragEnd } = this.getProps();
|
||||
if (onDragEnd) frame.postRender(() => onDragEnd(finalEvent, finalPanInfo));
|
||||
}
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
cancel() {
|
||||
this.isDragging = false;
|
||||
const { projection } = this.visualElement;
|
||||
if (projection) projection.isAnimationBlocked = false;
|
||||
this.endPanSession();
|
||||
const { dragPropagation } = this.getProps();
|
||||
if (!dragPropagation && this.openGlobalLock) {
|
||||
this.openGlobalLock();
|
||||
this.openGlobalLock = null;
|
||||
}
|
||||
this.state.setActive("whileDrag", false);
|
||||
}
|
||||
/**
|
||||
* Clean up the pan session without modifying other drag state.
|
||||
* Used during unmount to remove event listeners without affecting
|
||||
* projection animations or drag locks.
|
||||
* @internal
|
||||
*/
|
||||
endPanSession() {
|
||||
this.panSession && this.panSession.end();
|
||||
this.panSession = void 0;
|
||||
}
|
||||
updateAxis(axis, _point, offset) {
|
||||
const { drag } = this.getProps();
|
||||
if (!offset || !shouldDrag(axis, drag, this.currentDirection)) return;
|
||||
const axisValue = this.getAxisMotionValue(axis);
|
||||
let next = this.originPoint[axis] + offset[axis];
|
||||
if (this.constraints && this.constraints[axis]) next = applyConstraints(next, this.constraints[axis], this.elastic[axis]);
|
||||
axisValue.set(next);
|
||||
}
|
||||
resolveConstraints() {
|
||||
const { dragConstraints, dragElastic } = this.getProps();
|
||||
const layout = this.visualElement.projection && !this.visualElement.projection.layout ? this.visualElement.projection.measure(false) : this.visualElement.projection?.layout;
|
||||
const prevConstraints = this.constraints;
|
||||
if (dragConstraints && isHTMLElement(dragConstraints)) {
|
||||
if (!this.constraints) this.constraints = this.resolveRefConstraints();
|
||||
} else if (dragConstraints && layout) this.constraints = calcRelativeConstraints(layout.layoutBox, dragConstraints);
|
||||
else this.constraints = false;
|
||||
this.elastic = resolveDragElastic(dragElastic);
|
||||
/**
|
||||
* If we're outputting to external MotionValues, we want to rebase the measured constraints
|
||||
* from viewport-relative to component-relative. This only applies to relative (non-element)
|
||||
* constraints, as element-based constraints from calcViewportConstraints are already in the
|
||||
* correct coordinate space for the motion value transform offset.
|
||||
*/
|
||||
if (prevConstraints !== this.constraints && !isHTMLElement(dragConstraints) && layout && this.constraints && !this.hasMutatedConstraints) eachAxis((axis) => {
|
||||
if (this.constraints !== false && this.getAxisMotionValue(axis)) this.constraints[axis] = rebaseAxisConstraints(layout.layoutBox[axis], this.constraints[axis]);
|
||||
});
|
||||
}
|
||||
resolveRefConstraints() {
|
||||
const { dragConstraints: constraints, onMeasureDragConstraints } = this.getProps();
|
||||
if (!constraints || !isHTMLElement(constraints)) return false;
|
||||
const constraintsElement = constraints;
|
||||
invariant(constraintsElement !== null, "If `dragConstraints` is set as a React ref, that ref must be passed to another component's `ref` prop.");
|
||||
const { projection } = this.visualElement;
|
||||
if (!projection || !projection.layout) return false;
|
||||
/**
|
||||
* Refresh the root scroll offset so the constraint's viewport box
|
||||
* translates to correct page coordinates. The scroll captured at
|
||||
* drag mount can be stale if the document was scrolled afterwards —
|
||||
* e.g. via the browser restoring scroll on refresh, or an ancestor
|
||||
* layout effect running after this element's mount (#2829).
|
||||
*
|
||||
* Clear the cached scroll first so `updateScroll` bypasses its
|
||||
* per-animationId cache and re-reads the live value.
|
||||
*/
|
||||
if (projection.root) {
|
||||
projection.root.scroll = void 0;
|
||||
projection.root.updateScroll();
|
||||
}
|
||||
const constraintsBox = measurePageBox(constraintsElement, projection.root, this.visualElement.getTransformPagePoint());
|
||||
let measuredConstraints = calcViewportConstraints(projection.layout.layoutBox, constraintsBox);
|
||||
/**
|
||||
* If there's an onMeasureDragConstraints listener we call it and
|
||||
* if different constraints are returned, set constraints to that
|
||||
*/
|
||||
if (onMeasureDragConstraints) {
|
||||
const userConstraints = onMeasureDragConstraints(convertBoxToBoundingBox(measuredConstraints));
|
||||
this.hasMutatedConstraints = !!userConstraints;
|
||||
if (userConstraints) measuredConstraints = convertBoundingBoxToBox(userConstraints);
|
||||
}
|
||||
return measuredConstraints;
|
||||
}
|
||||
startAnimation(velocity) {
|
||||
const { drag, dragMomentum, dragElastic, dragTransition, dragSnapToOrigin, onDragTransitionEnd } = this.getProps();
|
||||
const constraints = this.constraints || {};
|
||||
const momentumAnimations = eachAxis((axis) => {
|
||||
if (!shouldDrag(axis, drag, this.currentDirection)) return;
|
||||
let transition = constraints && constraints[axis] || {};
|
||||
if (dragSnapToOrigin === true || dragSnapToOrigin === axis) transition = {
|
||||
min: 0,
|
||||
max: 0
|
||||
};
|
||||
/**
|
||||
* Overdamp the boundary spring if `dragElastic` is disabled. There's still a frame
|
||||
* of spring animations so we should look into adding a disable spring option to `inertia`.
|
||||
* We could do something here where we affect the `bounceStiffness` and `bounceDamping`
|
||||
* using the value of `dragElastic`.
|
||||
*/
|
||||
const bounceStiffness = dragElastic ? 200 : 1e6;
|
||||
const bounceDamping = dragElastic ? 40 : 1e7;
|
||||
const inertia = {
|
||||
type: "inertia",
|
||||
velocity: dragMomentum ? velocity[axis] : 0,
|
||||
bounceStiffness,
|
||||
bounceDamping,
|
||||
timeConstant: 750,
|
||||
restDelta: 1,
|
||||
restSpeed: 10,
|
||||
...dragTransition,
|
||||
...transition
|
||||
};
|
||||
return this.startAxisValueAnimation(axis, inertia);
|
||||
});
|
||||
return Promise.all(momentumAnimations).then(onDragTransitionEnd);
|
||||
}
|
||||
startAxisValueAnimation(axis, transition) {
|
||||
const axisValue = this.getAxisMotionValue(axis);
|
||||
addValueToWillChange(this.visualElement, axis);
|
||||
return axisValue.start(animateMotionValue(axis, axisValue, 0, transition, this.visualElement, false));
|
||||
}
|
||||
stopAnimation() {
|
||||
if (!this.visualElement.projection?.isPresent) return;
|
||||
eachAxis((axis) => this.getAxisMotionValue(axis).stop());
|
||||
}
|
||||
/**
|
||||
* Drag works differently depending on which props are provided.
|
||||
*
|
||||
* - If _dragX and _dragY are provided, we output the gesture delta directly to those motion values.
|
||||
* - Otherwise, we apply the delta to the x/y motion values.
|
||||
*/
|
||||
getAxisMotionValue(axis) {
|
||||
const dragKey = `_drag${axis.toUpperCase()}`;
|
||||
return this.visualElement.getProps()[dragKey] || this.visualElement.getValue(axis, this.visualElement.latestValues[axis] ?? 0);
|
||||
}
|
||||
/**
|
||||
* Measure the live box - projection.layout may or may not include the
|
||||
* drag transform, depending on when it was measured.
|
||||
*/
|
||||
snapToCursor({ clientX, clientY }) {
|
||||
const { drag } = this.getProps();
|
||||
const point = {
|
||||
x: clientX,
|
||||
y: clientY
|
||||
};
|
||||
const cursor = this.visualElement.getTransformPagePoint()?.(point) || point;
|
||||
const box = this.visualElement.measureViewportBox();
|
||||
eachAxis((axis) => {
|
||||
if (!shouldDrag(axis, drag, this.currentDirection)) return;
|
||||
const axisValue = this.getAxisMotionValue(axis);
|
||||
const { min, max } = box[axis];
|
||||
axisValue.set((axisValue.get() || 0) + cursor[axis] - mixNumber(min, max, .5));
|
||||
});
|
||||
}
|
||||
/**
|
||||
* When the viewport resizes we want to check if the measured constraints
|
||||
* have changed and, if so, reposition the element within those new constraints
|
||||
* relative to where it was before the resize.
|
||||
*/
|
||||
scalePositionWithinConstraints() {
|
||||
if (!this.visualElement.current) return;
|
||||
const { drag, dragConstraints } = this.getProps();
|
||||
const { projection } = this.visualElement;
|
||||
if (!isHTMLElement(dragConstraints) || !projection || !this.constraints) return;
|
||||
/**
|
||||
* Stop current animations as there can be visual glitching if we try to do
|
||||
* this mid-animation
|
||||
*/
|
||||
this.stopAnimation();
|
||||
/**
|
||||
* Record the relative position of the dragged element relative to the
|
||||
* constraints box and save as a progress value.
|
||||
*/
|
||||
const boxProgress = {
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
eachAxis((axis) => {
|
||||
const axisValue = this.getAxisMotionValue(axis);
|
||||
if (axisValue && this.constraints !== false) {
|
||||
const latest = axisValue.get();
|
||||
boxProgress[axis] = calcOrigin({
|
||||
min: latest,
|
||||
max: latest
|
||||
}, this.constraints[axis]);
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Update the layout of this element and resolve the latest drag constraints
|
||||
*/
|
||||
const { transformTemplate } = this.visualElement.getProps();
|
||||
this.state.element.style.transform = transformTemplate ? transformTemplate({}, "") : "none";
|
||||
projection.root && projection.root.updateScroll();
|
||||
projection.updateLayout();
|
||||
/**
|
||||
* Reset constraints so resolveConstraints() will recalculate them
|
||||
* with the freshly measured layout rather than returning the cached value.
|
||||
*/
|
||||
this.constraints = false;
|
||||
this.resolveConstraints();
|
||||
/**
|
||||
* For each axis, calculate the current progress of the layout axis
|
||||
* within the new constraints.
|
||||
*/
|
||||
eachAxis((axis) => {
|
||||
const axisValue = this.getAxisMotionValue(axis);
|
||||
/**
|
||||
* An element resting at its origin has no offset to preserve,
|
||||
* and re-projecting 0 into the new constraints would move it.
|
||||
*/
|
||||
if (!shouldDrag(axis, drag, null) || !axisValue.get()) return;
|
||||
/**
|
||||
* Calculate a new transform based on the previous box progress
|
||||
*/
|
||||
const { min, max } = this.constraints[axis];
|
||||
axisValue.set(mixNumber(min, max, boxProgress[axis]));
|
||||
});
|
||||
/**
|
||||
* Flush the updated transform to the DOM synchronously to prevent
|
||||
* a visual flash at the element's CSS layout position (0,0) when
|
||||
* the transform was stripped for measurement.
|
||||
*/
|
||||
this.visualElement.render();
|
||||
}
|
||||
addListeners() {
|
||||
if (!this.state.element) return;
|
||||
elementDragControls.set(this.visualElement, this);
|
||||
const element = this.state.element;
|
||||
/**
|
||||
* Attach a pointerdown event listener on this DOM element to initiate drag tracking.
|
||||
*/
|
||||
const stopPointerListener = addPointerEvent(element, "pointerdown", (event) => {
|
||||
const { drag, dragListener = true } = this.getProps();
|
||||
const target = event.target;
|
||||
/**
|
||||
* Only block drag if clicking on a text input child element
|
||||
* (input, textarea, select, contenteditable) where users might
|
||||
* want to select text or interact with the control.
|
||||
*
|
||||
* Buttons and links don't block drag since they don't have
|
||||
* click-and-move actions of their own.
|
||||
*/
|
||||
const isClickingTextInputChild = target !== element && isElementTextInput(target);
|
||||
if (drag && dragListener && !isClickingTextInputChild) this.start(event);
|
||||
});
|
||||
/**
|
||||
* If using element-based constraints, observe both the draggable element
|
||||
* and the constraint container for size changes via ResizeObserver.
|
||||
*/
|
||||
let stopResizeObservers;
|
||||
const measureDragConstraints = () => {
|
||||
const { dragConstraints } = this.getProps();
|
||||
if (isHTMLElement(dragConstraints)) {
|
||||
this.constraints = this.resolveRefConstraints();
|
||||
if (!stopResizeObservers) stopResizeObservers = startResizeObservers(element, dragConstraints, () => this.scalePositionWithinConstraints());
|
||||
}
|
||||
};
|
||||
const { projection } = this.visualElement;
|
||||
const stopMeasureLayoutListener = projection.addEventListener("measure", measureDragConstraints);
|
||||
if (projection && !projection.layout) {
|
||||
projection.root && projection.root.updateScroll();
|
||||
projection.updateLayout();
|
||||
}
|
||||
frame.read(measureDragConstraints);
|
||||
/**
|
||||
* Attach a window resize listener to scale the draggable target within its defined
|
||||
* constraints as the window resizes.
|
||||
*/
|
||||
const stopResizeListener = addDomEvent(window, "resize", () => this.scalePositionWithinConstraints());
|
||||
/**
|
||||
* If the element's layout changes, calculate the delta and apply that to
|
||||
* the drag gesture's origin point.
|
||||
*/
|
||||
const stopLayoutUpdateListener = projection.addEventListener("didUpdate", (({ delta, hasLayoutChanged }) => {
|
||||
if (this.isDragging && hasLayoutChanged) {
|
||||
eachAxis((axis) => {
|
||||
const motionValue = this.getAxisMotionValue(axis);
|
||||
if (!motionValue) return;
|
||||
this.originPoint[axis] += delta[axis].translate;
|
||||
motionValue.set(motionValue.get() + delta[axis].translate);
|
||||
});
|
||||
this.visualElement.render();
|
||||
}
|
||||
}));
|
||||
return () => {
|
||||
stopResizeListener();
|
||||
stopPointerListener();
|
||||
stopMeasureLayoutListener();
|
||||
stopLayoutUpdateListener && stopLayoutUpdateListener();
|
||||
stopResizeObservers && stopResizeObservers();
|
||||
};
|
||||
}
|
||||
getProps() {
|
||||
const props = this.visualElement.getProps();
|
||||
const { drag = false, dragDirectionLock = false, dragPropagation = false, dragConstraints = false, dragElastic = defaultElastic, dragMomentum = true } = props;
|
||||
return {
|
||||
...props,
|
||||
drag,
|
||||
dragDirectionLock,
|
||||
dragPropagation,
|
||||
dragConstraints,
|
||||
dragElastic,
|
||||
dragMomentum
|
||||
};
|
||||
}
|
||||
};
|
||||
function skipFirstCall(callback) {
|
||||
let isFirst = true;
|
||||
return () => {
|
||||
if (isFirst) {
|
||||
isFirst = false;
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
};
|
||||
}
|
||||
function startResizeObservers(element, constraintsElement, onResize) {
|
||||
const stopElement = resize(element, skipFirstCall(onResize));
|
||||
const stopContainer = resize(constraintsElement, skipFirstCall(onResize));
|
||||
return () => {
|
||||
stopElement();
|
||||
stopContainer();
|
||||
};
|
||||
}
|
||||
function shouldDrag(direction, drag, currentDirection) {
|
||||
return (drag === true || drag === direction) && (currentDirection === null || currentDirection === direction);
|
||||
}
|
||||
/**
|
||||
* Based on an x/y offset determine the current drag direction. If both axis' offsets are lower
|
||||
* than the provided threshold, return `null`.
|
||||
*
|
||||
* @param offset - The x/y offset from origin.
|
||||
* @param lockThreshold - (Optional) - the minimum absolute offset before we can determine a drag direction.
|
||||
*/
|
||||
function getCurrentDirection(offset, lockThreshold = 10) {
|
||||
let direction = null;
|
||||
if (Math.abs(offset.y) > lockThreshold) direction = "y";
|
||||
else if (Math.abs(offset.x) > lockThreshold) direction = "x";
|
||||
return direction;
|
||||
}
|
||||
//#endregion
|
||||
export { VisualElementDragControls, elementDragControls };
|
||||
Generated
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
import { Feature } from "../../feature.mjs";
|
||||
import { VisualElementDragControls } from "./VisualElementDragControls.mjs";
|
||||
import { noop } from "motion-utils";
|
||||
//#region src/features/gestures/drag/index.ts
|
||||
var DragGesture = class extends Feature {
|
||||
static {
|
||||
this.key = "drag";
|
||||
}
|
||||
constructor(state) {
|
||||
super(state);
|
||||
this.removeGroupControls = noop;
|
||||
this.removeListeners = noop;
|
||||
this.controls = new VisualElementDragControls(state);
|
||||
}
|
||||
mount() {
|
||||
const { dragControls } = this.state.options;
|
||||
if (dragControls) this.removeGroupControls = dragControls.subscribe(this.controls);
|
||||
this.prevDragControls = dragControls;
|
||||
this.removeListeners = this.controls.addListeners() || noop;
|
||||
}
|
||||
update() {
|
||||
const { dragControls } = this.state.options;
|
||||
if (dragControls !== this.prevDragControls) {
|
||||
this.removeGroupControls();
|
||||
this.removeGroupControls = dragControls ? dragControls.subscribe(this.controls) : noop;
|
||||
this.prevDragControls = dragControls;
|
||||
}
|
||||
}
|
||||
unmount() {
|
||||
this.removeGroupControls();
|
||||
this.removeListeners();
|
||||
if (!this.controls.isDragging) this.controls.endPanSession();
|
||||
}
|
||||
};
|
||||
//#endregion
|
||||
export { DragGesture };
|
||||
Generated
Vendored
+113
@@ -0,0 +1,113 @@
|
||||
//#region src/features/gestures/drag/use-drag-controls.ts
|
||||
/**
|
||||
* Can manually trigger a drag gesture on one or more `drag`-enabled `motion` components.
|
||||
*
|
||||
* ```jsx
|
||||
* const dragControls = useDragControls()
|
||||
*
|
||||
* function startDrag(event) {
|
||||
* dragControls.start(event, { snapToCursor: true })
|
||||
* }
|
||||
*
|
||||
* return (
|
||||
* <>
|
||||
* <div onPointerDown={startDrag} />
|
||||
* <motion.div drag="x" dragControls={dragControls} />
|
||||
* </>
|
||||
* )
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
var DragControls = class {
|
||||
constructor() {
|
||||
this.componentControls = /* @__PURE__ */ new Set();
|
||||
}
|
||||
/**
|
||||
* Subscribe a component's internal `VisualElementDragControls` to the user-facing API.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
subscribe(controls) {
|
||||
this.componentControls.add(controls);
|
||||
return () => this.componentControls.delete(controls);
|
||||
}
|
||||
/**
|
||||
* Start a drag gesture on every `motion` component that has this set of drag controls
|
||||
* passed into it via the `dragControls` prop.
|
||||
*
|
||||
* ```jsx
|
||||
* dragControls.start(e, {
|
||||
* snapToCursor: true
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @param event - PointerEvent
|
||||
* @param options - Options
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
start(event, options) {
|
||||
this.componentControls.forEach((controls) => {
|
||||
controls.start(event, options);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Cancels a drag gesture.
|
||||
*
|
||||
* ```jsx
|
||||
* dragControls.cancel()
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
cancel() {
|
||||
this.componentControls.forEach((controls) => {
|
||||
controls.cancel();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Stops a drag gesture.
|
||||
*
|
||||
* ```jsx
|
||||
* dragControls.stop()
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
stop() {
|
||||
this.componentControls.forEach((controls) => {
|
||||
controls.stop();
|
||||
});
|
||||
}
|
||||
};
|
||||
var createDragControls = () => new DragControls();
|
||||
/**
|
||||
* Usually, dragging is initiated by pressing down on a `motion` component with a `drag` prop
|
||||
* and moving it. For some use-cases, for instance clicking at an arbitrary point on a video scrubber, we
|
||||
* might want to initiate that dragging from a different component than the draggable one.
|
||||
*
|
||||
* By creating a `dragControls` using the `useDragControls` hook, we can pass this into
|
||||
* the draggable component's `dragControls` prop. It exposes a `start` method
|
||||
* that can start dragging from pointer events on other components.
|
||||
*
|
||||
* ```jsx
|
||||
* const dragControls = useDragControls()
|
||||
*
|
||||
* function startDrag(event) {
|
||||
* dragControls.start(event, { snapToCursor: true })
|
||||
* }
|
||||
*
|
||||
* return (
|
||||
* <>
|
||||
* <div onPointerDown={startDrag} />
|
||||
* <motion.div drag="x" dragControls={dragControls} />
|
||||
* </>
|
||||
* )
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
var useDragControls = createDragControls;
|
||||
//#endregion
|
||||
export { DragControls, useDragControls };
|
||||
Generated
Vendored
+99
@@ -0,0 +1,99 @@
|
||||
import { calcLength, mixNumber } from "motion-dom";
|
||||
import { clamp, progress } from "motion-utils";
|
||||
//#region src/features/gestures/drag/utils/constraints.ts
|
||||
/**
|
||||
* Apply constraints to a point. These constraints are both physical along an
|
||||
* axis, and an elastic factor that determines how much to constrain the point
|
||||
* by if it does lie outside the defined parameters.
|
||||
*/
|
||||
function applyConstraints(point, { min, max }, elastic) {
|
||||
if (min !== void 0 && point < min) point = elastic ? mixNumber(min, point, elastic.min) : Math.max(point, min);
|
||||
else if (max !== void 0 && point > max) point = elastic ? mixNumber(max, point, elastic.max) : Math.min(point, max);
|
||||
return point;
|
||||
}
|
||||
var defaultElastic = .35;
|
||||
/**
|
||||
* Calculate constraints in terms of the viewport when
|
||||
* defined relatively to the measured bounding box.
|
||||
*/
|
||||
function calcRelativeConstraints(layoutBox, { top, left, bottom, right }) {
|
||||
return {
|
||||
x: calcRelativeAxisConstraints(layoutBox.x, left, right),
|
||||
y: calcRelativeAxisConstraints(layoutBox.y, top, bottom)
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Calculate constraints in terms of the viewport when defined relatively to the
|
||||
* measured axis. This is measured from the nearest edge, so a max constraint of 200
|
||||
* on an axis with a max value of 300 would return a constraint of 500 - axis length
|
||||
*/
|
||||
function calcRelativeAxisConstraints(axis, min, max) {
|
||||
return {
|
||||
min: min !== void 0 ? axis.min + min : void 0,
|
||||
max: max !== void 0 ? axis.max + max - (axis.max - axis.min) : void 0
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Accepts a dragElastic prop and returns resolved elastic values for each axis.
|
||||
*/
|
||||
function resolveDragElastic(dragElastic = defaultElastic) {
|
||||
if (dragElastic === false) dragElastic = 0;
|
||||
else if (dragElastic === true) dragElastic = defaultElastic;
|
||||
return {
|
||||
x: resolveAxisElastic(dragElastic, "left", "right"),
|
||||
y: resolveAxisElastic(dragElastic, "top", "bottom")
|
||||
};
|
||||
}
|
||||
function resolveAxisElastic(dragElastic, minLabel, maxLabel) {
|
||||
return {
|
||||
min: resolvePointElastic(dragElastic, minLabel),
|
||||
max: resolvePointElastic(dragElastic, maxLabel)
|
||||
};
|
||||
}
|
||||
function resolvePointElastic(dragElastic, label) {
|
||||
return typeof dragElastic === "number" ? dragElastic : dragElastic[label] || 0;
|
||||
}
|
||||
/**
|
||||
* Rebase the calculated viewport constraints relative to the layout.min point.
|
||||
*/
|
||||
function rebaseAxisConstraints(layout, constraints) {
|
||||
const relativeConstraints = {};
|
||||
if (constraints.min !== void 0) relativeConstraints.min = constraints.min - layout.min;
|
||||
if (constraints.max !== void 0) relativeConstraints.max = constraints.max - layout.min;
|
||||
return relativeConstraints;
|
||||
}
|
||||
/**
|
||||
* Calculate viewport constraints when defined as another viewport-relative box
|
||||
*/
|
||||
function calcViewportConstraints(layoutBox, constraintsBox) {
|
||||
return {
|
||||
x: calcViewportAxisConstraints(layoutBox.x, constraintsBox.x),
|
||||
y: calcViewportAxisConstraints(layoutBox.y, constraintsBox.y)
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Calculate viewport constraints when defined as another viewport-relative axis
|
||||
*/
|
||||
function calcViewportAxisConstraints(layoutAxis, constraintsAxis) {
|
||||
let min = constraintsAxis.min - layoutAxis.min;
|
||||
let max = constraintsAxis.max - layoutAxis.max;
|
||||
if (constraintsAxis.max - constraintsAxis.min < layoutAxis.max - layoutAxis.min) [min, max] = [max, min];
|
||||
return {
|
||||
min,
|
||||
max
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Calculate a transform origin relative to the source axis, between 0-1, that results
|
||||
* in an asthetically pleasing scale/transform needed to project from source to target.
|
||||
*/
|
||||
function calcOrigin(source, target) {
|
||||
let origin = .5;
|
||||
const sourceLength = calcLength(source);
|
||||
const targetLength = calcLength(target);
|
||||
if (targetLength > sourceLength) origin = progress(target.min, target.max - sourceLength, source.min);
|
||||
else if (sourceLength > targetLength) origin = progress(source.min, source.max - targetLength, target.min);
|
||||
return clamp(0, 1, origin);
|
||||
}
|
||||
//#endregion
|
||||
export { applyConstraints, calcOrigin, calcRelativeAxisConstraints, calcRelativeConstraints, calcViewportAxisConstraints, calcViewportConstraints, defaultElastic, rebaseAxisConstraints, resolveAxisElastic, resolveDragElastic, resolvePointElastic };
|
||||
Generated
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
import { Feature } from "../../feature.mjs";
|
||||
import { addDomEvent } from "motion-dom";
|
||||
import { pipe } from "motion-utils";
|
||||
//#region src/features/gestures/focus/index.ts
|
||||
var FocusGesture = class extends Feature {
|
||||
static {
|
||||
this.key = "focus";
|
||||
}
|
||||
constructor(state) {
|
||||
super(state);
|
||||
this.isFocused = false;
|
||||
}
|
||||
onFocus() {
|
||||
let isFocusVisible = false;
|
||||
/**
|
||||
* If this element doesn't match focus-visible then don't
|
||||
* apply whileFocus. But, if matches throws that focus-visible
|
||||
* is not a valid selector then in that browser outline styles will be applied
|
||||
* to the element by default and we want to match that behaviour with whileFocus.
|
||||
*/
|
||||
try {
|
||||
isFocusVisible = this.state.element.matches(":focus-visible");
|
||||
} catch {
|
||||
isFocusVisible = true;
|
||||
}
|
||||
if (!isFocusVisible) return;
|
||||
this.state.setActive("whileFocus", true);
|
||||
this.isFocused = true;
|
||||
}
|
||||
onBlur() {
|
||||
if (!this.isFocused) return;
|
||||
this.state.setActive("whileFocus", false);
|
||||
this.isFocused = false;
|
||||
}
|
||||
mount() {
|
||||
const element = this.state.element;
|
||||
this.removeFocus = pipe(addDomEvent(element, "focus", () => this.onFocus()), addDomEvent(element, "blur", () => this.onBlur()));
|
||||
}
|
||||
unmount() {
|
||||
this.removeFocus?.();
|
||||
this.removeFocus = void 0;
|
||||
}
|
||||
};
|
||||
//#endregion
|
||||
export { FocusGesture };
|
||||
Generated
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
import { Feature } from "../../feature.mjs";
|
||||
import { extractEventInfo } from "../../../events/event-info.mjs";
|
||||
import { frame, hover } from "motion-dom";
|
||||
//#region src/features/gestures/hover/index.ts
|
||||
var HoverGesture = class extends Feature {
|
||||
static {
|
||||
this.key = "hover";
|
||||
}
|
||||
constructor(state) {
|
||||
super(state);
|
||||
}
|
||||
isActive() {
|
||||
const { whileHover, onHoverStart, onHoverEnd } = this.state.options;
|
||||
return Boolean(whileHover || onHoverStart || onHoverEnd);
|
||||
}
|
||||
register() {
|
||||
const element = this.state.element;
|
||||
if (!element || !this.isActive()) return;
|
||||
this.removeHover?.();
|
||||
this.removeHover = hover(element, (_el, startEvent) => {
|
||||
const props = this.state.options;
|
||||
this.state.setActive("whileHover", true);
|
||||
if (props.onHoverStart) frame.postRender(() => props.onHoverStart(startEvent, extractEventInfo(startEvent)));
|
||||
return (endEvent) => {
|
||||
this.state.setActive("whileHover", false);
|
||||
const callback = this.state.options.onHoverEnd;
|
||||
if (callback) frame.postRender(() => callback(endEvent, extractEventInfo(endEvent)));
|
||||
};
|
||||
});
|
||||
}
|
||||
mount() {
|
||||
this.register();
|
||||
}
|
||||
update() {
|
||||
const prev = this.state.visualElement.prevProps;
|
||||
if (!Boolean(prev?.whileHover || prev?.onHoverStart || prev?.onHoverEnd) && this.isActive()) this.register();
|
||||
}
|
||||
unmount() {
|
||||
this.removeHover?.();
|
||||
this.removeHover = void 0;
|
||||
}
|
||||
};
|
||||
//#endregion
|
||||
export { HoverGesture };
|
||||
Generated
Vendored
+51
@@ -0,0 +1,51 @@
|
||||
import { Feature } from "../../feature.mjs";
|
||||
import { inView } from "framer-motion/dom";
|
||||
import { frame } from "motion-dom";
|
||||
//#region src/features/gestures/in-view/index.ts
|
||||
var InViewGesture = class extends Feature {
|
||||
static {
|
||||
this.key = "inView";
|
||||
}
|
||||
constructor(state) {
|
||||
super(state);
|
||||
}
|
||||
isActive() {
|
||||
const { whileInView, onViewportEnter, onViewportLeave } = this.state.options;
|
||||
return Boolean(whileInView || onViewportEnter || onViewportLeave);
|
||||
}
|
||||
startObserver() {
|
||||
const element = this.state.element;
|
||||
if (!element || !this.isActive()) return;
|
||||
this.removeObserver?.();
|
||||
const { once, ...viewOptions } = this.state.options.inViewOptions || {};
|
||||
this.removeObserver = inView(element, (_, entry) => {
|
||||
const props = this.state.options;
|
||||
this.state.setActive("whileInView", true);
|
||||
if (props.onViewportEnter) frame.postRender(() => props.onViewportEnter(entry));
|
||||
if (!once) return () => {
|
||||
this.state.setActive("whileInView", false);
|
||||
const leaveCallback = this.state.options.onViewportLeave;
|
||||
if (leaveCallback) frame.postRender(() => leaveCallback(entry));
|
||||
};
|
||||
}, viewOptions);
|
||||
}
|
||||
mount() {
|
||||
this.startObserver();
|
||||
}
|
||||
update() {
|
||||
const { props, prevProps } = this.state.visualElement;
|
||||
if ([
|
||||
"amount",
|
||||
"margin",
|
||||
"root"
|
||||
].some((name) => {
|
||||
return props.inViewOptions?.[name] !== prevProps?.inViewOptions?.[name];
|
||||
})) this.startObserver();
|
||||
}
|
||||
unmount() {
|
||||
this.removeObserver?.();
|
||||
this.removeObserver = void 0;
|
||||
}
|
||||
};
|
||||
//#endregion
|
||||
export { InViewGesture };
|
||||
Generated
Vendored
+221
@@ -0,0 +1,221 @@
|
||||
import { extractEventInfo } from "../../../events/event-info.mjs";
|
||||
import { addPointerEvent } from "../../../events/add-pointer-event.mjs";
|
||||
import { distance2D } from "framer-motion/dom";
|
||||
import { cancelFrame, frame, frameData, isPrimaryPointer, time } from "motion-dom";
|
||||
import { millisecondsToSeconds, pipe, secondsToMilliseconds } from "motion-utils";
|
||||
//#region src/features/gestures/pan/PanSession.ts
|
||||
var overflowStyles = /* #__PURE__ */ new Set(["auto", "scroll"]);
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
var PanSession = class {
|
||||
constructor(event, handlers, { transformPagePoint, contextWindow, dragSnapToOrigin = false, distanceThreshold = 3, element } = {}) {
|
||||
this.startEvent = null;
|
||||
this.lastMoveEvent = null;
|
||||
this.lastMoveEventInfo = null;
|
||||
this.lastRawMoveEventInfo = null;
|
||||
this.handlers = {};
|
||||
this.contextWindow = window;
|
||||
this.scrollPositions = /* @__PURE__ */ new Map();
|
||||
this.onElementScroll = (event) => {
|
||||
this.handleScroll(event.target);
|
||||
};
|
||||
this.onWindowScroll = () => {
|
||||
this.handleScroll(window);
|
||||
};
|
||||
this.updatePoint = () => {
|
||||
if (!(this.lastMoveEvent && this.lastMoveEventInfo)) return;
|
||||
this.hasPendingMove = false;
|
||||
if (this.lastRawMoveEventInfo) this.lastMoveEventInfo = transformPoint(this.lastRawMoveEventInfo, this.transformPagePoint);
|
||||
const info = getPanInfo(this.lastMoveEventInfo, this.history);
|
||||
const isPanStarted = this.startEvent !== null;
|
||||
const isDistancePastThreshold = distance2D(info.offset, {
|
||||
x: 0,
|
||||
y: 0
|
||||
}) >= this.distanceThreshold;
|
||||
if (!isPanStarted && !isDistancePastThreshold) return;
|
||||
const { point } = info;
|
||||
this.history.push({
|
||||
...point,
|
||||
timestamp: time.now()
|
||||
});
|
||||
const { onStart, onMove } = this.handlers;
|
||||
if (!isPanStarted) {
|
||||
onStart && onStart(this.lastMoveEvent, info);
|
||||
this.startEvent = this.lastMoveEvent;
|
||||
}
|
||||
onMove && onMove(this.lastMoveEvent, info);
|
||||
};
|
||||
this.handlePointerMove = (event, info) => {
|
||||
this.lastMoveEvent = event;
|
||||
this.lastRawMoveEventInfo = info;
|
||||
this.lastMoveEventInfo = transformPoint(info, this.transformPagePoint);
|
||||
this.hasPendingMove = true;
|
||||
frame.update(this.updatePoint, true);
|
||||
};
|
||||
this.handlePointerUp = (event, info) => {
|
||||
this.hasPendingMove && this.updatePoint();
|
||||
this.end();
|
||||
const { onEnd, onSessionEnd, resumeAnimation } = this.handlers;
|
||||
if (this.dragSnapToOrigin || !this.startEvent) resumeAnimation && resumeAnimation();
|
||||
if (!(this.lastMoveEvent && this.lastMoveEventInfo)) return;
|
||||
const panInfo = getPanInfo(event.type === "pointercancel" ? this.lastMoveEventInfo : transformPoint(info, this.transformPagePoint), this.history);
|
||||
if (this.startEvent && onEnd) onEnd(event, panInfo);
|
||||
onSessionEnd && onSessionEnd(event, panInfo);
|
||||
};
|
||||
if (!isPrimaryPointer(event)) return;
|
||||
this.dragSnapToOrigin = dragSnapToOrigin;
|
||||
this.handlers = handlers;
|
||||
this.transformPagePoint = transformPagePoint;
|
||||
this.distanceThreshold = distanceThreshold;
|
||||
this.contextWindow = contextWindow || window;
|
||||
const initialInfo = transformPoint(extractEventInfo(event), this.transformPagePoint);
|
||||
const { point } = initialInfo;
|
||||
const { timestamp } = frameData;
|
||||
this.history = [{
|
||||
...point,
|
||||
timestamp
|
||||
}];
|
||||
const { onSessionStart } = handlers;
|
||||
onSessionStart && onSessionStart(event, getPanInfo(initialInfo, this.history));
|
||||
const eventOptions = {
|
||||
passive: true,
|
||||
capture: true
|
||||
};
|
||||
this.removeListeners = pipe(addPointerEvent(this.contextWindow, "pointermove", this.handlePointerMove, eventOptions), addPointerEvent(this.contextWindow, "pointerup", this.handlePointerUp, eventOptions), addPointerEvent(this.contextWindow, "pointercancel", this.handlePointerUp, eventOptions));
|
||||
if (element) this.startScrollTracking(element);
|
||||
}
|
||||
/**
|
||||
* Start tracking scroll on ancestors and window.
|
||||
*/
|
||||
startScrollTracking(element) {
|
||||
let current = element.parentElement;
|
||||
while (current) {
|
||||
const style = getComputedStyle(current);
|
||||
if (overflowStyles.has(style.overflowX) || overflowStyles.has(style.overflowY)) this.scrollPositions.set(current, {
|
||||
x: current.scrollLeft,
|
||||
y: current.scrollTop
|
||||
});
|
||||
current = current.parentElement;
|
||||
}
|
||||
this.scrollPositions.set(window, {
|
||||
x: window.scrollX,
|
||||
y: window.scrollY
|
||||
});
|
||||
window.addEventListener("scroll", this.onElementScroll, {
|
||||
capture: true,
|
||||
passive: true
|
||||
});
|
||||
window.addEventListener("scroll", this.onWindowScroll, { passive: true });
|
||||
this.removeScrollListeners = () => {
|
||||
window.removeEventListener("scroll", this.onElementScroll, { capture: true });
|
||||
window.removeEventListener("scroll", this.onWindowScroll);
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Handle scroll compensation during drag.
|
||||
*
|
||||
* For element scroll: adjusts history origin since pageX/pageY doesn't change.
|
||||
* For window scroll: adjusts lastMoveEventInfo since pageX/pageY would change.
|
||||
*/
|
||||
handleScroll(target) {
|
||||
const initial = this.scrollPositions.get(target);
|
||||
if (!initial) return;
|
||||
const isWindow = target === window;
|
||||
const current = isWindow ? {
|
||||
x: window.scrollX,
|
||||
y: window.scrollY
|
||||
} : {
|
||||
x: target.scrollLeft,
|
||||
y: target.scrollTop
|
||||
};
|
||||
const delta = {
|
||||
x: current.x - initial.x,
|
||||
y: current.y - initial.y
|
||||
};
|
||||
if (delta.x === 0 && delta.y === 0) return;
|
||||
if (isWindow) {
|
||||
if (this.lastMoveEventInfo) {
|
||||
this.lastMoveEventInfo.point.x += delta.x;
|
||||
this.lastMoveEventInfo.point.y += delta.y;
|
||||
}
|
||||
} else if (this.history.length > 0) {
|
||||
this.history[0].x -= delta.x;
|
||||
this.history[0].y -= delta.y;
|
||||
}
|
||||
this.scrollPositions.set(target, current);
|
||||
frame.update(this.updatePoint, true);
|
||||
}
|
||||
updateHandlers(handlers) {
|
||||
this.handlers = handlers;
|
||||
}
|
||||
end() {
|
||||
this.removeListeners && this.removeListeners();
|
||||
this.removeScrollListeners?.();
|
||||
this.scrollPositions.clear();
|
||||
cancelFrame(this.updatePoint);
|
||||
}
|
||||
};
|
||||
function transformPoint(info, transformPagePoint) {
|
||||
return transformPagePoint ? { point: transformPagePoint(info.point) } : info;
|
||||
}
|
||||
function subtractPoint(a, b) {
|
||||
return {
|
||||
x: a.x - b.x,
|
||||
y: a.y - b.y
|
||||
};
|
||||
}
|
||||
function getPanInfo({ point }, history) {
|
||||
return {
|
||||
point,
|
||||
delta: subtractPoint(point, lastDevicePoint(history)),
|
||||
offset: subtractPoint(point, startDevicePoint(history)),
|
||||
velocity: getVelocity(history, .1)
|
||||
};
|
||||
}
|
||||
function startDevicePoint(history) {
|
||||
return history[0];
|
||||
}
|
||||
function lastDevicePoint(history) {
|
||||
return history[history.length - 1];
|
||||
}
|
||||
function getVelocity(history, timeDelta) {
|
||||
if (history.length < 2) return {
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
let i = history.length - 1;
|
||||
let timestampedPoint = null;
|
||||
const lastPoint = lastDevicePoint(history);
|
||||
while (i >= 0) {
|
||||
timestampedPoint = history[i];
|
||||
if (lastPoint.timestamp - timestampedPoint.timestamp > secondsToMilliseconds(timeDelta)) break;
|
||||
i--;
|
||||
}
|
||||
if (!timestampedPoint) return {
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
/**
|
||||
* If the selected point is the pointer-down origin (history[0]),
|
||||
* there are better movement points available, and the time gap
|
||||
* is suspiciously large (>2x timeDelta), use the next point instead.
|
||||
* This prevents stale pointer-down points from diluting velocity
|
||||
* in hold-then-flick gestures.
|
||||
*/
|
||||
if (timestampedPoint === history[0] && history.length > 2 && lastPoint.timestamp - timestampedPoint.timestamp > secondsToMilliseconds(timeDelta) * 2) timestampedPoint = history[1];
|
||||
const time = millisecondsToSeconds(lastPoint.timestamp - timestampedPoint.timestamp);
|
||||
if (time === 0) return {
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
const currentVelocity = {
|
||||
x: (lastPoint.x - timestampedPoint.x) / time,
|
||||
y: (lastPoint.y - timestampedPoint.y) / time
|
||||
};
|
||||
if (currentVelocity.x === Infinity) currentVelocity.x = 0;
|
||||
if (currentVelocity.y === Infinity) currentVelocity.y = 0;
|
||||
return currentVelocity;
|
||||
}
|
||||
//#endregion
|
||||
export { PanSession };
|
||||
Generated
Vendored
+57
@@ -0,0 +1,57 @@
|
||||
import { getContextWindow } from "../../../utils/get-context-window.mjs";
|
||||
import { Feature } from "../../feature.mjs";
|
||||
import { addPointerEvent } from "../../../events/add-pointer-event.mjs";
|
||||
import { PanSession } from "./PanSession.mjs";
|
||||
import { frame } from "motion-dom";
|
||||
import { noop } from "motion-utils";
|
||||
//#region src/features/gestures/pan/index.ts
|
||||
function asyncHandler(handler) {
|
||||
return (event, info) => {
|
||||
if (handler) frame.update(() => handler(event, info), false, true);
|
||||
};
|
||||
}
|
||||
var PanGesture = class extends Feature {
|
||||
constructor(..._args) {
|
||||
super(..._args);
|
||||
this.removePointerDownListener = noop;
|
||||
}
|
||||
static {
|
||||
this.key = "pan";
|
||||
}
|
||||
onPointerDown(pointerDownEvent) {
|
||||
this.session = new PanSession(pointerDownEvent, this.createPanHandlers(), {
|
||||
transformPagePoint: this.state.visualElement.getTransformPagePoint(),
|
||||
contextWindow: getContextWindow(this.state.visualElement)
|
||||
});
|
||||
}
|
||||
createPanHandlers() {
|
||||
return {
|
||||
onSessionStart: asyncHandler((_, info) => {
|
||||
const { onPanSessionStart } = this.state.options;
|
||||
onPanSessionStart && onPanSessionStart(_, info);
|
||||
}),
|
||||
onStart: asyncHandler((_, info) => {
|
||||
const { onPanStart } = this.state.options;
|
||||
onPanStart && onPanStart(_, info);
|
||||
}),
|
||||
onMove: asyncHandler((event, info) => {
|
||||
const { onPan } = this.state.options;
|
||||
onPan && onPan(event, info);
|
||||
}),
|
||||
onEnd: (event, info) => {
|
||||
const { onPanEnd } = this.state.options;
|
||||
delete this.session;
|
||||
if (onPanEnd) frame.postRender(() => onPanEnd(event, info));
|
||||
}
|
||||
};
|
||||
}
|
||||
mount() {
|
||||
this.removePointerDownListener = addPointerEvent(this.state.element, "pointerdown", this.onPointerDown.bind(this));
|
||||
}
|
||||
unmount() {
|
||||
this.removePointerDownListener();
|
||||
this.session && this.session.end();
|
||||
}
|
||||
};
|
||||
//#endregion
|
||||
export { PanGesture };
|
||||
Generated
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
import { Feature } from "../../feature.mjs";
|
||||
import { extractEventInfo } from "../../../events/event-info.mjs";
|
||||
import { frame, press } from "motion-dom";
|
||||
//#region src/features/gestures/press/index.ts
|
||||
var PressGesture = class extends Feature {
|
||||
static {
|
||||
this.key = "press";
|
||||
}
|
||||
constructor(state) {
|
||||
super(state);
|
||||
}
|
||||
isActive() {
|
||||
const { whilePress, onPress, onPressCancel, onPressStart } = this.state.options;
|
||||
return Boolean(whilePress || onPress || onPressCancel || onPressStart);
|
||||
}
|
||||
register() {
|
||||
const element = this.state.element;
|
||||
if (!element || !this.isActive()) return;
|
||||
this.removePress?.();
|
||||
this.removePress = press(element, (_el, startEvent) => {
|
||||
const props = this.state.options;
|
||||
this.state.setActive("whilePress", true);
|
||||
if (props.onPressStart) frame.postRender(() => props.onPressStart(startEvent, extractEventInfo(startEvent)));
|
||||
return (endEvent, { success }) => {
|
||||
this.state.setActive("whilePress", false);
|
||||
const callbackName = success ? "onPress" : "onPressCancel";
|
||||
const callback = this.state.options[callbackName];
|
||||
if (callback) frame.postRender(() => callback(endEvent, extractEventInfo(endEvent)));
|
||||
};
|
||||
}, { useGlobalTarget: this.state.options.globalPressTarget });
|
||||
}
|
||||
mount() {
|
||||
this.register();
|
||||
}
|
||||
update() {
|
||||
const prev = this.state.visualElement.prevProps;
|
||||
if (!Boolean(prev?.whilePress || prev?.whileTap || prev?.onPress || prev?.onPressCancel || prev?.onPressStart) && this.isActive()) this.register();
|
||||
}
|
||||
unmount() {
|
||||
this.removePress?.();
|
||||
this.removePress = void 0;
|
||||
}
|
||||
};
|
||||
//#endregion
|
||||
export { PressGesture };
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { correctBorderRadius, correctBoxShadow } from "motion-dom";
|
||||
//#region src/features/layout/config.ts
|
||||
var defaultScaleCorrector = {
|
||||
borderRadius: {
|
||||
...correctBorderRadius,
|
||||
applyTo: [
|
||||
"borderTopLeftRadius",
|
||||
"borderTopRightRadius",
|
||||
"borderBottomLeftRadius",
|
||||
"borderBottomRightRadius"
|
||||
]
|
||||
},
|
||||
borderTopLeftRadius: correctBorderRadius,
|
||||
borderTopRightRadius: correctBorderRadius,
|
||||
borderBottomLeftRadius: correctBorderRadius,
|
||||
borderBottomRightRadius: correctBorderRadius,
|
||||
boxShadow: correctBoxShadow
|
||||
};
|
||||
//#endregion
|
||||
export { defaultScaleCorrector };
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import { Feature } from "../feature.mjs";
|
||||
import { isHidden } from "../../utils/is-hidden.mjs";
|
||||
import { defaultScaleCorrector } from "./config.mjs";
|
||||
import { addScaleCorrector, globalProjectionState } from "motion-dom";
|
||||
//#region src/features/layout/layout.ts
|
||||
var LayoutFeature = class extends Feature {
|
||||
static {
|
||||
this.key = "layout";
|
||||
}
|
||||
constructor(state) {
|
||||
super(state);
|
||||
addScaleCorrector(defaultScaleCorrector);
|
||||
}
|
||||
updatePrevLead(isPresent) {
|
||||
const projection = this.state.visualElement.projection;
|
||||
if (projection.isPresent !== isPresent) {
|
||||
projection.isPresent = isPresent;
|
||||
if (isPresent) !projection.isLead() && projection?.promote();
|
||||
else projection.isLead() && projection?.relegate();
|
||||
}
|
||||
}
|
||||
didUpdate() {
|
||||
if (this.state.options.layout || this.state.options.layoutId || this.state.options.drag) this.state.visualElement.projection?.root?.didUpdate();
|
||||
}
|
||||
mount() {
|
||||
const options = this.state.options;
|
||||
const layoutGroup = this.state.options.layoutGroup;
|
||||
if (options.layout || options.layoutId) {
|
||||
const projection = this.state.visualElement.projection;
|
||||
if (options.layoutId) {
|
||||
const isPresent = !isHidden(this.state.element);
|
||||
projection.isPresent = isPresent;
|
||||
if (isPresent) projection?.promote();
|
||||
else projection?.relegate();
|
||||
}
|
||||
layoutGroup?.group?.add(projection);
|
||||
globalProjectionState.hasEverUpdated = true;
|
||||
}
|
||||
this.didUpdate();
|
||||
}
|
||||
unmount() {
|
||||
const layoutGroup = this.state.options.layoutGroup;
|
||||
const projection = this.state.visualElement.projection;
|
||||
if (projection) {
|
||||
if (layoutGroup?.group && (this.state.options.layout || this.state.options.layoutId)) layoutGroup.group.remove(projection);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Capture a layout snapshot. Options are always refreshed by the caller
|
||||
* before this runs (updateOptions → lifecycle hook), so the only remaining
|
||||
* signal is presence: an exiting element is treated as not present.
|
||||
*/
|
||||
getSnapshot(isPresent) {
|
||||
const projection = this.state.visualElement.projection;
|
||||
const { layout, layoutId, drag, layoutDependency } = this.state.visualElement.props;
|
||||
const prevProps = this.state.visualElement.prevProps;
|
||||
if (!projection || !layout && !layoutId && !drag) return;
|
||||
/**
|
||||
* If drag is enabled, no layoutDependency is set, or presence changed,
|
||||
* we need to update the snapshot
|
||||
*/
|
||||
if (drag || prevProps.layoutDependency !== layoutDependency || layoutDependency === void 0 || isPresent !== projection.isPresent) projection.willUpdate();
|
||||
/**
|
||||
* If presence has changed, promote or relegate the projection accordingly
|
||||
*/
|
||||
if (layoutId) this.updatePrevLead(isPresent);
|
||||
this.didUpdate();
|
||||
}
|
||||
refreshRootScroll() {
|
||||
const root = this.state.visualElement?.projection?.root;
|
||||
if (root && this.state.options.layoutId) {
|
||||
root.scroll = void 0;
|
||||
root.updateScroll?.();
|
||||
}
|
||||
}
|
||||
};
|
||||
//#endregion
|
||||
export { LayoutFeature };
|
||||
Generated
Vendored
+50
@@ -0,0 +1,50 @@
|
||||
import { Feature } from "../feature.mjs";
|
||||
import { getClosestProjectingNode } from "./utils.mjs";
|
||||
import { defaultScaleCorrector } from "./config.mjs";
|
||||
import { isSSR } from "../../utils/is.mjs";
|
||||
import { HTMLProjectionNode, addScaleCorrector, isHTMLElement } from "motion-dom";
|
||||
//#region src/features/layout/projection.ts
|
||||
var ProjectionFeature = class extends Feature {
|
||||
static {
|
||||
this.key = "projection";
|
||||
}
|
||||
constructor(state) {
|
||||
super(state);
|
||||
addScaleCorrector(defaultScaleCorrector);
|
||||
if (!isSSR) this.initProjection();
|
||||
}
|
||||
initProjection() {
|
||||
const options = this.state.options;
|
||||
this.state.visualElement.projection = new HTMLProjectionNode(this.state.visualElement.latestValues, options["data-framer-portal-id"] ? void 0 : getClosestProjectingNode(this.state.visualElement.parent));
|
||||
this.projection = this.state.visualElement.projection;
|
||||
this.projection.isPresent = true;
|
||||
this.setOptions();
|
||||
}
|
||||
setOptions() {
|
||||
const options = this.state.options;
|
||||
const { layoutId, layout, drag = false, dragConstraints = false } = options;
|
||||
this.projection?.setOptions({
|
||||
layout,
|
||||
layoutId,
|
||||
alwaysMeasureLayout: Boolean(layoutId) || Boolean(drag) || dragConstraints && isHTMLElement(dragConstraints),
|
||||
visualElement: this.state.visualElement,
|
||||
animationType: typeof options.layout === "string" ? options.layout : "both",
|
||||
layoutRoot: options.layoutRoot,
|
||||
layoutScroll: options.layoutScroll,
|
||||
crossfade: options.crossfade,
|
||||
onExitComplete: () => {
|
||||
if (!this.projection?.isPresent && this.state.options.layoutId && !this.state.isExiting) queueMicrotask(() => {
|
||||
this.state.getFeature("exit")?.completeExitFromProjection();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
update() {
|
||||
this.setOptions();
|
||||
}
|
||||
mount() {
|
||||
this.projection?.mount(this.state.element);
|
||||
}
|
||||
};
|
||||
//#endregion
|
||||
export { ProjectionFeature };
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
//#region src/features/layout/utils.ts
|
||||
function getClosestProjectingNode(visualElement) {
|
||||
if (!visualElement) return void 0;
|
||||
return visualElement.options.allowProjection !== false ? visualElement.projection : getClosestProjectingNode(visualElement.parent);
|
||||
}
|
||||
//#endregion
|
||||
export { getClosestProjectingNode };
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { createContext } from "./utils/createContext.mjs";
|
||||
import { resolveMotionProps } from "./utils/resolve-motion-props.mjs";
|
||||
import { useInView } from "./utils/use-in-view.mjs";
|
||||
import { useAnimationFrame } from "./utils/use-animation-frame.mjs";
|
||||
import { getContextWindow } from "./utils/get-context-window.mjs";
|
||||
import { useDomRef } from "./utils/use-dom-ref.mjs";
|
||||
import { usePageInView } from "./utils/use-page-in-view.mjs";
|
||||
import { injectLayoutGroup, injectMotion, layoutGroupInjectionKey, motionInjectionKey, provideLayoutGroup, provideMotion } from "./components/context.mjs";
|
||||
import { provideMotionConfig, useMotionConfig } from "./components/motion-config/context.mjs";
|
||||
import MotionConfig_default from "./components/motion-config/MotionConfig.mjs";
|
||||
import { mountedStates } from "./state/motion-state.mjs";
|
||||
import { domMax } from "./features/dom-max.mjs";
|
||||
import { Motion, motion } from "./components/motion/index.mjs";
|
||||
import AnimatePresence_default from "./components/animate-presence/AnimatePresence.mjs";
|
||||
import { usePresence } from "./components/animate-presence/use-presence.mjs";
|
||||
import AnimateView_default from "./components/animate-view/AnimateView.mjs";
|
||||
import { startTransition } from "./components/animate-view/start-transition.mjs";
|
||||
import { useCombineMotionValues } from "./value/use-combine-values.mjs";
|
||||
import { useComputed } from "./value/use-computed.mjs";
|
||||
import { useTransform } from "./value/use-transform.mjs";
|
||||
import { useTime } from "./value/use-time.mjs";
|
||||
import { useMotionTemplate } from "./value/use-motion-template.mjs";
|
||||
import { useMotionValueEvent } from "./value/use-motion-value-event.mjs";
|
||||
import { useFollowValue, useSpring } from "./value/use-spring.mjs";
|
||||
import { ScrollOffset, offsetToViewTimelineRange } from "./value/scroll/offsets.mjs";
|
||||
import { useScroll } from "./value/use-scroll.mjs";
|
||||
import { useVelocity } from "./value/use-velocity.mjs";
|
||||
import { Reorder, ReorderGroup, ReorderItem } from "./components/reorder/index.mjs";
|
||||
import RowValue_default from "./components/RowValue.mjs";
|
||||
import { LazyMotion } from "./components/lazy-motion/index.mjs";
|
||||
import { M, m } from "./components/motion/m.mjs";
|
||||
import { useLayoutGroup } from "./components/use-layout-group.mjs";
|
||||
import LayoutGroup_default from "./components/LayoutGroup.mjs";
|
||||
import { useAnimate } from "./animation/hooks/use-animate.mjs";
|
||||
import { useAnimationControls } from "./animation/hooks/use-animation-controls.mjs";
|
||||
import { useReducedMotion } from "./animation/hooks/use-reduced-motion.mjs";
|
||||
import { useDragControls } from "./features/gestures/drag/use-drag-controls.mjs";
|
||||
import { domAnimation } from "./features/dom-animation.mjs";
|
||||
import { MotionPlugin, createMotionDirective, createPresetDirective, vMotion } from "./directive/index.mjs";
|
||||
import { motionGlobalConfig } from "./config.mjs";
|
||||
import { delay as delayInMs, motionValue as useMotionValue } from "motion-dom";
|
||||
export * from "framer-motion/dom";
|
||||
export { AnimatePresence_default as AnimatePresence, AnimateView_default as AnimateView, LayoutGroup_default as LayoutGroup, LazyMotion, M, Motion, MotionConfig_default as MotionConfig, MotionPlugin, Reorder, ReorderGroup, ReorderItem, RowValue_default as RowValue, ScrollOffset, createContext, createMotionDirective, createPresetDirective, delayInMs, domAnimation, domMax, getContextWindow, injectLayoutGroup, injectMotion, layoutGroupInjectionKey, m, motion, motionGlobalConfig, motionInjectionKey, mountedStates, offsetToViewTimelineRange, provideLayoutGroup, provideMotion, provideMotionConfig, resolveMotionProps, startTransition, useAnimate, useAnimationControls, useAnimationFrame, useCombineMotionValues, useComputed, useDomRef, useDragControls, useFollowValue, useInView, useLayoutGroup, useMotionConfig, useMotionTemplate, useMotionValue, useMotionValueEvent, usePageInView, usePresence, useReducedMotion, useScroll, useSpring, useTime, useTransform, useVelocity, vMotion };
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
import { isSVGElement } from "./utils.mjs";
|
||||
import { HTMLVisualElement, SVGVisualElement } from "motion-dom";
|
||||
//#region src/state/create-visual-element.ts
|
||||
function createVisualElement(Component, options) {
|
||||
return isSVGElement(Component) ? new SVGVisualElement(options) : new HTMLVisualElement(options);
|
||||
}
|
||||
//#endregion
|
||||
export { createVisualElement };
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
import { isSVGElement, resolveInitialValues } from "./utils.mjs";
|
||||
import { isVariantLabel } from "motion-dom";
|
||||
import { invariant } from "hey-listen";
|
||||
//#region src/state/motion-state.ts
|
||||
var mountedStates = /* @__PURE__ */ new WeakMap();
|
||||
/**
|
||||
* Core class that manages animation state and orchestrates animations.
|
||||
* Handles component lifecycle methods in the correct order based on component tree position.
|
||||
*/
|
||||
var MotionState = class {
|
||||
constructor(options, parent, bundle = {}) {
|
||||
this.element = null;
|
||||
this.isExiting = false;
|
||||
this.children = /* @__PURE__ */ new Set();
|
||||
this.features = /* @__PURE__ */ new Map();
|
||||
this.featureCtors = [];
|
||||
this._context = null;
|
||||
this.options = options;
|
||||
this.parent = parent;
|
||||
parent?.children?.add(this);
|
||||
this.latestValues = resolveInitialValues(options, this.context);
|
||||
this.type = isSVGElement(this.options.as) ? "svg" : "html";
|
||||
this.setBundle(bundle);
|
||||
}
|
||||
get context() {
|
||||
if (!this._context) {
|
||||
const handler = { get: (_target, prop) => {
|
||||
const value = this.options[prop];
|
||||
if (isVariantLabel(value) || prop === "initial" && value === false) return value;
|
||||
return this.parent?.context[prop];
|
||||
} };
|
||||
this._context = new Proxy({}, handler);
|
||||
}
|
||||
return this._context;
|
||||
}
|
||||
/**
|
||||
* Declare (or asynchronously resolve, e.g. LazyMotion) the render + feature
|
||||
* bundle for this state, then sync — the visual element and feature
|
||||
* instances are created lazily inside updateFeatures().
|
||||
*/
|
||||
setBundle(bundle) {
|
||||
if (bundle.renderer) this.initVisualElement(bundle.renderer);
|
||||
if (bundle.features?.length) this.featureCtors = bundle.features;
|
||||
this.updateFeatures();
|
||||
}
|
||||
/**
|
||||
* Instantiate any missing features, then mount unmounted ones and update
|
||||
* the rest. Feature constructors dereference the visual element, so this
|
||||
* no-ops until setBundle() has created one. Feature mount is isMount-guarded,
|
||||
* so this is safe to call repeatedly and from any lifecycle stage.
|
||||
*/
|
||||
updateFeatures() {
|
||||
if (!this.visualElement) return;
|
||||
for (const FeatureCtor of this.featureCtors) {
|
||||
let feature = this.features.get(FeatureCtor.key);
|
||||
if (!feature) {
|
||||
feature = new FeatureCtor(this);
|
||||
this.features.set(FeatureCtor.key, feature);
|
||||
}
|
||||
if (!this.isMounted()) continue;
|
||||
if (feature.isMount) feature.update();
|
||||
else {
|
||||
feature.mount();
|
||||
feature.isMount = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
updateOptions(options) {
|
||||
this.options = options;
|
||||
this.visualElement?.update({
|
||||
...this.options,
|
||||
whileTap: this.options.whilePress
|
||||
}, this.options.presenceContext ?? null);
|
||||
}
|
||||
mount(element) {
|
||||
invariant(Boolean(element), "Animation state must be mounted with valid Element");
|
||||
mountedStates.set(element, this);
|
||||
this.element = element;
|
||||
this.features.get("layout")?.refreshRootScroll();
|
||||
this.visualElement?.mount(element);
|
||||
this.updateFeatures();
|
||||
}
|
||||
beforeUnmount() {
|
||||
this.getSnapshot(false);
|
||||
}
|
||||
unmount() {
|
||||
this.parent?.children?.delete(this);
|
||||
if (this.element) mountedStates.delete(this.element);
|
||||
this.features.forEach((f) => {
|
||||
f.unmount();
|
||||
f.isMount = false;
|
||||
});
|
||||
this.visualElement?.unmount();
|
||||
this.element = null;
|
||||
}
|
||||
beforeUpdate() {
|
||||
this.getSnapshot(!this.isExiting);
|
||||
}
|
||||
update() {
|
||||
this.updateFeatures();
|
||||
this.didUpdate();
|
||||
}
|
||||
/** Access an installed feature by key, e.g. getFeature<ExitFeature>('exit') */
|
||||
getFeature(key) {
|
||||
return this.features.get(key);
|
||||
}
|
||||
setActive(name, isActive) {
|
||||
if (name === "exit" && isActive) this.isExiting = true;
|
||||
const type = name === "whilePress" ? "whileTap" : name;
|
||||
this.visualElement?.animationState?.setActive(type, isActive).then(() => {
|
||||
if (name === "exit" && isActive) this.isExiting = false;
|
||||
});
|
||||
}
|
||||
isMounted() {
|
||||
return Boolean(this.element);
|
||||
}
|
||||
/**
|
||||
* Create and attach the visual element using the given renderer.
|
||||
* Called by setBundle(); no-ops once created.
|
||||
*/
|
||||
initVisualElement(renderer) {
|
||||
if (this.visualElement) return;
|
||||
this.visualElement = renderer(this.options.as, {
|
||||
presenceContext: this.options.presenceContext ?? null,
|
||||
parent: this.parent?.visualElement,
|
||||
props: {
|
||||
...this.options,
|
||||
whileTap: this.options.whilePress
|
||||
},
|
||||
visualState: {
|
||||
renderState: {
|
||||
transform: {},
|
||||
transformOrigin: {},
|
||||
style: {},
|
||||
vars: {},
|
||||
attrs: {}
|
||||
},
|
||||
latestValues: { ...this.latestValues }
|
||||
},
|
||||
reducedMotionConfig: this.options.motionConfig?.reducedMotion
|
||||
});
|
||||
if (this.isMounted()) this.visualElement.mount(this.element);
|
||||
}
|
||||
getSnapshot(isPresent) {
|
||||
this.features.get("layout")?.getSnapshot(isPresent);
|
||||
}
|
||||
didUpdate() {}
|
||||
};
|
||||
//#endregion
|
||||
export { MotionState, mountedStates };
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { buildHTMLStyles, buildSVGAttrs, camelCaseAttributes, isSVGTag } from "motion-dom";
|
||||
//#region src/state/style.ts
|
||||
function camelToDash(str) {
|
||||
return str.replace(/([A-Z])/g, (match) => `-${match.toLowerCase()}`);
|
||||
}
|
||||
function createHTMLRenderState() {
|
||||
return {
|
||||
transform: {},
|
||||
transformOrigin: {},
|
||||
style: {},
|
||||
vars: {}
|
||||
};
|
||||
}
|
||||
function createSVGRenderState() {
|
||||
return {
|
||||
...createHTMLRenderState(),
|
||||
attrs: {}
|
||||
};
|
||||
}
|
||||
function createStyles(latestValues, transformTemplate) {
|
||||
const state = createHTMLRenderState();
|
||||
buildHTMLStyles(state, latestValues, transformTemplate);
|
||||
const result = { ...state.style };
|
||||
if (result.transform === "") delete result.transform;
|
||||
for (const key in state.vars) result[key] = state.vars[key];
|
||||
if (Object.keys(result).length === 0) return null;
|
||||
return result;
|
||||
}
|
||||
function createSVGStyles(latestValues, tag, styleProp) {
|
||||
const state = createSVGRenderState();
|
||||
buildSVGAttrs(state, latestValues, isSVGTag(tag), void 0, styleProp);
|
||||
const attrs = {};
|
||||
for (const key in state.attrs) {
|
||||
const attrKey = camelCaseAttributes.has(key) ? key : camelToDash(key);
|
||||
attrs[attrKey] = state.attrs[key];
|
||||
}
|
||||
return {
|
||||
attrs,
|
||||
style: {
|
||||
...state.style,
|
||||
...state.vars
|
||||
}
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
export { createSVGStyles, createStyles };
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
//#region src/state/utils.ts
|
||||
function resolveVariantValue(definition, variants, custom) {
|
||||
if (Array.isArray(definition)) return definition.reduce((acc, item) => {
|
||||
const resolvedVariant = resolveVariantValue(item, variants, custom);
|
||||
return resolvedVariant ? {
|
||||
...acc,
|
||||
...resolvedVariant
|
||||
} : acc;
|
||||
}, {});
|
||||
else if (typeof definition === "object") return definition;
|
||||
else if (definition && variants) {
|
||||
const variant = variants[definition];
|
||||
return typeof variant === "function" ? variant(custom) : variant;
|
||||
}
|
||||
}
|
||||
function resolveVariant(definition, variants, custom) {
|
||||
const resolved = resolveVariantValue(definition, variants, custom);
|
||||
if (!resolved) return void 0;
|
||||
const { transition, transitionEnd, ...target } = resolved;
|
||||
return {
|
||||
...target,
|
||||
...transitionEnd
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Resolve initial latest values from variant sources.
|
||||
* Shared by MotionState constructor and SSR style resolution.
|
||||
*
|
||||
* @param options - Motion options
|
||||
* @param context - Optional parent context for variant inheritance (MotionState passes this)
|
||||
*/
|
||||
function resolveInitialValues(options, context) {
|
||||
const sources = (options.initial === void 0 && options.variants ? context?.initial : options.initial) === false ? ["initial", "animate"] : ["initial"];
|
||||
const custom = options.custom ?? options.presenceContext?.custom;
|
||||
return sources.reduce((acc, variant) => {
|
||||
return {
|
||||
...acc,
|
||||
...resolveVariant(options[variant] || context?.[variant], options.variants, custom)
|
||||
};
|
||||
}, {});
|
||||
}
|
||||
var svgElements = [
|
||||
"animate",
|
||||
"circle",
|
||||
"defs",
|
||||
"desc",
|
||||
"ellipse",
|
||||
"g",
|
||||
"image",
|
||||
"line",
|
||||
"filter",
|
||||
"marker",
|
||||
"mask",
|
||||
"metadata",
|
||||
"path",
|
||||
"pattern",
|
||||
"polygon",
|
||||
"polyline",
|
||||
"rect",
|
||||
"stop",
|
||||
"svg",
|
||||
"switch",
|
||||
"symbol",
|
||||
"text",
|
||||
"tspan",
|
||||
"use",
|
||||
"view",
|
||||
"clipPath",
|
||||
"feBlend",
|
||||
"feColorMatrix",
|
||||
"feComponentTransfer",
|
||||
"feComposite",
|
||||
"feConvolveMatrix",
|
||||
"feDiffuseLighting",
|
||||
"feDisplacementMap",
|
||||
"feDistantLight",
|
||||
"feDropShadow",
|
||||
"feFlood",
|
||||
"feFuncA",
|
||||
"feFuncB",
|
||||
"feFuncG",
|
||||
"feFuncR",
|
||||
"feGaussianBlur",
|
||||
"feImage",
|
||||
"feMerge",
|
||||
"feMergeNode",
|
||||
"feMorphology",
|
||||
"feOffset",
|
||||
"fePointLight",
|
||||
"feSpecularLighting",
|
||||
"feSpotLight",
|
||||
"feTile",
|
||||
"feTurbulence",
|
||||
"foreignObject",
|
||||
"linearGradient",
|
||||
"radialGradient",
|
||||
"textPath"
|
||||
];
|
||||
var svgElementSet = new Set(svgElements);
|
||||
function isSVGElement(as) {
|
||||
return svgElementSet.has(as);
|
||||
}
|
||||
//#endregion
|
||||
export { isSVGElement, resolveInitialValues, resolveVariant, svgElements };
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { inject, provide } from "vue";
|
||||
//#region src/utils/createContext.ts
|
||||
/**
|
||||
* @param providerComponentName - The name(s) of the component(s) providing the context.
|
||||
*
|
||||
* There are situations where context can come from multiple components. In such cases, you might need to give an array of component names to provide your context, instead of just a single string.
|
||||
*
|
||||
* @param contextName The description for injection key symbol.
|
||||
*/
|
||||
function createContext(providerComponentName, contextName) {
|
||||
const symbolDescription = typeof providerComponentName === "string" && !contextName ? `${providerComponentName}Context` : contextName;
|
||||
const injectionKey = Symbol(symbolDescription);
|
||||
/**
|
||||
* @param fallback The context value to return if the injection fails.
|
||||
*
|
||||
* @throws When context injection failed and no fallback is specified.
|
||||
* This happens when the component injecting the context is not a child of the root component providing the context.
|
||||
*/
|
||||
const injectContext = (fallback) => {
|
||||
const context = inject(injectionKey, fallback);
|
||||
if (context === void 0) throw new Error(`Injection \`${injectionKey.toString()}\` not found. Component must be used within ${Array.isArray(providerComponentName) ? `one of the following components: ${providerComponentName.join(", ")}` : `\`${providerComponentName}\``}`);
|
||||
return context;
|
||||
};
|
||||
const provideContext = (contextValue) => {
|
||||
provide(injectionKey, contextValue);
|
||||
return contextValue;
|
||||
};
|
||||
return [
|
||||
injectContext,
|
||||
provideContext,
|
||||
injectionKey
|
||||
];
|
||||
}
|
||||
//#endregion
|
||||
export { createContext };
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
//#region src/utils/get-context-window.ts
|
||||
function getContextWindow({ current }) {
|
||||
return current ? current.ownerDocument.defaultView : null;
|
||||
}
|
||||
//#endregion
|
||||
export { getContextWindow };
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
//#region src/utils/is-hidden.ts
|
||||
function isHidden(element) {
|
||||
return element.style.display === "none" || element.offsetParent === null && window.getComputedStyle(element).position !== "fixed";
|
||||
}
|
||||
//#endregion
|
||||
export { isHidden };
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
//#region src/utils/is.ts
|
||||
var isSSR = typeof window === "undefined";
|
||||
//#endregion
|
||||
export { isSSR };
|
||||
Generated
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
//#region src/utils/resolve-motion-props.ts
|
||||
/**
|
||||
* Merge motion props with context values (layout group, presence, config).
|
||||
* Shared by useMotionState (component) and v-motion directive.
|
||||
*/
|
||||
function resolveMotionProps(props, context) {
|
||||
const { layoutGroup, presenceContext, config } = context;
|
||||
const layoutId = layoutGroup.id && props.layoutId ? `${layoutGroup.id}-${props.layoutId}` : props.layoutId || void 0;
|
||||
return {
|
||||
...props,
|
||||
layoutId,
|
||||
transition: props.transition ?? config.transition,
|
||||
layoutGroup,
|
||||
motionConfig: config,
|
||||
inViewOptions: props.inViewOptions ?? config.inViewOptions,
|
||||
presenceContext,
|
||||
initial: presenceContext.initial === false ? presenceContext.initial : props.initial === true ? void 0 : props.initial
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
export { resolveMotionProps };
|
||||
Generated
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
import { cancelFrame, frame } from "motion-dom";
|
||||
import { onBeforeUpdate, onUnmounted } from "vue";
|
||||
//#region src/utils/use-animation-frame.ts
|
||||
function useAnimationFrame(callback) {
|
||||
let initialTimestamp = 0;
|
||||
const provideTimeSinceStart = ({ timestamp, delta }) => {
|
||||
if (!initialTimestamp) initialTimestamp = timestamp;
|
||||
callback(timestamp - initialTimestamp, delta);
|
||||
};
|
||||
const cancel = () => cancelFrame(provideTimeSinceStart);
|
||||
onBeforeUpdate(() => {
|
||||
cancel();
|
||||
frame.update(provideTimeSinceStart, true);
|
||||
});
|
||||
onUnmounted(() => cancel());
|
||||
frame.update(provideTimeSinceStart, true);
|
||||
}
|
||||
//#endregion
|
||||
export { useAnimationFrame };
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { getMotionElement } from "../components/hooks/use-motion-elm.mjs";
|
||||
import { ref } from "vue";
|
||||
//#region src/utils/use-dom-ref.ts
|
||||
function useDomRef() {
|
||||
const dom = ref(null);
|
||||
return new Proxy(dom, {
|
||||
get(target, key) {
|
||||
if (typeof key === "string" || typeof key === "symbol") return Reflect.get(target, key);
|
||||
},
|
||||
set(target, key, value) {
|
||||
if (key === "value") return Reflect.set(target, key, getMotionElement(value?.$el || value));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
//#endregion
|
||||
export { useDomRef };
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { inView } from "framer-motion/dom";
|
||||
import { ref, unref, watchEffect } from "vue";
|
||||
import { unrefElement } from "@vueuse/core";
|
||||
//#region src/utils/use-in-view.ts
|
||||
function useInView(domRef, options) {
|
||||
const isInView = ref(false);
|
||||
watchEffect((onCleanup) => {
|
||||
const realOptions = unref(options) || {};
|
||||
const { once } = realOptions;
|
||||
const el = unrefElement(domRef);
|
||||
if (!el || once && isInView.value) return;
|
||||
const onEnter = () => {
|
||||
isInView.value = true;
|
||||
return once ? void 0 : () => {
|
||||
isInView.value = false;
|
||||
};
|
||||
};
|
||||
const cleanup = inView(el, onEnter, {
|
||||
...realOptions,
|
||||
root: unref(realOptions.root)
|
||||
});
|
||||
onCleanup(() => {
|
||||
cleanup();
|
||||
});
|
||||
}, { flush: "post" });
|
||||
return isInView;
|
||||
}
|
||||
//#endregion
|
||||
export { useInView };
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
//#region src/utils/use-page-in-view.ts
|
||||
function usePageInView() {
|
||||
const isInView = ref(true);
|
||||
const handleVisibilityChange = () => {
|
||||
isInView.value = !document.hidden;
|
||||
};
|
||||
onMounted(() => {
|
||||
if (document.hidden) handleVisibilityChange();
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
});
|
||||
return isInView;
|
||||
}
|
||||
//#endregion
|
||||
export { usePageInView };
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
//#region src/value/scroll/offsets.ts
|
||||
/**
|
||||
* Preset scroll offsets matching framer-motion's ScrollOffset presets.
|
||||
* Use with useScroll's offset option to define scroll-linked animation ranges.
|
||||
*
|
||||
* @example
|
||||
* useScroll({ target: el, offset: ScrollOffset.Enter })
|
||||
*/
|
||||
var ScrollOffset = {
|
||||
/** Progress 0→1 as target enters the container */
|
||||
Enter: [[0, 1], [1, 1]],
|
||||
/** Progress 0→1 as target exits the container */
|
||||
Exit: [[0, 0], [1, 0]],
|
||||
/** Progress 0→1 across any overlap between target and container */
|
||||
Any: [[1, 0], [0, 1]],
|
||||
/** Progress 0→1 while target is fully contained within the container */
|
||||
All: [[0, 0], [1, 1]]
|
||||
};
|
||||
var presets = [
|
||||
[ScrollOffset.Enter, "entry"],
|
||||
[ScrollOffset.Exit, "exit"],
|
||||
[ScrollOffset.Any, "cover"],
|
||||
[ScrollOffset.All, "contain"]
|
||||
];
|
||||
var stringToProgress = {
|
||||
start: 0,
|
||||
end: 1
|
||||
};
|
||||
function parseStringOffset(s) {
|
||||
const parts = s.trim().split(/\s+/);
|
||||
if (parts.length !== 2) return void 0;
|
||||
const a = stringToProgress[parts[0]];
|
||||
const b = stringToProgress[parts[1]];
|
||||
if (a === void 0 || b === void 0) return void 0;
|
||||
return [a, b];
|
||||
}
|
||||
function normaliseOffset(offset) {
|
||||
if (offset.length !== 2) return void 0;
|
||||
const result = [];
|
||||
for (const item of offset) if (Array.isArray(item)) result.push(item);
|
||||
else if (typeof item === "string") {
|
||||
const parsed = parseStringOffset(item);
|
||||
if (!parsed) return void 0;
|
||||
result.push(parsed);
|
||||
} else return;
|
||||
return result;
|
||||
}
|
||||
function matchesPreset(offset, preset) {
|
||||
const normalised = normaliseOffset(offset);
|
||||
if (!normalised) return false;
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const o = normalised[i];
|
||||
const p = preset[i];
|
||||
if (o[0] !== p[0] || o[1] !== p[1]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Maps a ScrollOffset array to a ViewTimeline named range.
|
||||
* Returns undefined for unrecognised patterns — signals fallback to JS scroll tracking.
|
||||
*
|
||||
* Ported from framer-motion's internal render/dom/scroll/utils/offset-to-range.mjs
|
||||
*/
|
||||
function offsetToViewTimelineRange(offset) {
|
||||
if (!offset) return {
|
||||
rangeStart: "contain 0%",
|
||||
rangeEnd: "contain 100%"
|
||||
};
|
||||
for (const [preset, name] of presets) if (matchesPreset(offset, preset)) return {
|
||||
rangeStart: `${name} 0%`,
|
||||
rangeEnd: `${name} 100%`
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
export { ScrollOffset, offsetToViewTimelineRange };
|
||||
Generated
Vendored
+40
@@ -0,0 +1,40 @@
|
||||
import { cancelFrame, frame, motionValue } from "motion-dom";
|
||||
import { getCurrentInstance, onUnmounted } from "vue";
|
||||
//#region src/value/use-combine-values.ts
|
||||
function useCombineMotionValues(combineValues) {
|
||||
/**
|
||||
* Initialise the returned motion value. This remains the same between renders.
|
||||
*/
|
||||
const value = motionValue(combineValues());
|
||||
/**
|
||||
* Create a function that will update the template motion value with the latest values.
|
||||
* This is pre-bound so whenever a motion value updates it can schedule its
|
||||
* execution in Framesync. If it's already been scheduled it won't be fired twice
|
||||
* in a single frame.
|
||||
*/
|
||||
const updateValue = () => value.set(combineValues());
|
||||
/**
|
||||
* Subscribe to all motion values found within the template. Whenever any of them change,
|
||||
* schedule an update.
|
||||
*/
|
||||
const scheduleUpdate = () => frame.preRender(updateValue, false, true);
|
||||
let subscriptions;
|
||||
const subscribe = (values) => {
|
||||
subscriptions = values.map((v) => v.on("change", scheduleUpdate));
|
||||
};
|
||||
const unsubscribe = () => {
|
||||
subscriptions.forEach((unsubscribe) => unsubscribe());
|
||||
cancelFrame(updateValue);
|
||||
};
|
||||
if (getCurrentInstance()) onUnmounted(() => {
|
||||
unsubscribe();
|
||||
});
|
||||
return {
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
value,
|
||||
updateValue
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
export { useCombineMotionValues };
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { useCombineMotionValues } from "./use-combine-values.mjs";
|
||||
import { collectMotionValues } from "motion-dom";
|
||||
import { watchEffect } from "vue";
|
||||
//#region src/value/use-computed.ts
|
||||
function useComputed(computed) {
|
||||
/**
|
||||
* Open session of collectMotionValues. Any MotionValue that calls get()
|
||||
* will be saved into this array.
|
||||
*/
|
||||
collectMotionValues.current = [];
|
||||
const { value, subscribe, unsubscribe, updateValue } = useCombineMotionValues(computed);
|
||||
subscribe(collectMotionValues.current);
|
||||
collectMotionValues.current = void 0;
|
||||
watchEffect(() => {
|
||||
unsubscribe();
|
||||
collectMotionValues.current = [];
|
||||
updateValue();
|
||||
subscribe(collectMotionValues.current);
|
||||
collectMotionValues.current = void 0;
|
||||
});
|
||||
return value;
|
||||
}
|
||||
//#endregion
|
||||
export { useComputed };
|
||||
Generated
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
import { useCombineMotionValues } from "./use-combine-values.mjs";
|
||||
import { isMotionValue } from "motion-dom";
|
||||
//#region src/value/use-motion-template.ts
|
||||
/**
|
||||
* 将多个motion值组合成一个新的motion值,使用模板字符串语法
|
||||
*
|
||||
* ```vue
|
||||
* <script setup>
|
||||
* import { useSpring, motionValue, useMotionTemplate } from 'motion-v'
|
||||
*
|
||||
* const shadowX = useSpring(0)
|
||||
* const shadowY = motionValue(0)
|
||||
* const shadow = useMotionTemplate`drop-shadow(${shadowX}px ${shadowY}px 20px rgba(0,0,0,0.3))`
|
||||
* <\/script>
|
||||
*
|
||||
* <template>
|
||||
* <Motion :style="{ filter: shadow }" />
|
||||
* </template>
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
function useMotionTemplate(fragments, ...values) {
|
||||
/**
|
||||
* 创建一个函数用于从最新的motion值构建字符串
|
||||
*/
|
||||
const numFragments = fragments.length;
|
||||
function buildValue() {
|
||||
let output = "";
|
||||
for (let i = 0; i < numFragments; i++) {
|
||||
output += fragments[i];
|
||||
const value = values[i];
|
||||
if (value) output += isMotionValue(value) ? value.get() : value;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
const { value, subscribe } = useCombineMotionValues(buildValue);
|
||||
subscribe(values.filter(isMotionValue));
|
||||
return value;
|
||||
}
|
||||
//#endregion
|
||||
export { useMotionTemplate };
|
||||
Generated
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
import { onUnmounted } from "vue";
|
||||
//#region src/value/use-motion-value-event.ts
|
||||
/**
|
||||
* 监听 MotionValue 的事件
|
||||
* @param value MotionValue 实例
|
||||
* @param event 事件名称
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
function useMotionValueEvent(value, event, callback) {
|
||||
const unlisten = value.on(event, callback);
|
||||
onUnmounted(() => {
|
||||
unlisten();
|
||||
});
|
||||
return unlisten;
|
||||
}
|
||||
//#endregion
|
||||
export { useMotionValueEvent };
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import { getElement } from "../components/hooks/use-motion-elm.mjs";
|
||||
import { isSSR } from "../utils/is.mjs";
|
||||
import { offsetToViewTimelineRange } from "./scroll/offsets.mjs";
|
||||
import { scroll } from "framer-motion/dom";
|
||||
import { motionValue, supportsScrollTimeline, supportsViewTimeline } from "motion-dom";
|
||||
import { toValue, watchEffect } from "vue";
|
||||
//#region src/value/use-scroll.ts
|
||||
function createScrollMotionValues() {
|
||||
return {
|
||||
scrollX: motionValue(0),
|
||||
scrollY: motionValue(0),
|
||||
scrollXProgress: motionValue(0),
|
||||
scrollYProgress: motionValue(0)
|
||||
};
|
||||
}
|
||||
function canAccelerateScroll(target, offset) {
|
||||
if (isSSR) return false;
|
||||
return target ? supportsViewTimeline() && !!offsetToViewTimelineRange(offset) : supportsScrollTimeline();
|
||||
}
|
||||
function makeAccelerateConfig(axis, options) {
|
||||
return {
|
||||
factory: (animation) => {
|
||||
const { container, target, ...rest } = toValue(options);
|
||||
return scroll(animation, {
|
||||
...rest,
|
||||
axis,
|
||||
container: getElement(container),
|
||||
target: getElement(target)
|
||||
});
|
||||
},
|
||||
times: [0, 1],
|
||||
keyframes: [0, 1],
|
||||
ease: (v) => v,
|
||||
duration: 1
|
||||
};
|
||||
}
|
||||
function useScroll(options = {}) {
|
||||
const values = createScrollMotionValues();
|
||||
const { target, offset } = toValue(options);
|
||||
if (canAccelerateScroll(target, offset)) {
|
||||
values.scrollXProgress.accelerate = makeAccelerateConfig("x", options);
|
||||
values.scrollYProgress.accelerate = makeAccelerateConfig("y", options);
|
||||
}
|
||||
watchEffect((onCleanup) => {
|
||||
if (isSSR) return;
|
||||
const { container, target, ...rest } = toValue(options);
|
||||
const cleanup = scroll((_progress, { x, y }) => {
|
||||
values.scrollX.set(x.current);
|
||||
values.scrollXProgress.set(x.progress);
|
||||
values.scrollY.set(y.current);
|
||||
values.scrollYProgress.set(y.progress);
|
||||
}, {
|
||||
...rest,
|
||||
container: getElement(container),
|
||||
target: getElement(target)
|
||||
});
|
||||
onCleanup(() => {
|
||||
cleanup();
|
||||
});
|
||||
}, { flush: "post" });
|
||||
return values;
|
||||
}
|
||||
//#endregion
|
||||
export { useScroll };
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { attachFollow, isMotionValue, motionValue } from "motion-dom";
|
||||
import { toValue, watch } from "vue";
|
||||
//#region src/value/use-spring.ts
|
||||
function useFollowValue(source, options = {}) {
|
||||
const value = motionValue(isMotionValue(source) ? source.get() : source);
|
||||
let cleanup;
|
||||
watch(() => toValue(options), (_1, _2, onCleanup) => {
|
||||
cleanup = attachFollow(value, source, toValue(options));
|
||||
onCleanup(() => {
|
||||
cleanup?.();
|
||||
});
|
||||
}, { immediate: true });
|
||||
return value;
|
||||
}
|
||||
function useSpring(source, config = {}) {
|
||||
const value = motionValue(isMotionValue(source) ? source.get() : source);
|
||||
watch(() => toValue(config), (_1, _2, onCleanup) => {
|
||||
const cleanup = attachFollow(value, source, {
|
||||
type: "spring",
|
||||
...toValue(config)
|
||||
});
|
||||
onCleanup(() => {
|
||||
cleanup?.();
|
||||
});
|
||||
}, { immediate: true });
|
||||
return value;
|
||||
}
|
||||
//#endregion
|
||||
export { useFollowValue, useSpring };
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { useAnimationFrame } from "../utils/use-animation-frame.mjs";
|
||||
import { motionValue } from "motion-dom";
|
||||
//#region src/value/use-time.ts
|
||||
function useTime() {
|
||||
const time = motionValue(0);
|
||||
useAnimationFrame((t) => time.set(t));
|
||||
return time;
|
||||
}
|
||||
//#endregion
|
||||
export { useTime };
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import { useCombineMotionValues } from "./use-combine-values.mjs";
|
||||
import { useComputed } from "./use-computed.mjs";
|
||||
import { motionValue, transform } from "motion-dom";
|
||||
import { isRef, watch } from "vue";
|
||||
//#region src/value/use-transform.ts
|
||||
function useTransform(input, inputRangeOrTransformer, outputRange, options) {
|
||||
if (typeof input === "function") return useComputed(input);
|
||||
if (outputRange && !Array.isArray(outputRange) && typeof outputRange === "object") {
|
||||
const result = {};
|
||||
for (const key in outputRange) if (Object.prototype.hasOwnProperty.call(outputRange, key)) {
|
||||
const keyOutputRange = outputRange[key];
|
||||
result[key] = useTransform(input, inputRangeOrTransformer, keyOutputRange, options);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
let inputValues;
|
||||
let transformer;
|
||||
if (typeof inputRangeOrTransformer === "function") {
|
||||
transformer = inputRangeOrTransformer;
|
||||
inputValues = Array.isArray(input) ? input : [input];
|
||||
} else if (isRef(inputRangeOrTransformer)) {
|
||||
const bridgeMV = motionValue(0);
|
||||
let currentTransformer = transform(inputRangeOrTransformer.value, outputRange, options);
|
||||
watch(inputRangeOrTransformer, (newRange) => {
|
||||
currentTransformer = transform(newRange, outputRange, options);
|
||||
bridgeMV.set(bridgeMV.get() + 1);
|
||||
}, { flush: "sync" });
|
||||
transformer = (values) => {
|
||||
return Array.isArray(values) ? currentTransformer(values[0]) : currentTransformer(values);
|
||||
};
|
||||
inputValues = Array.isArray(input) ? [...input, bridgeMV] : [input, bridgeMV];
|
||||
} else {
|
||||
transformer = transform(inputRangeOrTransformer, outputRange, options);
|
||||
inputValues = Array.isArray(input) ? input : [input];
|
||||
}
|
||||
const result = Array.isArray(input) ? useListTransform(inputValues, transformer) : useListTransform(inputValues, (values) => {
|
||||
return transformer(values[0]);
|
||||
});
|
||||
if (!Array.isArray(input)) {
|
||||
const inputAccelerate = input.accelerate;
|
||||
if (inputAccelerate && !inputAccelerate.isTransformed && typeof inputRangeOrTransformer !== "function" && Array.isArray(outputRange) && options?.clamp !== false) {
|
||||
const resolvedInputRange = isRef(inputRangeOrTransformer) ? inputRangeOrTransformer.value : inputRangeOrTransformer;
|
||||
result.accelerate = {
|
||||
...inputAccelerate,
|
||||
times: resolvedInputRange,
|
||||
keyframes: outputRange,
|
||||
isTransformed: true,
|
||||
...options?.ease ? { ease: options.ease } : {}
|
||||
};
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function useListTransform(values, transformer) {
|
||||
const latest = [];
|
||||
const combineValues = () => {
|
||||
latest.length = 0;
|
||||
const numValues = values.length;
|
||||
for (let i = 0; i < numValues; i++) latest[i] = values[i].get();
|
||||
return transformer(latest);
|
||||
};
|
||||
const { value, subscribe } = useCombineMotionValues(combineValues);
|
||||
subscribe(values);
|
||||
return value;
|
||||
}
|
||||
//#endregion
|
||||
export { useTransform };
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { useMotionValueEvent } from "./use-motion-value-event.mjs";
|
||||
import { frame, motionValue } from "motion-dom";
|
||||
//#region src/value/use-velocity.ts
|
||||
/**
|
||||
* 创建一个 MotionValue,当提供的 MotionValue 速度变化时更新
|
||||
*
|
||||
* ```javascript
|
||||
* const x = useMotionValue(0)
|
||||
* const xVelocity = useVelocity(x)
|
||||
* const xAcceleration = useVelocity(xVelocity)
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
function useVelocity(value) {
|
||||
const velocity = motionValue(value.getVelocity());
|
||||
const updateVelocity = () => {
|
||||
const latest = value.getVelocity();
|
||||
velocity.set(latest);
|
||||
/**
|
||||
* 如果还有速度,安排下一帧继续检查直到速度为零
|
||||
*/
|
||||
if (latest) frame.update(updateVelocity);
|
||||
};
|
||||
useMotionValueEvent(value, "change", () => {
|
||||
frame.update(updateVelocity, false, true);
|
||||
});
|
||||
return velocity;
|
||||
}
|
||||
//#endregion
|
||||
export { useVelocity };
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"name": "motion-v",
|
||||
"type": "module",
|
||||
"version": "2.8.0",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/motiondivision/motion-vue",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/motiondivision/motion-vue.git"
|
||||
},
|
||||
"keywords": [
|
||||
"vue",
|
||||
"motion",
|
||||
"motionone",
|
||||
"framer-motion"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/es/index.d.ts",
|
||||
"import": "./dist/es/index.mjs"
|
||||
},
|
||||
"./nuxt": {
|
||||
"types": "./dist/nuxt/index.d.mts",
|
||||
"import": "./dist/nuxt/index.mjs"
|
||||
},
|
||||
"./resolver": {
|
||||
"types": "./dist/resolver/index.d.mts",
|
||||
"import": "./dist/resolver/index.mjs"
|
||||
}
|
||||
},
|
||||
"main": "./dist/es/index.mjs",
|
||||
"module": "./dist/es/index.mjs",
|
||||
"types": "./dist/es/index.d.ts",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"./dist/es/index.d.ts"
|
||||
],
|
||||
"nuxt": [
|
||||
"./dist/nuxt/index.d.mts",
|
||||
"./dist/nuxt/index.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"./LICENSE",
|
||||
"./README.md",
|
||||
"./dist"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@vueuse/core": ">=10.0.0",
|
||||
"vue": ">=3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"framer-motion": "~14.3.0",
|
||||
"hey-listen": "^1.0.8",
|
||||
"motion-dom": "~14.3.0",
|
||||
"motion-utils": "~14.1.1"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"prebuild": "cp ../../README.md ./README.md || true",
|
||||
"build": "rimraf dist && vite build",
|
||||
"test": "vitest --dom",
|
||||
"coverage": "vitest run --coverage",
|
||||
"pub:release": "pnpm publish --access public"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user