Teleport
import type { Node } from '@xyflow/vue';
import { nextTick, shallowRef } from 'vue';
import { getConnectedEdges, useVueFlow } from '@xyflow/vue';
export type TeleportNodeData = {
label?: string;
/** the selector the node is currently teleported into, or null when it lives in the graph */
destination?: string | null;
};
export type TeleportNode = Node<TeleportNodeData, 'teleportable'>;
type Animation = 'fade' | 'shrink';
/**
* Drives a single teleportable node: it stays a graph node (edges still track its position) but its DOM is
* moved into another element via Vue's `<Teleport>`. Animations that resize the node call
* `updateNodeInternals` so the handle positions — and therefore the connected edges — stay correct.
*/
export function useTeleport(id: string) {
const animation = shallowRef<Animation>('fade');
const transition = shallowRef(false);
const target = shallowRef<string | null>(null);
const { updateNodeInternals, updateNodeData, getNode, getEdges, setEdges } = useVueFlow<TeleportNode>();
// hide an edge while either of its endpoints is teleported out of the graph
function syncHiddenEdges(connectedEdgeIds: string[]) {
setEdges((edges) =>
edges.map((edge) =>
connectedEdgeIds.includes(edge.id)
? {
...edge,
hidden: !!getNode(edge.source)?.data.destination || !!getNode(edge.target)?.data.destination,
}
: edge,
),
);
}
function fade(destination: string | null, onFinish: () => void) {
setTimeout(() => {
target.value = destination;
setTimeout(() => {
transition.value = false;
// when returning to the graph, defer hiding edges until the node is back
if (!destination) {
onFinish();
}
}, 500);
}, 500);
}
function shrink(destination: string | null, onFinish: () => void) {
setTimeout(() => {
target.value = destination;
setTimeout(() => {
transition.value = false;
if (!destination) {
setTimeout(() => {
updateNodeInternals([id]);
nextTick(onFinish);
}, 500);
}
}, 500);
}, 500);
}
function onClick(destination: string | null) {
const node = getNode(id);
if (!node) {
return;
}
transition.value = true;
// remember where the node went so `syncHiddenEdges` can check both endpoints
updateNodeData(id, { destination });
const connectedEdgeIds = getConnectedEdges([node], [...getEdges.value]).map((edge) => edge.id);
// hide edges immediately when leaving the graph
if (destination) {
syncHiddenEdges(connectedEdgeIds);
}
const onFinish = () => syncHiddenEdges(connectedEdgeIds);
if (animation.value === 'shrink') {
shrink(destination, onFinish);
}
else {
fade(destination, onFinish);
}
}
return { animation, transition, target, onClick };
}Last updated on