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 (`
`): `anchor` is absent, falls back to `el`, the * loop stops after one node. * - Multi-root fragment (`
`): 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`). * - `` 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 `` 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 };