From 5f5099d1fc67c511a1fbcb6583c2f2f419473b7a Mon Sep 17 00:00:00 2001 From: Hayleigh Thompson Date: Fri, 19 Jan 2024 09:38:32 +0000 Subject: [PATCH 01/38] :construction: Janky explorations. --- packages/react/src/hooks/useReactFlow.ts | 84 +++++++++++++++++------- 1 file changed, 62 insertions(+), 22 deletions(-) diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index bdd05aa0..097c78b6 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useRef } from 'react'; +import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { getElementsToRemove, getOverlappingArea, isRectObject, nodeToRect, type Rect } from '@xyflow/system'; import useViewportHelper from './useViewportHelper'; @@ -46,33 +46,73 @@ export function useReactFlow e.id === id) as EdgeType; }, []); - // this is used to handle multiple syncronous setNodes calls - const setNodesData = useRef(); - const setNodesTimeout = useRef>(); + const setNodesQueue = useRef<(NodeType[] | ((nodes: NodeType[]) => NodeType[]))[]>([]); + const setNodesHasStateReplacement = useRef(false); + const [setNodesShouldFlushNodesQueue, setSetNodesShouldFlushNodesQueue] = useState(false); const setNodes = useCallback>((payload) => { - const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup } = store.getState(); - const nextNodes = typeof payload === 'function' ? payload((setNodesData.current as NodeType[]) || nodes) : payload; + if (typeof payload === 'function' && !setNodesHasStateReplacement.current) { + setNodesQueue.current.push(payload); + setSetNodesShouldFlushNodesQueue(true); + } else { + setNodesQueue.current = [payload]; + setNodesHasStateReplacement.current = true; + setSetNodesShouldFlushNodesQueue(true); + } + }, []); - setNodesData.current = nextNodes; + useLayoutEffect(() => { + if (!setNodesShouldFlushNodesQueue) { + setNodesQueue.current = []; + setNodesHasStateReplacement.current = false; - if (setNodesTimeout.current) { - clearTimeout(setNodesTimeout.current); + return; } - // if there are multiple synchronous setNodes calls, we only want to call onNodesChange once - // for this, we use a timeout to wait for the last call and store updated nodes in setNodesData - // this is not perfect, but should work in most cases - setNodesTimeout.current = setTimeout(() => { - if (hasDefaultNodes) { - setNodes(nextNodes); - } else if (onNodesChange) { - const changes: NodeChange[] = getElementsDiffChanges({ items: setNodesData.current, lookup: nodeLookup }); - onNodesChange(changes); - } + const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup } = store.getState(); + const nextNodes = setNodesQueue.current.reduce( + (prev: NodeType[], payload) => (typeof payload === 'function' ? payload(prev) : payload), + nodes as NodeType[] + ); - setNodesData.current = undefined; - }, 0); - }, []); + if (hasDefaultNodes) { + setNodes(nextNodes); + } else if (onNodesChange) { + const changes: NodeChange[] = getElementsDiffChanges({ items: nextNodes, lookup: nodeLookup }); + onNodesChange(changes); + } + + setNodesQueue.current = []; + setNodesHasStateReplacement.current = false; + setSetNodesShouldFlushNodesQueue(false); + }, [setNodesShouldFlushNodesQueue]); + + // // this is used to handle multiple syncronous setNodes calls + // const setNodesData = useRef(); + // const setNodesTimeout = useRef>(); + // const setNodes = useCallback>((payload) => { + // const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup } = store.getState(); + // const nextNodes = typeof payload === 'function' ? payload((setNodesData.current as NodeType[]) || nodes) : payload; + + // setNodesData.current = nextNodes; + + // if (setNodesTimeout.current) { + // clearTimeout(setNodesTimeout.current); + // } + + // // if there are multiple synchronous setNodes calls, we only want to call onNodesChange once + // // for this, we use a timeout to wait for the last call and store updated nodes in setNodesData + // // this is not perfect, but should work in most cases + // setNodesTimeout.current = setTimeout(() => { + // if (hasDefaultNodes) { + // setNodes(nextNodes); + // } else if (onNodesChange) { + // const changes: NodeChange[] = getElementsDiffChanges({ items: setNodesData.current, lookup: nodeLookup }); + // onNodesChange(changes); + // } + + // setNodesData.current = undefined; + // }, 0); + // }, []); // this is used to handle multiple syncronous setEdges calls const setEdgesData = useRef(); From a25854646a1ebd8ea978bd4372f276c341398a29 Mon Sep 17 00:00:00 2001 From: Hayleigh Thompson Date: Fri, 19 Jan 2024 16:09:51 +0000 Subject: [PATCH 02/38] :sparkles: Batch calls to setNodes and addNodes for better perf. --- packages/react/src/hooks/useReactFlow.ts | 187 +++++++++++------------ 1 file changed, 87 insertions(+), 100 deletions(-) diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index 097c78b6..18de6fc6 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -46,122 +46,109 @@ export function useReactFlow e.id === id) as EdgeType; }, []); - const setNodesQueue = useRef<(NodeType[] | ((nodes: NodeType[]) => NodeType[]))[]>([]); - const setNodesHasStateReplacement = useRef(false); - const [setNodesShouldFlushNodesQueue, setSetNodesShouldFlushNodesQueue] = useState(false); - const setNodes = useCallback>((payload) => { - if (typeof payload === 'function' && !setNodesHasStateReplacement.current) { - setNodesQueue.current.push(payload); - setSetNodesShouldFlushNodesQueue(true); - } else { - setNodesQueue.current = [payload]; - setNodesHasStateReplacement.current = true; - setSetNodesShouldFlushNodesQueue(true); - } - }, []); + type SetElementsQueue = { + nodes: (NodeType[] | ((nodes: NodeType[]) => NodeType[]))[]; + edges: (EdgeType[] | ((edges: EdgeType[]) => EdgeType[]))[]; + }; + // A reference of all the batched updates to process before the next render. We + // want a mutable reference here so multiple synchronous calls to `setNodes` etc + // can be batched together. + const setElementsQueue = useRef({ nodes: [], edges: [] }); + // Because we're using a ref above, we need some way to let React know when to + // actually process the queue. We flip this bit of state to `true` any time we + // mutate the queue and then flip it back to `false` after flushing the queue. + const [shouldFlushQueue, setShouldFlushQueue] = useState(false); + + // Layout effects are guaranteed to run before the next render which means we + // shouldn't run into any issues with stale state or weird issues that come from + // rendering things one frame later than expected (we used to use `setTimeout`). useLayoutEffect(() => { - if (!setNodesShouldFlushNodesQueue) { - setNodesQueue.current = []; - setNodesHasStateReplacement.current = false; - + // Because we need to flip the state back to false after flushing, this should + // trigger the hook again (!). If the hook is being run again we know that any + // updates should have been processed by now and we can safely clear the queue + // and bail early. + if (!shouldFlushQueue) { + setElementsQueue.current = { nodes: [], edges: [] }; return; } - const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup } = store.getState(); - const nextNodes = setNodesQueue.current.reduce( - (prev: NodeType[], payload) => (typeof payload === 'function' ? payload(prev) : payload), - nodes as NodeType[] - ); + if (setElementsQueue.current.nodes.length) { + const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup } = store.getState(); - if (hasDefaultNodes) { - setNodes(nextNodes); - } else if (onNodesChange) { - const changes: NodeChange[] = getElementsDiffChanges({ items: nextNodes, lookup: nodeLookup }); - onNodesChange(changes); - } - - setNodesQueue.current = []; - setNodesHasStateReplacement.current = false; - setSetNodesShouldFlushNodesQueue(false); - }, [setNodesShouldFlushNodesQueue]); - - // // this is used to handle multiple syncronous setNodes calls - // const setNodesData = useRef(); - // const setNodesTimeout = useRef>(); - // const setNodes = useCallback>((payload) => { - // const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup } = store.getState(); - // const nextNodes = typeof payload === 'function' ? payload((setNodesData.current as NodeType[]) || nodes) : payload; - - // setNodesData.current = nextNodes; - - // if (setNodesTimeout.current) { - // clearTimeout(setNodesTimeout.current); - // } - - // // if there are multiple synchronous setNodes calls, we only want to call onNodesChange once - // // for this, we use a timeout to wait for the last call and store updated nodes in setNodesData - // // this is not perfect, but should work in most cases - // setNodesTimeout.current = setTimeout(() => { - // if (hasDefaultNodes) { - // setNodes(nextNodes); - // } else if (onNodesChange) { - // const changes: NodeChange[] = getElementsDiffChanges({ items: setNodesData.current, lookup: nodeLookup }); - // onNodesChange(changes); - // } - - // setNodesData.current = undefined; - // }, 0); - // }, []); - - // this is used to handle multiple syncronous setEdges calls - const setEdgesData = useRef(); - const setEdgesTimeout = useRef>(); - const setEdges = useCallback>((payload) => { - const { edges = [], setEdges, hasDefaultEdges, onEdgesChange, edgeLookup } = store.getState(); - const nextEdges = typeof payload === 'function' ? payload((setEdgesData.current as EdgeType[]) || edges) : payload; - - setEdgesData.current = nextEdges; - - if (setEdgesTimeout.current) { - clearTimeout(setEdgesTimeout.current); - } - - setEdgesTimeout.current = setTimeout(() => { - if (hasDefaultEdges) { - setEdges(nextEdges); - } else if (onEdgesChange) { - const changes: EdgeChange[] = getElementsDiffChanges({ items: nextEdges, lookup: edgeLookup }); - onEdgesChange(changes); + // This is essentially an `Array.reduce` in imperative clothing. Processing + // this queue is a relatively hot path so we'd like to avoid the overhead of + // array methods where we can. + let next = nodes as NodeType[]; + for (const payload of setElementsQueue.current.nodes) { + next = typeof payload === 'function' ? payload(next) : payload; } - setEdgesData.current = undefined; - }, 0); + if (hasDefaultNodes) { + setNodes(next); + } else if (onNodesChange) { + onNodesChange( + getElementsDiffChanges({ + items: next, + lookup: nodeLookup, + }) + ); + } + + setElementsQueue.current.nodes = []; + } + + if (setElementsQueue.current.edges.length) { + const { edges = [], setEdges, hasDefaultEdges, onEdgesChange, edgeLookup } = store.getState(); + + let next = edges as EdgeType[]; + for (const payload of setElementsQueue.current.edges) { + next = typeof payload === 'function' ? payload(next) : payload; + } + + if (hasDefaultEdges) { + setEdges(next); + } else if (onEdgesChange) { + onEdgesChange( + getElementsDiffChanges({ + items: next, + lookup: edgeLookup, + }) + ); + } + + setElementsQueue.current.edges = []; + } + + // Beacuse we're using reactive state to trigger this effect, we need to flip + // it back to false. + setShouldFlushQueue(false); + }, [shouldFlushQueue]); + + const setNodes = useCallback>((payload) => { + setElementsQueue.current.nodes.push(payload); + setShouldFlushQueue(true); + }, []); + + const setEdges = useCallback>((payload) => { + setElementsQueue.current.edges.push(payload); + setShouldFlushQueue(true); }, []); const addNodes = useCallback>((payload) => { - const nodes = Array.isArray(payload) ? payload : [payload]; - const { nodes: currentNodes, hasDefaultNodes, onNodesChange, setNodes } = store.getState(); + const newNodes = Array.isArray(payload) ? payload : [payload]; - if (hasDefaultNodes) { - const nextNodes = [...currentNodes, ...nodes]; - setNodes(nextNodes); - } else if (onNodesChange) { - const changes = nodes.map((node) => ({ item: node, type: 'add' } as NodeAddChange)); - onNodesChange(changes); - } + // Queueing a functional update means that we won't worry about other calls + // to `setNodes` that might happen elsewhere. + setElementsQueue.current.nodes.push((nodes) => [...nodes, ...newNodes]); + setShouldFlushQueue(true); }, []); const addEdges = useCallback>((payload) => { - const nextEdges = Array.isArray(payload) ? payload : [payload]; - const { edges = [], setEdges, hasDefaultEdges, onEdgesChange } = store.getState(); + const newEdges = Array.isArray(payload) ? payload : [payload]; - if (hasDefaultEdges) { - setEdges([...edges, ...nextEdges]); - } else if (onEdgesChange) { - const changes = nextEdges.map((edge) => ({ item: edge, type: 'add' } as EdgeAddChange)); - onEdgesChange(changes); - } + setElementsQueue.current.edges.push((edges) => [...edges, ...newEdges]); + setShouldFlushQueue(true); }, []); const toObject = useCallback>(() => { From 81b5ea61412cfccecdabdad5abacc0998fc70840 Mon Sep 17 00:00:00 2001 From: Hayleigh Thompson Date: Fri, 19 Jan 2024 16:10:06 +0000 Subject: [PATCH 03/38] :sparkles: Add an example demonstrating how batched updates work. --- examples/react/src/App/routes.ts | 6 ++ .../src/examples/SetNodesBatching/index.tsx | 70 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 examples/react/src/examples/SetNodesBatching/index.tsx diff --git a/examples/react/src/App/routes.ts b/examples/react/src/App/routes.ts index 76ab08e4..444586ca 100644 --- a/examples/react/src/App/routes.ts +++ b/examples/react/src/App/routes.ts @@ -28,6 +28,7 @@ import NodeTypesObjectChange from '../examples/NodeTypesObjectChange'; import Overview from '../examples/Overview'; import Provider from '../examples/Provider'; import SaveRestore from '../examples/SaveRestore'; +import SetNodesBatching from '../examples/SetNodesBatching'; import Stress from '../examples/Stress'; import Subflow from '../examples/Subflow'; import SwitchFlow from '../examples/Switch'; @@ -226,6 +227,11 @@ const routes: IRoute[] = [ path: 'save-restore', component: SaveRestore, }, + { + name: 'SetNodes Batching', + path: 'setnodes-batching', + component: SetNodesBatching, + }, { name: 'Stress', path: 'stress', diff --git a/examples/react/src/examples/SetNodesBatching/index.tsx b/examples/react/src/examples/SetNodesBatching/index.tsx new file mode 100644 index 00000000..2dc602eb --- /dev/null +++ b/examples/react/src/examples/SetNodesBatching/index.tsx @@ -0,0 +1,70 @@ +import { useCallback } from 'react'; +import { + ReactFlow, + MiniMap, + Background, + BackgroundVariant, + Controls, + ReactFlowProvider, + Node, + Edge, + useReactFlow, + Panel, +} from '@xyflow/react'; + +const a = { id: 'a', data: { label: 'A' }, position: { x: 250, y: 5 } }; +const b = { id: 'b', data: { label: 'B' }, position: { x: 100, y: 100 } }; +const c = { id: 'c', data: { label: 'C' }, position: { x: 400, y: 100 } }; + +const SetNotesBatchingFlow = () => { + const { setNodes, updateNode } = useReactFlow(); + + const triggerMultipleSetNodes = useCallback(() => { + setNodes([a]); + setNodes((nodes) => [...nodes, b]); + setNodes((nodes) => [...nodes, c]); + setNodes((nodes) => + nodes.map((node) => + node.id === 'a' ? { ...node, position: { x: node.position.x + 20, y: node.position.y + 20 } } : node + ) + ); + }, []); + + const triggerMultipleUpdateNodes = useCallback(() => { + triggerMultipleSetNodes(); + updateNode('a', (a) => ({ position: { x: a.position.x + 20, y: a.position.y + 20 } })); + updateNode('b', (b) => ({ position: { x: b.position.x + 20, y: b.position.y + 20 } })); + updateNode('c', (c) => ({ position: { x: c.position.x + 20, y: c.position.y + 20 } })); + updateNode('a', (a) => ({ data: { ...a.data, label: `A ${Date.now()}` } })); + updateNode('b', (b) => ({ data: { ...b.data, label: `B ${Date.now()}` } })); + updateNode('c', (c) => ({ data: { ...c.data, label: `C ${Date.now()}` } })); + }, []); + + return ( + + + + + + + + + + + ); +}; + +export default function App() { + return ( + + + + ); +} From 2b0fa97afebf1b9c38169e723a41419a37c9fbeb Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 22 Jan 2024 11:42:20 +0100 Subject: [PATCH 04/38] chore(examples): second input for useNodesData --- .../react/src/examples/UseNodesData/TextNode.tsx | 12 ++++++++++-- packages/react/src/hooks/useReactFlow.ts | 11 +---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/examples/react/src/examples/UseNodesData/TextNode.tsx b/examples/react/src/examples/UseNodesData/TextNode.tsx index 113d41fb..dd9f25d1 100644 --- a/examples/react/src/examples/UseNodesData/TextNode.tsx +++ b/examples/react/src/examples/UseNodesData/TextNode.tsx @@ -14,8 +14,16 @@ function TextNode({ id, data }: NodeProps) { return (
node {id}
-
- updateText(evt.target.value)} value={text} /> +
+ + updateText(evt.target.value)} value={text} style={{ display: 'block' }} /> + + + updateNodeData(id, { text: evt.target.value })} + value={data.text} + style={{ display: 'block' }} + />
diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index 18de6fc6..02080731 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -3,16 +3,7 @@ import { getElementsToRemove, getOverlappingArea, isRectObject, nodeToRect, type import useViewportHelper from './useViewportHelper'; import { useStoreApi } from './useStore'; -import type { - ReactFlowInstance, - Instance, - NodeAddChange, - EdgeAddChange, - Node, - Edge, - NodeChange, - EdgeChange, -} from '../types'; +import type { ReactFlowInstance, Instance, Node, Edge } from '../types'; import { getElementsDiffChanges, isNode } from '../utils'; /** From abc6fea20ba68aab0472fef2edf7c2cb3c8a1e59 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Tue, 23 Jan 2024 12:35:24 +0100 Subject: [PATCH 05/38] child nodes get repositioned correctly when resizing top or left --- .../react/src/examples/NodeResizer/index.tsx | 23 +++++++++++ .../routes/examples/node-resizer/+page.svelte | 23 +++++++++++ .../NodeResizer/NodeResizeControl.tsx | 21 +++++++++- .../plugins/NodeResizer/ResizeControl.svelte | 12 +++++- packages/system/src/xyresizer/XYResizer.ts | 39 +++++++++++++++++-- 5 files changed, 111 insertions(+), 7 deletions(-) diff --git a/examples/react/src/examples/NodeResizer/index.tsx b/examples/react/src/examples/NodeResizer/index.tsx index 11ce29d2..f8e46940 100644 --- a/examples/react/src/examples/NodeResizer/index.tsx +++ b/examples/react/src/examples/NodeResizer/index.tsx @@ -116,6 +116,29 @@ const initialNodes: Node[] = [ position: { x: 250, y: 400 }, style: { ...nodeStyle }, }, + { + id: '5', + type: 'defaultResizer', + data: { label: 'Parent' }, + position: { x: 700, y: 0 }, + style: { ...nodeStyle, width: 300, height: 300 }, + }, + { + id: '5a', + type: 'defaultResizer', + data: { label: 'Child' }, + position: { x: 50, y: 50 }, + parentNode: '5', + style: { ...nodeStyle }, + }, + { + id: '5b', + type: 'defaultResizer', + data: { label: 'Child' }, + position: { x: 100, y: 100 }, + parentNode: '5', + style: { ...nodeStyle }, + }, ]; const CustomNodeFlow = () => { diff --git a/examples/svelte/src/routes/examples/node-resizer/+page.svelte b/examples/svelte/src/routes/examples/node-resizer/+page.svelte index 61257ee1..39778156 100644 --- a/examples/svelte/src/routes/examples/node-resizer/+page.svelte +++ b/examples/svelte/src/routes/examples/node-resizer/+page.svelte @@ -100,6 +100,29 @@ data: { label: 'horizontal resizer with maxWidth', maxWidth: 300 }, position: { x: 250, y: 400 }, style: nodeStyle + }, + { + id: '5', + type: 'defaultResizer', + data: { label: 'Parent' }, + position: { x: 700, y: 0 }, + style: nodeStyle + 'width: 300px; height: 300px' + }, + { + id: '5a', + type: 'defaultResizer', + data: { label: 'Child' }, + position: { x: 50, y: 50 }, + parentNode: '5', + style: nodeStyle + }, + { + id: '5b', + type: 'defaultResizer', + data: { label: 'Child' }, + position: { x: 100, y: 100 }, + parentNode: '5', + style: nodeStyle } ]); diff --git a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx index 2f16f150..525b68f7 100644 --- a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx +++ b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx @@ -1,6 +1,12 @@ import { useRef, useEffect, memo } from 'react'; import cc from 'classcat'; -import { XYResizer, ResizeControlVariant, type XYResizerInstance, type XYResizerChange } from '@xyflow/system'; +import { + XYResizer, + ResizeControlVariant, + type XYResizerInstance, + type XYResizerChange, + XYResizerChildChange, +} from '@xyflow/system'; import { useStoreApi } from '../../hooks/useStore'; import { useNodeId } from '../../contexts/NodeIdContext'; @@ -52,7 +58,7 @@ function ResizeControl({ snapToGrid, }; }, - onChange: (change: XYResizerChange) => { + onChange: (change: XYResizerChange, childChanges: XYResizerChildChange[]) => { const { triggerNodeChanges } = store.getState(); const changes: NodeChange[] = []; @@ -83,6 +89,17 @@ function ResizeControl({ changes.push(dimensionChange); } + + childChanges.forEach((childChange) => { + const positionChange: NodePositionChange = { + id: childChange.id, + type: 'position', + position: childChange.position, + }; + + changes.push(positionChange); + }); + triggerNodeChanges(changes); }, onEnd: () => { diff --git a/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte b/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte index 4688b4e7..3ec0ad71 100644 --- a/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte +++ b/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte @@ -7,7 +7,8 @@ ResizeControlVariant, type ControlPosition, type XYResizerInstance, - type XYResizerChange + type XYResizerChange, + type XYResizerChildChange } from '@xyflow/system'; import type { ResizeControlProps } from './types'; @@ -66,7 +67,7 @@ snapToGrid: !!$snapGrid }; }, - onChange: (change: XYResizerChange) => { + onChange: (change: XYResizerChange, childChanges: XYResizerChildChange[]) => { const node = $nodeLookup.get(id); if (node) { node.height = change.isHeightChange ? change.height : node.height; @@ -76,6 +77,13 @@ ? { x: change.x, y: change.y } : node.position; + childChanges.forEach((childChange) => { + const childNode = $nodeLookup.get(childChange.id); + if (childNode) { + childNode.position = childChange.position; + } + }); + $nodes = $nodes; } } diff --git a/packages/system/src/xyresizer/XYResizer.ts b/packages/system/src/xyresizer/XYResizer.ts index 63f146d5..c4c97237 100644 --- a/packages/system/src/xyresizer/XYResizer.ts +++ b/packages/system/src/xyresizer/XYResizer.ts @@ -3,7 +3,7 @@ import { select } from 'd3-selection'; import { getControlDirection, getDimensionsAfterResize, getPositionAfterResize, getResizeDirection } from './utils'; import { getPointerPosition } from '../utils'; -import type { NodeLookup, Transform } from '../types'; +import type { NodeBase, NodeLookup, Transform } from '../types'; import type { OnResize, OnResizeEnd, OnResizeStart, ResizeDragEvent, ShouldResize, ControlPosition } from './types'; const initPrevValues = { width: 0, height: 0, x: 0, y: 0 }; @@ -28,6 +28,14 @@ const initChange = { export type XYResizerChange = typeof initChange; +export type XYResizerChildChange = { + id: string; + position: { + x: number; + y: number; + }; +}; + type XYResizerParams = { domNode: HTMLDivElement; nodeId: string; @@ -37,7 +45,7 @@ type XYResizerParams = { snapGrid?: [number, number]; snapToGrid: boolean; }; - onChange: (changes: XYResizerChange) => void; + onChange: (changes: XYResizerChange, childChanges: XYResizerChildChange[]) => void; onEnd?: () => void; }; @@ -78,6 +86,8 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize const controlDirection = getControlDirection(controlPosition); + let childNodes: XYResizerChildChange[] = []; + const dragHandler = drag() .on('start', (event: ResizeDragEvent) => { const { nodeLookup, transform, snapGrid, snapToGrid } = getStoreItems(); @@ -98,6 +108,15 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize aspectRatio: prevValues.width / prevValues.height, }; + childNodes = []; + nodeLookup.forEach((_node, _nodeId) => { + if (_node.parentNode === nodeId) { + childNodes.push({ + id: _nodeId, + position: { ..._node.position }, + }); + } + }); onResizeStart?.(event, { ...prevValues }); }) .on('drag', (event: ResizeDragEvent) => { @@ -105,6 +124,8 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize const pointerPosition = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); const node = nodeLookup.get(nodeId); + let childChanges: XYResizerChildChange[] = []; + if (node) { const change = { ...initChange }; @@ -137,6 +158,18 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize prevValues.x = change.x; prevValues.y = change.y; } + + if (childNodes.length > 0) { + const xChange = x - prevX; + const yChange = y - prevY; + childNodes.forEach((childNode) => { + childNode.position = { + x: childNode.position.x - xChange, + y: childNode.position.y - yChange, + }; + childChanges.push(childNode); + }); + } } if (isWidthChange || isHeightChange) { @@ -170,7 +203,7 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize } onResize?.(event, nextValues); - onChange(change); + onChange(change, childChanges); } }) .on('end', (event: ResizeDragEvent) => { From 1bd71af135a153d4ade8eda3db93b071dda33e0b Mon Sep 17 00:00:00 2001 From: peterkogo Date: Tue, 23 Jan 2024 12:37:38 +0100 Subject: [PATCH 06/38] use destructuring --- .../additional-components/NodeResizer/NodeResizeControl.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx index 525b68f7..c76642ed 100644 --- a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx +++ b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx @@ -92,9 +92,8 @@ function ResizeControl({ childChanges.forEach((childChange) => { const positionChange: NodePositionChange = { - id: childChange.id, + ...childChange, type: 'position', - position: childChange.position, }; changes.push(positionChange); From c3104f1fbba19ed68dfd73cd7607dcd0ee05129b Mon Sep 17 00:00:00 2001 From: peterkogo Date: Tue, 23 Jan 2024 18:16:05 +0100 Subject: [PATCH 07/38] refactored updateNodePositions in svelte --- packages/svelte/src/lib/store/index.ts | 34 +++++++++++--------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index 91160860..1cdc8904 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -63,29 +63,23 @@ export function createStore({ store.edges.set(addEdgeUtil(edgeParams, edges)); } - const updateNodePositions: UpdateNodePositions = (nodeDragItems, dragging = false) => { - store.nodes.update((nds) => { - return nds.map((node) => { - const nodeDragItem = (nodeDragItems as Array).find( - (ndi) => ndi.id === node.id - ); + const updateNodePositions: UpdateNodePositions = (nodeDragItems, _, dragging = false) => { + const nodeLookup = get(store.nodeLookup); - if (nodeDragItem) { - return { - ...node, - dragging, - position: nodeDragItem.position, - computed: { - ...node.computed, - positionAbsolute: nodeDragItem.computed?.positionAbsolute - }, - [internalsSymbol]: node[internalsSymbol] - }; - } + nodeDragItems.forEach((nodeDragItem) => { + const node = nodeLookup.get(nodeDragItem.id); - return node; - }); + if (node) { + node.position = nodeDragItem.position; + node.dragging = dragging; + node.computed = { + ...node.computed, + positionAbsolute: nodeDragItem.computed?.positionAbsolute + }; + } }); + + store.nodes.set(get(store.nodes)); }; function updateNodeDimensions(updates: Map) { From 5d3efa73a8819362193964f804ae59b71697863b Mon Sep 17 00:00:00 2001 From: peterkogo Date: Wed, 24 Jan 2024 12:25:39 +0100 Subject: [PATCH 08/38] refactored useUpdateNodePositions to useMoveSelectedNodes --- .../src/components/NodeWrapper/index.tsx | 11 +++++----- .../src/components/NodesSelection/index.tsx | 11 +++++----- ...dePositions.ts => useMoveSelectedNodes.ts} | 21 +++++++++---------- 3 files changed, 20 insertions(+), 23 deletions(-) rename packages/react/src/hooks/{useUpdateNodePositions.ts => useMoveSelectedNodes.ts} (70%) diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index 1e796de1..ebf65163 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -14,7 +14,7 @@ import { useStore, useStoreApi } from '../../hooks/useStore'; import { Provider } from '../../contexts/NodeIdContext'; import { ARIA_NODE_DESC_KEY } from '../A11yDescriptions'; import { useDrag } from '../../hooks/useDrag'; -import { useUpdateNodePositions } from '../../hooks/useUpdateNodePositions'; +import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes'; import { handleNodeClick } from '../Nodes/utils'; import { arrowKeyDiffs, builtinNodeTypes } from './utils'; import type { NodeWrapperProps } from '../../types'; @@ -79,7 +79,7 @@ export function NodeWrapper({ const prevTargetPosition = useRef(node.targetPosition); const prevType = useRef(nodeType); - const updatePositions = useUpdateNodePositions(); + const moveSelectedNodes = useMoveSelectedNodes(); useEffect(() => { if (nodeRef.current && !node.hidden) { @@ -188,10 +188,9 @@ export function NodeWrapper({ .toLowerCase()}. New position, x: ${~~positionAbsoluteX}, y: ${~~positionAbsoluteY}`, }); - updatePositions({ - x: arrowKeyDiffs[event.key].x, - y: arrowKeyDiffs[event.key].y, - isShiftPressed: event.shiftKey, + moveSelectedNodes({ + direction: arrowKeyDiffs[event.key], + factor: event.shiftKey ? 4 : 1, }); } }; diff --git a/packages/react/src/components/NodesSelection/index.tsx b/packages/react/src/components/NodesSelection/index.tsx index f8aeb77c..8f2e7602 100644 --- a/packages/react/src/components/NodesSelection/index.tsx +++ b/packages/react/src/components/NodesSelection/index.tsx @@ -10,7 +10,7 @@ import { getNodesBounds } from '@xyflow/system'; import { useStore, useStoreApi } from '../../hooks/useStore'; import { useDrag } from '../../hooks/useDrag'; -import { useUpdateNodePositions } from '../../hooks/useUpdateNodePositions'; +import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes'; import { arrowKeyDiffs } from '../NodeWrapper/utils'; import type { Node, ReactFlowState } from '../../types'; @@ -35,7 +35,7 @@ const selector = (s: ReactFlowState) => { export function NodesSelection({ onSelectionContextMenu, noPanClassName, disableKeyboardA11y }: NodesSelectionProps) { const store = useStoreApi(); const { width, height, transformString, userSelectionActive } = useStore(selector, shallow); - const updatePositions = useUpdateNodePositions(); + const moveSelectedNodes = useMoveSelectedNodes(); const nodeRef = useRef(null); @@ -64,10 +64,9 @@ export function NodesSelection({ onSelectionContextMenu, noPanClassName, disable const onKeyDown = (event: KeyboardEvent) => { if (Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)) { - updatePositions({ - x: arrowKeyDiffs[event.key].x, - y: arrowKeyDiffs[event.key].y, - isShiftPressed: event.shiftKey, + moveSelectedNodes({ + direction: arrowKeyDiffs[event.key], + factor: event.shiftKey ? 4 : 1, }); } }; diff --git a/packages/react/src/hooks/useUpdateNodePositions.ts b/packages/react/src/hooks/useMoveSelectedNodes.ts similarity index 70% rename from packages/react/src/hooks/useUpdateNodePositions.ts rename to packages/react/src/hooks/useMoveSelectedNodes.ts index 1ca0d4ee..5e54ef50 100644 --- a/packages/react/src/hooks/useUpdateNodePositions.ts +++ b/packages/react/src/hooks/useMoveSelectedNodes.ts @@ -1,32 +1,31 @@ import { useCallback } from 'react'; -import { calcNextPosition, snapPosition } from '@xyflow/system'; +import { XYPosition, calcNextPosition, snapPosition } from '@xyflow/system'; import { Node } from '../types'; -import { useStoreApi } from '../hooks/useStore'; +import { useStoreApi } from './useStore'; const selectedAndDraggable = (nodesDraggable: boolean) => (n: Node) => n.selected && (n.draggable || (nodesDraggable && typeof n.draggable === 'undefined')); /** - * Hook for updating node positions. + * Hook for updating node positions with keyboard presses * * @internal * @returns function for updating node positions */ -export function useUpdateNodePositions() { +export function useMoveSelectedNodes() { const store = useStoreApi(); - const updatePositions = useCallback((params: { x: number; y: number; isShiftPressed: boolean }) => { + const moveSelectedNodes = useCallback((params: { direction: XYPosition; factor: number }) => { const { nodeExtent, nodes, snapToGrid, snapGrid, nodesDraggable, onError, updateNodePositions } = store.getState(); const selectedNodes = nodes.filter(selectedAndDraggable(nodesDraggable)); - // by default a node moves 5px on each key press, or 20px if shift is pressed - // if snap grid is enabled, we use that for the velocity. + // by default a node moves 5px on each key press + // if snap grid is enabled, we use that for the velocity const xVelo = snapToGrid ? snapGrid[0] : 5; const yVelo = snapToGrid ? snapGrid[1] : 5; - const factor = params.isShiftPressed ? 4 : 1; - const xDiff = params.x * xVelo * factor; - const yDiff = params.y * yVelo * factor; + const xDiff = params.direction.x * xVelo * params.factor; + const yDiff = params.direction.y * yVelo * params.factor; const nodeUpdates = selectedNodes.map((node) => { if (node.computed?.positionAbsolute) { @@ -61,5 +60,5 @@ export function useUpdateNodePositions() { updateNodePositions(nodeUpdates, true, false); }, []); - return updatePositions; + return moveSelectedNodes; } From 829c82cfda95c8e863c23a00791f36a2887cf5d0 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Wed, 24 Jan 2024 13:44:28 +0100 Subject: [PATCH 09/38] fixed, parentExpand & simplified updateNodePositions --- packages/react/src/store/index.ts | 9 +++------ packages/react/src/utils/changes.ts | 3 ++- packages/svelte/src/lib/store/index.ts | 2 +- packages/system/src/types/general.ts | 6 +----- packages/system/src/xydrag/XYDrag.ts | 4 ++-- 5 files changed, 9 insertions(+), 15 deletions(-) diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 13311a1b..173d5e5c 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -151,19 +151,16 @@ const createRFStore = ({ onNodesChange?.(changes); } }, - updateNodePositions: (nodeDragItems, positionChanged = true, dragging = false) => { + updateNodePositions: (nodeDragItems, dragging = false) => { const changes = nodeDragItems.map((node) => { const change: NodePositionChange = { id: node.id, type: 'position', + position: node.position, + positionAbsolute: node.computed?.positionAbsolute, dragging, }; - if (positionChanged) { - change.positionAbsolute = node.computed?.positionAbsolute; - change.position = node.position; - } - return change; }); diff --git a/packages/react/src/utils/changes.ts b/packages/react/src/utils/changes.ts index 6bcddabd..f295b815 100644 --- a/packages/react/src/utils/changes.ts +++ b/packages/react/src/utils/changes.ts @@ -3,6 +3,7 @@ import { EdgeLookup, NodeLookup } from '@xyflow/system'; import type { Node, Edge, EdgeChange, NodeChange, NodeSelectionChange, EdgeSelectionChange } from '../types'; export function handleParentExpand(updatedElements: any[], updateItem: any) { + console.log(updatedElements, updateItem); for (const [index, item] of updatedElements.entries()) { if (item.id === updateItem.parentNode) { const parent = { ...item }; @@ -106,7 +107,7 @@ function applyChanges(changes: any[], elements: any[]): any[] { const updatedElement = { ...element }; for (const change of changes) { - applyChange(change, updatedElement, elements); + applyChange(change, updatedElement, updatedElements); } updatedElements.push(updatedElement); diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index 1cdc8904..f2b74e03 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -63,7 +63,7 @@ export function createStore({ store.edges.set(addEdgeUtil(edgeParams, edges)); } - const updateNodePositions: UpdateNodePositions = (nodeDragItems, _, dragging = false) => { + const updateNodePositions: UpdateNodePositions = (nodeDragItems, dragging = false) => { const nodeLookup = get(store.nodeLookup); nodeDragItems.forEach((nodeDragItem) => { diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index 5fe4a844..b52930ef 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -123,11 +123,7 @@ export type SelectionRect = Rect & { export type OnError = (id: string, message: string) => void; -export type UpdateNodePositions = ( - dragItems: NodeDragItem[] | NodeBase[], - positionChanged?: boolean, - dragging?: boolean -) => void; +export type UpdateNodePositions = (dragItems: NodeDragItem[] | NodeBase[], dragging?: boolean) => void; export type PanBy = (delta: XYPosition) => boolean; export type UpdateConnection = (params: { diff --git a/packages/system/src/xydrag/XYDrag.ts b/packages/system/src/xydrag/XYDrag.ts index 40d1aa4b..78eefc74 100644 --- a/packages/system/src/xydrag/XYDrag.ts +++ b/packages/system/src/xydrag/XYDrag.ts @@ -164,7 +164,7 @@ export function XYDrag voi return; } - updateNodePositions(dragItems, true, true); + updateNodePositions(dragItems, true); const onNodeOrSelectionDrag = nodeId ? onNodeDrag : wrapSelectionDragFunc(onSelectionDrag); if (dragEvent && (onDrag || onNodeOrSelectionDrag)) { @@ -296,7 +296,7 @@ export function XYDrag voi const { nodeLookup, updateNodePositions, onNodeDragStop, onSelectionDragStop } = getStoreItems(); const onNodeOrSelectionDragStop = nodeId ? onNodeDragStop : wrapSelectionDragFunc(onSelectionDragStop); - updateNodePositions(dragItems, false, false); + updateNodePositions(dragItems, false); if (onDragStop || onNodeOrSelectionDragStop) { const [currentNode, currentNodes] = getEventHandlerParams({ From ef12aa413e50ffd582d48f0be28b9b68dbb667b4 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Wed, 24 Jan 2024 13:53:44 +0100 Subject: [PATCH 10/38] removed console print and fixed updatePosition --- packages/react/src/hooks/useMoveSelectedNodes.ts | 2 +- packages/react/src/utils/changes.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react/src/hooks/useMoveSelectedNodes.ts b/packages/react/src/hooks/useMoveSelectedNodes.ts index 5e54ef50..67abb9ff 100644 --- a/packages/react/src/hooks/useMoveSelectedNodes.ts +++ b/packages/react/src/hooks/useMoveSelectedNodes.ts @@ -57,7 +57,7 @@ export function useMoveSelectedNodes() { return node; }); - updateNodePositions(nodeUpdates, true, false); + updateNodePositions(nodeUpdates); }, []); return moveSelectedNodes; diff --git a/packages/react/src/utils/changes.ts b/packages/react/src/utils/changes.ts index f295b815..3bd0840d 100644 --- a/packages/react/src/utils/changes.ts +++ b/packages/react/src/utils/changes.ts @@ -3,7 +3,6 @@ import { EdgeLookup, NodeLookup } from '@xyflow/system'; import type { Node, Edge, EdgeChange, NodeChange, NodeSelectionChange, EdgeSelectionChange } from '../types'; export function handleParentExpand(updatedElements: any[], updateItem: any) { - console.log(updatedElements, updateItem); for (const [index, item] of updatedElements.entries()) { if (item.id === updateItem.parentNode) { const parent = { ...item }; From 39ea6013326bc6de074caf6783e1bd4626c2a069 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Thu, 25 Jan 2024 18:20:02 +0100 Subject: [PATCH 11/38] resizer now respects parentExtent --- packages/system/src/xyresizer/XYResizer.ts | 37 ++++- packages/system/src/xyresizer/utils.ts | 167 +++++++++++++++++---- 2 files changed, 165 insertions(+), 39 deletions(-) diff --git a/packages/system/src/xyresizer/XYResizer.ts b/packages/system/src/xyresizer/XYResizer.ts index c4c97237..ae81b1a0 100644 --- a/packages/system/src/xyresizer/XYResizer.ts +++ b/packages/system/src/xyresizer/XYResizer.ts @@ -3,7 +3,7 @@ import { select } from 'd3-selection'; import { getControlDirection, getDimensionsAfterResize, getPositionAfterResize, getResizeDirection } from './utils'; import { getPointerPosition } from '../utils'; -import type { NodeBase, NodeLookup, Transform } from '../types'; +import type { CoordinateExtent, NodeBase, NodeLookup, Transform } from '../types'; import type { OnResize, OnResizeEnd, OnResizeStart, ResizeDragEvent, ShouldResize, ControlPosition } from './types'; const initPrevValues = { width: 0, height: 0, x: 0, y: 0 }; @@ -34,6 +34,7 @@ export type XYResizerChildChange = { x: number; y: number; }; + extent?: 'parent' | CoordinateExtent; }; type XYResizerParams = { @@ -69,6 +70,13 @@ export type XYResizerInstance = { destroy: () => void; }; +function nodeToParentExtent(node: NodeBase): CoordinateExtent { + return [ + [0, 0], + [node.computed!.width!, node.computed!.height!], + ]; +} + export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResizerParams): XYResizerInstance { const selection = select(domNode); @@ -114,6 +122,7 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize childNodes.push({ id: _nodeId, position: { ..._node.position }, + extent: _node.extent, }); } }); @@ -131,20 +140,31 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize const { x: prevX, y: prevY, width: prevWidth, height: prevHeight } = prevValues; - const { width, height } = getDimensionsAfterResize( + let extent = undefined; + if (node.extent === 'parent') { + const parentNode = nodeLookup.get(node.parentNode!); + if (parentNode) { + extent = nodeToParentExtent(parentNode); + } + } + + const { width, height, x, y } = getDimensionsAfterResize( startValues, controlDirection, pointerPosition, boundaries, - keepAspectRatio + keepAspectRatio, + extent ); const isWidthChange = width !== prevWidth; const isHeightChange = height !== prevHeight; if (controlDirection.affectsX || controlDirection.affectsY) { - const { x, y } = getPositionAfterResize(startValues, controlDirection, width, height); - + // const { + // x, + // y, + // } = getPositionAfterResize(startValues, controlDirection, width, height, extent); // only transform the node if the width or height changes const isXPosChange = x !== prevX && isWidthChange; const isYPosChange = y !== prevY && isHeightChange; @@ -173,12 +193,15 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize } if (isWidthChange || isHeightChange) { + if (extent) { + } + // console.log(clampedX, clampedY); change.isWidthChange = isWidthChange; change.isHeightChange = isHeightChange; change.width = width; change.height = height; - prevValues.width = width; - prevValues.height = height; + prevValues.width = change.width; + prevValues.height = change.height; } if (!change.isXPosChange && !change.isYPosChange && !isWidthChange && !isHeightChange) { diff --git a/packages/system/src/xyresizer/utils.ts b/packages/system/src/xyresizer/utils.ts index 0d7d8a9e..ab54163a 100644 --- a/packages/system/src/xyresizer/utils.ts +++ b/packages/system/src/xyresizer/utils.ts @@ -1,3 +1,4 @@ +import { CoordinateExtent } from '../types'; import { clamp, getPointerPosition } from '../utils'; import { ControlPosition } from './types'; @@ -75,6 +76,18 @@ type StartValues = PrevValues & { aspectRatio: number; }; +function getLowerExtentClamp(lowerExtent: number, lowerBound: number) { + return Math.max(0, lowerBound - lowerExtent); +} + +function getUpperExtentClamp(upperExtent: number, upperBound: number) { + return Math.max(0, upperExtent - upperBound); +} + +function getSizeClamp(size: number, minSize: number, maxSize: number) { + return Math.max(0, minSize - size, size - maxSize); +} + /** * Calculates new width & height of node after resize based on pointer position * @param startValues - starting values of resize @@ -89,48 +102,128 @@ export function getDimensionsAfterResize( controlDirection: ReturnType, pointerPosition: ReturnType, boundaries: { minWidth: number; maxWidth: number; minHeight: number; maxHeight: number }, - keepAspectRatio: boolean + keepAspectRatio: boolean, + extent?: CoordinateExtent ) { - const { isHorizontal, isVertical, affectsX, affectsY } = controlDirection; + let { affectsX, affectsY } = controlDirection; + const { isHorizontal, isVertical } = controlDirection; + const isDiagonal = isHorizontal && isVertical; + const { xSnapped, ySnapped } = pointerPosition; const { minWidth, maxWidth, minHeight, maxHeight } = boundaries; - const { pointerX: startX, pointerY: startY, width: startWidth, height: startHeight, aspectRatio } = startValues; - const distX = Math.floor(isHorizontal ? xSnapped - startX : 0); - const distY = Math.floor(isVertical ? ySnapped - startY : 0); + const { x: startX, y: startY, width: startWidth, height: startHeight, aspectRatio } = startValues; + let distX = Math.floor(isHorizontal ? xSnapped - startValues.pointerX : 0); + let distY = Math.floor(isVertical ? ySnapped - startValues.pointerY : 0); - let width = clamp(startWidth + (affectsX ? -distX : distX), minWidth, maxWidth); - let height = clamp(startHeight + (affectsY ? -distY : distY), minHeight, maxHeight); + let newWidth = startWidth + (affectsX ? -distX : distX); + let newHeight = startHeight + (affectsY ? -distY : distY); - if (keepAspectRatio) { - const nextAspectRatio = width / height; - const isDiagonal = isHorizontal && isVertical; - const isOnlyHorizontal = isHorizontal && !isVertical; - const isOnlyVertical = isVertical && !isHorizontal; + // Check if maxWidth, minWWidth, maxHeight, minHeight are restricting the resize + let clampX = getSizeClamp(newWidth, minWidth, maxWidth); + let clampY = getSizeClamp(newHeight, minHeight, maxHeight); - width = (nextAspectRatio <= aspectRatio && isDiagonal) || isOnlyVertical ? height * aspectRatio : width; - height = (nextAspectRatio > aspectRatio && isDiagonal) || isOnlyHorizontal ? width / aspectRatio : height; - - if (width >= maxWidth) { - width = maxWidth; - height = maxWidth / aspectRatio; - } else if (width <= minWidth) { - width = minWidth; - height = minWidth / aspectRatio; + // Check if extent is restricting the resize + if (extent) { + let xExtentClamp = 0; + let yExtentClamp = 0; + if (affectsX && distX < 0) { + xExtentClamp = getLowerExtentClamp(startX + distX, extent[0][0]); + } else if (!affectsX && distX > 0) { + xExtentClamp = getUpperExtentClamp(startX + newWidth, extent[1][0]); } - if (height >= maxHeight) { - height = maxHeight; - width = maxHeight * aspectRatio; - } else if (height <= minHeight) { - height = minHeight; - width = minHeight * aspectRatio; + if (affectsY && distY < 0) { + yExtentClamp = getLowerExtentClamp(startY + distY, extent[0][1]); + } else if (!affectsY && distY > 0) { + yExtentClamp = getUpperExtentClamp(startY + newHeight, extent[1][1]); + } + + clampX = Math.max(clampX, xExtentClamp); + clampY = Math.max(clampY, yExtentClamp); + } + + // Check if the aspect ratio resizing of the other side is restricting the resize + if (keepAspectRatio) { + if (isHorizontal) { + // Check if the max dimensions might be restricting the resize + const aspectHeightClamp = getSizeClamp(newWidth / aspectRatio, minHeight, maxHeight) * aspectRatio; + clampX = Math.max(clampX, aspectHeightClamp); + + // Check if the extent is restricting the resize + if (extent) { + let aspectExtentClamp = 0; + if ((!affectsX && !affectsY) || (affectsX && !affectsY && isDiagonal)) { + aspectExtentClamp = getUpperExtentClamp(startY + newWidth / aspectRatio, extent[1][1]) * aspectRatio; + } else { + aspectExtentClamp = + getLowerExtentClamp(startY + (affectsX ? distX : -distX) / aspectRatio, extent[0][1]) * aspectRatio; + } + clampX = Math.max(clampX, aspectExtentClamp); + } + } + + if (isVertical) { + const aspectWidthClamp = getSizeClamp(newHeight * aspectRatio, minWidth, maxWidth) / aspectRatio; + clampY = Math.max(clampY, aspectWidthClamp); + + if (extent) { + let aspectExtentClamp = 0; + if ((!affectsX && !affectsY) || (affectsY && !affectsX && isDiagonal)) { + aspectExtentClamp = getUpperExtentClamp(startX + newHeight * aspectRatio, extent[1][0]) / aspectRatio; + } else { + aspectExtentClamp = + getLowerExtentClamp(startX + (affectsY ? distY : -distY) * aspectRatio, extent[0][0]) / aspectRatio; + } + clampY = Math.max(clampY, aspectExtentClamp); + } } } + distY = distY + (distY < 0 ? clampY : -clampY); + distX = distX + (distX < 0 ? clampX : -clampX); + + function xor(a: boolean, b: boolean) { + return a ? !b : b; + } + + if (keepAspectRatio) { + if (isDiagonal) { + if (newWidth > newHeight * aspectRatio) { + distY = (xor(affectsX, affectsY) ? -distX : distX) / aspectRatio; + } else { + distX = (xor(affectsX, affectsY) ? -distY : distY) * aspectRatio; + } + } else { + if (isHorizontal) { + if (affectsX) { + distY = distX / aspectRatio; + affectsY = true; + } else { + distY = distX / aspectRatio; + } + } else { + if (affectsY) { + distX = distY * aspectRatio; + affectsX = true; + } else { + distX = distY * aspectRatio; + } + } + } + } + + let width = startWidth + (affectsX ? -distX : distX); + let height = startHeight + (affectsY ? -distY : distY); + + let x = affectsX ? startX + distX : startX; + let y = affectsY ? startY + distY : startY; + return { width, height, + x, + y, }; } @@ -146,10 +239,20 @@ export function getPositionAfterResize( startValues: StartValues, controlDirection: ReturnType, width: number, - height: number + height: number, + extent?: CoordinateExtent ) { - return { - x: controlDirection.affectsX ? startValues.x - (width - startValues.width) : startValues.x, - y: controlDirection.affectsY ? startValues.y - (height - startValues.height) : startValues.y, - }; + let x = controlDirection.affectsX ? startValues.x - (width - startValues.width) : startValues.x; + let y = controlDirection.affectsY ? startValues.y - (height - startValues.height) : startValues.y; + let clampedX = 0; + let clampedY = 0; + + if (extent) { + clampedX = Math.max(Math.max(0, x - extent[1][0]), Math.max(0, extent[0][0] - x)); + clampedY = Math.max(Math.max(0, y - extent[1][1]), Math.max(0, extent[0][1] - y)); + x = x + clampedX; + y = y + clampedY; + } + + return { x, y, clampedX, clampedY }; } From ddda4615c69308672e03b9d4793f10ff2fdeeb6d Mon Sep 17 00:00:00 2001 From: Jeff Jenner Date: Sat, 27 Jan 2024 16:01:40 +1030 Subject: [PATCH 12/38] Propagate on:panecontextmenu event to SvelteFlow component so right click cb works --- .../svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte index 7f3baf36..6a80ef62 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte @@ -215,7 +215,12 @@ panOnScroll={panOnScroll === undefined ? false : panOnScroll} panOnDrag={panOnDrag === undefined ? true : panOnDrag} > - + Date: Mon, 29 Jan 2024 11:47:05 +0100 Subject: [PATCH 13/38] children with parent=extent and expandParent restrict the resizing of the parent --- packages/system/src/xyresizer/XYResizer.ts | 110 ++++++++++++--------- packages/system/src/xyresizer/utils.ts | 84 +++++++--------- 2 files changed, 102 insertions(+), 92 deletions(-) diff --git a/packages/system/src/xyresizer/XYResizer.ts b/packages/system/src/xyresizer/XYResizer.ts index ae81b1a0..43b75066 100644 --- a/packages/system/src/xyresizer/XYResizer.ts +++ b/packages/system/src/xyresizer/XYResizer.ts @@ -1,7 +1,7 @@ import { drag } from 'd3-drag'; import { select } from 'd3-selection'; -import { getControlDirection, getDimensionsAfterResize, getPositionAfterResize, getResizeDirection } from './utils'; +import { getControlDirection, getDimensionsAfterResize, getResizeDirection } from './utils'; import { getPointerPosition } from '../utils'; import type { CoordinateExtent, NodeBase, NodeLookup, Transform } from '../types'; import type { OnResize, OnResizeEnd, OnResizeStart, ResizeDragEvent, ShouldResize, ControlPosition } from './types'; @@ -77,6 +77,16 @@ function nodeToParentExtent(node: NodeBase): CoordinateExtent { ]; } +function nodeToChildExtent(child: NodeBase, parent: NodeBase): CoordinateExtent { + return [ + [parent.position.x + child.position.x, parent.position.y + child.position.y], + [ + parent.position.x + child.position.x + child.computed!.width!, + parent.position.y + child.position.y + child.computed!.height!, + ], + ]; +} + export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResizerParams): XYResizerInstance { const selection = select(domNode); @@ -94,44 +104,71 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize const controlDirection = getControlDirection(controlPosition); + let node: NodeBase | undefined = undefined; let childNodes: XYResizerChildChange[] = []; + let parentExtent: CoordinateExtent | undefined = undefined; + let childExtent: CoordinateExtent | undefined = undefined; const dragHandler = drag() .on('start', (event: ResizeDragEvent) => { const { nodeLookup, transform, snapGrid, snapToGrid } = getStoreItems(); - const node = nodeLookup.get(nodeId); - const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); + node = nodeLookup.get(nodeId); - prevValues = { - width: node?.computed?.width ?? 0, - height: node?.computed?.height ?? 0, - x: node?.position.x ?? 0, - y: node?.position.y ?? 0, - }; + if (node) { + const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); - startValues = { - ...prevValues, - pointerX: xSnapped, - pointerY: ySnapped, - aspectRatio: prevValues.width / prevValues.height, - }; + prevValues = { + width: node.computed?.width ?? 0, + height: node.computed?.height ?? 0, + x: node.position.x ?? 0, + y: node.position.y ?? 0, + }; - childNodes = []; - nodeLookup.forEach((_node, _nodeId) => { - if (_node.parentNode === nodeId) { - childNodes.push({ - id: _nodeId, - position: { ..._node.position }, - extent: _node.extent, - }); + startValues = { + ...prevValues, + pointerX: xSnapped, + pointerY: ySnapped, + aspectRatio: prevValues.width / prevValues.height, + }; + + if (node.extent === 'parent') { + const parentNode = nodeLookup.get(node.parentNode!); + if (parentNode) { + parentExtent = nodeToParentExtent(parentNode); + } } - }); - onResizeStart?.(event, { ...prevValues }); + + // Collect all child nodes to correct their relative positions when top/left changes + // Determine largest minimal extent the parent node is allowed to resize to + childNodes = []; + childExtent = undefined; + nodeLookup.forEach((child, childId) => { + if (child.parentNode === nodeId) { + childNodes.push({ + id: childId, + position: { ...child.position }, + extent: child.extent, + }); + if (child.extent === 'parent' || child.expandParent) { + const extent = nodeToChildExtent(child, node!); + if (childExtent) { + childExtent = [ + [Math.min(extent[0][0], childExtent[0][0]), Math.min(extent[0][1], childExtent[0][1])], + [Math.max(extent[1][0], childExtent[1][0]), Math.max(extent[1][1], childExtent[1][1])], + ]; + } else { + childExtent = extent; + } + } + } + }); + + onResizeStart?.(event, { ...prevValues }); + } }) .on('drag', (event: ResizeDragEvent) => { - const { nodeLookup, transform, snapGrid, snapToGrid } = getStoreItems(); + const { transform, snapGrid, snapToGrid } = getStoreItems(); const pointerPosition = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); - const node = nodeLookup.get(nodeId); let childChanges: XYResizerChildChange[] = []; @@ -140,32 +177,20 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize const { x: prevX, y: prevY, width: prevWidth, height: prevHeight } = prevValues; - let extent = undefined; - if (node.extent === 'parent') { - const parentNode = nodeLookup.get(node.parentNode!); - if (parentNode) { - extent = nodeToParentExtent(parentNode); - } - } - const { width, height, x, y } = getDimensionsAfterResize( startValues, controlDirection, pointerPosition, boundaries, keepAspectRatio, - extent + parentExtent, + childExtent ); const isWidthChange = width !== prevWidth; const isHeightChange = height !== prevHeight; if (controlDirection.affectsX || controlDirection.affectsY) { - // const { - // x, - // y, - // } = getPositionAfterResize(startValues, controlDirection, width, height, extent); - // only transform the node if the width or height changes const isXPosChange = x !== prevX && isWidthChange; const isYPosChange = y !== prevY && isHeightChange; @@ -193,9 +218,6 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize } if (isWidthChange || isHeightChange) { - if (extent) { - } - // console.log(clampedX, clampedY); change.isWidthChange = isWidthChange; change.isHeightChange = isHeightChange; change.width = width; diff --git a/packages/system/src/xyresizer/utils.ts b/packages/system/src/xyresizer/utils.ts index ab54163a..f284c4e7 100644 --- a/packages/system/src/xyresizer/utils.ts +++ b/packages/system/src/xyresizer/utils.ts @@ -88,8 +88,12 @@ function getSizeClamp(size: number, minSize: number, maxSize: number) { return Math.max(0, minSize - size, size - maxSize); } +function xor(a: boolean, b: boolean) { + return a ? !b : b; +} + /** - * Calculates new width & height of node after resize based on pointer position + * Calculates new width & height and x & y of node after resize based on pointer position * @param startValues - starting values of resize * @param controlDirection - dimensions affected by the resize * @param pointerPosition - the current pointer position corrected for snapping @@ -103,7 +107,8 @@ export function getDimensionsAfterResize( pointerPosition: ReturnType, boundaries: { minWidth: number; maxWidth: number; minHeight: number; maxHeight: number }, keepAspectRatio: boolean, - extent?: CoordinateExtent + extent?: CoordinateExtent, + childExtent?: CoordinateExtent ) { let { affectsX, affectsY } = controlDirection; const { isHorizontal, isVertical } = controlDirection; @@ -143,6 +148,26 @@ export function getDimensionsAfterResize( clampY = Math.max(clampY, yExtentClamp); } + // Check if the child extent is restricting the resize + if (childExtent) { + let xExtentClamp = 0; + let yExtentClamp = 0; + if (affectsX && distX > 0) { + xExtentClamp = getUpperExtentClamp(startX + distX, childExtent[0][0]); + } else if (!affectsX && distX < 0) { + xExtentClamp = getLowerExtentClamp(startX + newWidth, childExtent[1][0]); + } + + if (affectsY && distY > 0) { + yExtentClamp = getUpperExtentClamp(startY + distY, childExtent[0][1]); + } else if (!affectsY && distY < 0) { + yExtentClamp = getLowerExtentClamp(startY + newHeight, childExtent[1][1]); + } + + clampX = Math.max(clampX, xExtentClamp); + clampY = Math.max(clampY, yExtentClamp); + } + // Check if the aspect ratio resizing of the other side is restricting the resize if (keepAspectRatio) { if (isHorizontal) { @@ -161,8 +186,11 @@ export function getDimensionsAfterResize( } clampX = Math.max(clampX, aspectExtentClamp); } + + // TODO: Check if the child extent is restricting the resize } + // Do the same thing for vertical resizing if (isVertical) { const aspectWidthClamp = getSizeClamp(newHeight * aspectRatio, minWidth, maxWidth) / aspectRatio; clampY = Math.max(clampY, aspectWidthClamp); @@ -177,16 +205,14 @@ export function getDimensionsAfterResize( } clampY = Math.max(clampY, aspectExtentClamp); } + + // TODO: Check if the child extent is restricting the resize } } distY = distY + (distY < 0 ? clampY : -clampY); distX = distX + (distX < 0 ? clampX : -clampX); - function xor(a: boolean, b: boolean) { - return a ? !b : b; - } - if (keepAspectRatio) { if (isDiagonal) { if (newWidth > newHeight * aspectRatio) { @@ -196,19 +222,11 @@ export function getDimensionsAfterResize( } } else { if (isHorizontal) { - if (affectsX) { - distY = distX / aspectRatio; - affectsY = true; - } else { - distY = distX / aspectRatio; - } + distY = distX / aspectRatio; + affectsY = affectsX; } else { - if (affectsY) { - distX = distY * aspectRatio; - affectsX = true; - } else { - distX = distY * aspectRatio; - } + distX = distY * aspectRatio; + affectsX = affectsY; } } } @@ -226,33 +244,3 @@ export function getDimensionsAfterResize( y, }; } - -/** - * Determines new x & y position of node after resize based on new width & height - * @param startValues - starting values of resize - * @param controlDirection - dimensions affected by the resize - * @param width - new width of node - * @param height - new height of node - * @returns x: new x position of node, y: new y position of node - */ -export function getPositionAfterResize( - startValues: StartValues, - controlDirection: ReturnType, - width: number, - height: number, - extent?: CoordinateExtent -) { - let x = controlDirection.affectsX ? startValues.x - (width - startValues.width) : startValues.x; - let y = controlDirection.affectsY ? startValues.y - (height - startValues.height) : startValues.y; - let clampedX = 0; - let clampedY = 0; - - if (extent) { - clampedX = Math.max(Math.max(0, x - extent[1][0]), Math.max(0, extent[0][0] - x)); - clampedY = Math.max(Math.max(0, y - extent[1][1]), Math.max(0, extent[0][1] - y)); - x = x + clampedX; - y = y + clampedY; - } - - return { x, y, clampedX, clampedY }; -} From 6e167bda16814761a8ab68b9cf6d678d8495fc24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A0=97=E5=98=89=E7=94=B7?= <574980606@qq.com> Date: Tue, 30 Jan 2024 11:56:55 +0800 Subject: [PATCH 14/38] type: remove repeated type --- packages/react/src/container/GraphView/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react/src/container/GraphView/index.tsx b/packages/react/src/container/GraphView/index.tsx index fd7ac277..b95fa610 100644 --- a/packages/react/src/container/GraphView/index.tsx +++ b/packages/react/src/container/GraphView/index.tsx @@ -27,7 +27,6 @@ export type GraphViewProps = Omit< | 'maxZoom' | 'defaultMarkerColor' | 'noDragClassName' - | 'noDragClassName' | 'noWheelClassName' | 'noPanClassName' | 'defaultViewport' From c1391f5b13dfca5b917d75c354a75f171b1d2138 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Tue, 30 Jan 2024 11:19:28 +0100 Subject: [PATCH 15/38] keepAspectRatio and children with parent=extent and expandParent restrict the resizing of the parent --- packages/system/src/xyresizer/utils.ts | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/system/src/xyresizer/utils.ts b/packages/system/src/xyresizer/utils.ts index f284c4e7..db37d76c 100644 --- a/packages/system/src/xyresizer/utils.ts +++ b/packages/system/src/xyresizer/utils.ts @@ -187,7 +187,17 @@ export function getDimensionsAfterResize( clampX = Math.max(clampX, aspectExtentClamp); } - // TODO: Check if the child extent is restricting the resize + // Check if the child extent is restricting the resize + if (childExtent) { + let aspectExtentClamp = 0; + if ((!affectsX && !affectsY) || (affectsX && !affectsY && isDiagonal)) { + aspectExtentClamp = getLowerExtentClamp(startY + newWidth / aspectRatio, childExtent[1][1]) * aspectRatio; + } else { + aspectExtentClamp = + getUpperExtentClamp(startY + (affectsX ? distX : -distX) / aspectRatio, childExtent[0][1]) * aspectRatio; + } + clampX = Math.max(clampX, aspectExtentClamp); + } } // Do the same thing for vertical resizing @@ -206,7 +216,16 @@ export function getDimensionsAfterResize( clampY = Math.max(clampY, aspectExtentClamp); } - // TODO: Check if the child extent is restricting the resize + if (childExtent) { + let aspectExtentClamp = 0; + if ((!affectsX && !affectsY) || (affectsY && !affectsX && isDiagonal)) { + aspectExtentClamp = getLowerExtentClamp(startX + newHeight * aspectRatio, childExtent[1][0]) / aspectRatio; + } else { + aspectExtentClamp = + getUpperExtentClamp(startX + (affectsY ? distY : -distY) * aspectRatio, childExtent[0][0]) / aspectRatio; + } + clampY = Math.max(clampY, aspectExtentClamp); + } } } From c1311c198a8154f6a38a4f72bbbd449cc803ee3c Mon Sep 17 00:00:00 2001 From: peterkogo Date: Tue, 30 Jan 2024 12:06:38 +0100 Subject: [PATCH 16/38] fixed resizing & expandParent --- packages/system/src/xyresizer/XYResizer.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/system/src/xyresizer/XYResizer.ts b/packages/system/src/xyresizer/XYResizer.ts index 43b75066..b2251834 100644 --- a/packages/system/src/xyresizer/XYResizer.ts +++ b/packages/system/src/xyresizer/XYResizer.ts @@ -106,6 +106,7 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize let node: NodeBase | undefined = undefined; let childNodes: XYResizerChildChange[] = []; + let parentNode: NodeBase | undefined = undefined; // Needed to fix expandParent let parentExtent: CoordinateExtent | undefined = undefined; let childExtent: CoordinateExtent | undefined = undefined; @@ -131,9 +132,10 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize aspectRatio: prevValues.width / prevValues.height, }; - if (node.extent === 'parent') { - const parentNode = nodeLookup.get(node.parentNode!); - if (parentNode) { + parentNode = undefined; + if (node.extent === 'parent' || node.expandParent) { + parentNode = nodeLookup.get(node.parentNode!); + if (parentNode && node.extent === 'parent') { parentExtent = nodeToParentExtent(parentNode); } } @@ -202,6 +204,19 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize prevValues.x = change.x; prevValues.y = change.y; + + // Fix expandParent when resizing from top/left + if (parentNode && node.expandParent) { + if (change.x < 0) { + prevValues.x = 0; + startValues.x = startValues.x - change.x; + } + + if (change.y < 0) { + prevValues.y = 0; + startValues.y = startValues.y - change.y; + } + } } if (childNodes.length > 0) { From 7fbb245ecc0fcab72c8aa6ecd6bd0a4bbb9d9796 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Tue, 30 Jan 2024 12:28:35 +0100 Subject: [PATCH 17/38] added a comment --- packages/system/src/xyresizer/utils.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/system/src/xyresizer/utils.ts b/packages/system/src/xyresizer/utils.ts index db37d76c..6ed0cf34 100644 --- a/packages/system/src/xyresizer/utils.ts +++ b/packages/system/src/xyresizer/utils.ts @@ -94,6 +94,14 @@ function xor(a: boolean, b: boolean) { /** * Calculates new width & height and x & y of node after resize based on pointer position + * @description - Buckle up, this is a chunky one! If you want to determine the new dimensions of a node after a resize, + * you have to account for all possible restrictions: min/max width/height of the node, the maximum extent the node is allowed + * to move in (in this case: resize into) determined by the parent node, the minimal extent determined by child nodes + * with expandParent or extent: 'parent' set and oh yeah, these things also have to work with keepAspectRatio! + * The way this is done is by determining how much each of these restricting actually restricts the resize and then applying the + * strongest restriction. Because the resize affects x, y and width, height and width, height of a opposing side with keepAspectRatio, + * the resize amount is always kept in distX & distY amount (the distance in mouse movement) + * Instead of clamping each value, we first calculate the biggest 'clamp' (for the lack of a better name) and then apply it to all values. * @param startValues - starting values of resize * @param controlDirection - dimensions affected by the resize * @param pointerPosition - the current pointer position corrected for snapping From d5e21b00017c6059cf57dccfad5225d3f7bd34fe Mon Sep 17 00:00:00 2001 From: peterkogo Date: Tue, 30 Jan 2024 12:32:02 +0100 Subject: [PATCH 18/38] improved nodeResizer example --- .../react/src/examples/NodeResizer/index.tsx | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/examples/react/src/examples/NodeResizer/index.tsx b/examples/react/src/examples/NodeResizer/index.tsx index f8e46940..42e83523 100644 --- a/examples/react/src/examples/NodeResizer/index.tsx +++ b/examples/react/src/examples/NodeResizer/index.tsx @@ -119,24 +119,37 @@ const initialNodes: Node[] = [ { id: '5', type: 'defaultResizer', - data: { label: 'Parent' }, + data: { label: 'Parent', keepAspectRatio: true }, position: { x: 700, y: 0 }, - style: { ...nodeStyle, width: 300, height: 300 }, + style: { ...nodeStyle, width: 300, height: 400 }, }, { id: '5a', type: 'defaultResizer', - data: { label: 'Child' }, + data: { + label: 'Child with extent: parent', + }, position: { x: 50, y: 50 }, parentNode: '5', - style: { ...nodeStyle }, + extent: 'parent', + style: { ...nodeStyle, width: 50, height: 100 }, }, { id: '5b', type: 'defaultResizer', - data: { label: 'Child' }, - position: { x: 100, y: 100 }, + data: { label: 'Child with expandParent' }, + position: { x: 150, y: 100 }, parentNode: '5', + expandParent: true, + style: { ...nodeStyle }, + }, + { + id: '5c', + type: 'defaultResizer', + data: { label: 'Child with expandParent & keepAspectRatio', keepAspectRatio: true }, + position: { x: 25, y: 200 }, + parentNode: '5', + expandParent: true, style: { ...nodeStyle }, }, ]; From c9b1b78edf39278f33973eab6c68f45a4c730ec3 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Tue, 30 Jan 2024 13:25:02 +0100 Subject: [PATCH 19/38] use for..of instead of forEach --- .../NodeResizer/NodeResizeControl.tsx | 4 ++-- .../src/lib/plugins/NodeResizer/ResizeControl.svelte | 4 ++-- packages/system/src/xyresizer/XYResizer.ts | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx index c76642ed..c03c023e 100644 --- a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx +++ b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx @@ -90,14 +90,14 @@ function ResizeControl({ changes.push(dimensionChange); } - childChanges.forEach((childChange) => { + for (const childChange of childChanges) { const positionChange: NodePositionChange = { ...childChange, type: 'position', }; changes.push(positionChange); - }); + } triggerNodeChanges(changes); }, diff --git a/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte b/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte index 3ec0ad71..56caa8b5 100644 --- a/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte +++ b/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte @@ -77,12 +77,12 @@ ? { x: change.x, y: change.y } : node.position; - childChanges.forEach((childChange) => { + for (const childChange of childChanges) { const childNode = $nodeLookup.get(childChange.id); if (childNode) { childNode.position = childChange.position; } - }); + } $nodes = $nodes; } diff --git a/packages/system/src/xyresizer/XYResizer.ts b/packages/system/src/xyresizer/XYResizer.ts index b2251834..5fe6af91 100644 --- a/packages/system/src/xyresizer/XYResizer.ts +++ b/packages/system/src/xyresizer/XYResizer.ts @@ -144,7 +144,7 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize // Determine largest minimal extent the parent node is allowed to resize to childNodes = []; childExtent = undefined; - nodeLookup.forEach((child, childId) => { + for (const [childId, child] of nodeLookup) { if (child.parentNode === nodeId) { childNodes.push({ id: childId, @@ -163,7 +163,7 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize } } } - }); + } onResizeStart?.(event, { ...prevValues }); } @@ -222,13 +222,13 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize if (childNodes.length > 0) { const xChange = x - prevX; const yChange = y - prevY; - childNodes.forEach((childNode) => { + for (const childNode of childNodes) { childNode.position = { x: childNode.position.x - xChange, y: childNode.position.y - yChange, }; childChanges.push(childNode); - }); + } } } From 7830b3573fc093150692a7c0cb7e9fdcae74bcca Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 5 Feb 2024 16:58:19 +0100 Subject: [PATCH 20/38] chore(resizer): cleanup --- .../react/src/hooks/useMoveSelectedNodes.ts | 2 +- packages/system/src/xyresizer/XYResizer.ts | 13 +++++------ packages/system/src/xyresizer/utils.ts | 22 +++++++------------ 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/packages/react/src/hooks/useMoveSelectedNodes.ts b/packages/react/src/hooks/useMoveSelectedNodes.ts index 4c2ef93b..c627628a 100644 --- a/packages/react/src/hooks/useMoveSelectedNodes.ts +++ b/packages/react/src/hooks/useMoveSelectedNodes.ts @@ -8,7 +8,7 @@ const selectedAndDraggable = (nodesDraggable: boolean) => (n: Node) => n.selected && (n.draggable || (nodesDraggable && typeof n.draggable === 'undefined')); /** - * Hook for updating node positions with keyboard presses + * Hook for updating node positions by passing a direction and factor * * @internal * @returns function for updating node positions diff --git a/packages/system/src/xyresizer/XYResizer.ts b/packages/system/src/xyresizer/XYResizer.ts index 5fe6af91..c02ca47c 100644 --- a/packages/system/src/xyresizer/XYResizer.ts +++ b/packages/system/src/xyresizer/XYResizer.ts @@ -3,7 +3,7 @@ import { select } from 'd3-selection'; import { getControlDirection, getDimensionsAfterResize, getResizeDirection } from './utils'; import { getPointerPosition } from '../utils'; -import type { CoordinateExtent, NodeBase, NodeLookup, Transform } from '../types'; +import type { CoordinateExtent, NodeBase, NodeLookup, Transform, XYPosition } from '../types'; import type { OnResize, OnResizeEnd, OnResizeStart, ResizeDragEvent, ShouldResize, ControlPosition } from './types'; const initPrevValues = { width: 0, height: 0, x: 0, y: 0 }; @@ -30,10 +30,7 @@ export type XYResizerChange = typeof initChange; export type XYResizerChildChange = { id: string; - position: { - x: number; - y: number; - }; + position: XYPosition; extent?: 'parent' | CoordinateExtent; }; @@ -144,6 +141,7 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize // Determine largest minimal extent the parent node is allowed to resize to childNodes = []; childExtent = undefined; + for (const [childId, child] of nodeLookup) { if (child.parentNode === nodeId) { childNodes.push({ @@ -151,8 +149,10 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize position: { ...child.position }, extent: child.extent, }); + if (child.extent === 'parent' || child.expandParent) { const extent = nodeToChildExtent(child, node!); + if (childExtent) { childExtent = [ [Math.min(extent[0][0], childExtent[0][0]), Math.min(extent[0][1], childExtent[0][1])], @@ -171,8 +171,7 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize .on('drag', (event: ResizeDragEvent) => { const { transform, snapGrid, snapToGrid } = getStoreItems(); const pointerPosition = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); - - let childChanges: XYResizerChildChange[] = []; + const childChanges: XYResizerChildChange[] = []; if (node) { const change = { ...initChange }; diff --git a/packages/system/src/xyresizer/utils.ts b/packages/system/src/xyresizer/utils.ts index 6ed0cf34..a651df3f 100644 --- a/packages/system/src/xyresizer/utils.ts +++ b/packages/system/src/xyresizer/utils.ts @@ -1,5 +1,5 @@ import { CoordinateExtent } from '../types'; -import { clamp, getPointerPosition } from '../utils'; +import { getPointerPosition } from '../utils'; import { ControlPosition } from './types'; type GetResizeDirectionParams = { @@ -107,7 +107,7 @@ function xor(a: boolean, b: boolean) { * @param pointerPosition - the current pointer position corrected for snapping * @param boundaries - minimum and maximum dimensions of the node * @param keepAspectRatio - prevent changes of asprect ratio - * @returns width: new width of node, height: new height of node + * @returns x, y, width and height of the node after resize */ export function getDimensionsAfterResize( startValues: StartValues, @@ -129,8 +129,8 @@ export function getDimensionsAfterResize( let distX = Math.floor(isHorizontal ? xSnapped - startValues.pointerX : 0); let distY = Math.floor(isVertical ? ySnapped - startValues.pointerY : 0); - let newWidth = startWidth + (affectsX ? -distX : distX); - let newHeight = startHeight + (affectsY ? -distY : distY); + const newWidth = startWidth + (affectsX ? -distX : distX); + const newHeight = startHeight + (affectsY ? -distY : distY); // Check if maxWidth, minWWidth, maxHeight, minHeight are restricting the resize let clampX = getSizeClamp(newWidth, minWidth, maxWidth); @@ -258,16 +258,10 @@ export function getDimensionsAfterResize( } } - let width = startWidth + (affectsX ? -distX : distX); - let height = startHeight + (affectsY ? -distY : distY); - - let x = affectsX ? startX + distX : startX; - let y = affectsY ? startY + distY : startY; - return { - width, - height, - x, - y, + width: startWidth + (affectsX ? -distX : distX), + height: startHeight + (affectsY ? -distY : distY), + x: affectsX ? startX + distX : startX, + y: affectsY ? startY + distY : startY, }; } From 2581832f2731f5512cfec858f6b47167e395d537 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 5 Feb 2024 17:01:18 +0100 Subject: [PATCH 21/38] chore(libs): update changelog --- packages/react/CHANGELOG.md | 4 ++++ packages/svelte/CHANGELOG.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 6931ea5b..54c5f9ec 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -1,5 +1,9 @@ # @xyflow/react +## 12.0.0-next.9 + +- a better NodeResizer that works with subflows. Child nodes do not move when parent node gets resized and parent extent is taken into account + ## 12.0.0-next.8 ### Patch changes diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index b534df65..9d6cac3d 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,9 @@ # @xyflow/svelte +## 0.0.36 + +- a better NodeResizer (child nodes do not move when parent node gets resized) + ## 0.0.35 ## Minor changes From 663104692342a5b3271154b5731a6099a3b889e6 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 5 Feb 2024 17:12:07 +0100 Subject: [PATCH 22/38] chore(svelte): update changelog --- packages/svelte/CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 9d6cac3d..11724a56 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -2,7 +2,10 @@ ## 0.0.36 -- a better NodeResizer (child nodes do not move when parent node gets resized) +## Minor changes + +- a better NodeResizer (child nodes do not move when parent node gets resized) +- fix `on:panecontextmenu` ## 0.0.35 From 1aaed8458fcd46ec2a5a00a71687d21e7e0bd867 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 5 Feb 2024 17:23:36 +0100 Subject: [PATCH 23/38] chore(svelte): add role to EdgeLabel to prevent a11y warnings closes #3841 --- packages/svelte/CHANGELOG.md | 1 + packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 11724a56..123295bd 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -6,6 +6,7 @@ - a better NodeResizer (child nodes do not move when parent node gets resized) - fix `on:panecontextmenu` +- add `role="button"` to `` to prevent a11y warnings ## 0.0.35 diff --git a/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte b/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte index 81039d0d..bc12b604 100644 --- a/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte +++ b/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte @@ -19,6 +19,7 @@ class="svelte-flow__edge-label" style:transform="translate(-50%, -50%) translate({x}px,{y}px)" style={'pointer-events: all;' + style} + role="button" on:click={() => { if (id) handleEdgeSelect(id); }} From 4c0a5a62c7b51b38237a61b1c64dfd3b7ef5c294 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Tue, 6 Feb 2024 13:19:54 +0100 Subject: [PATCH 24/38] fixed minimap styling by props & mask-stroke-width --- .../src/additional-components/MiniMap/MiniMap.tsx | 7 +++++-- .../react/src/additional-components/MiniMap/types.ts | 2 ++ .../svelte/src/lib/plugins/Minimap/Minimap.svelte | 12 ++++++------ packages/system/src/styles/init.css | 9 ++++++++- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/react/src/additional-components/MiniMap/MiniMap.tsx b/packages/react/src/additional-components/MiniMap/MiniMap.tsx index cd3f64ae..80410922 100644 --- a/packages/react/src/additional-components/MiniMap/MiniMap.tsx +++ b/packages/react/src/additional-components/MiniMap/MiniMap.tsx @@ -49,6 +49,7 @@ function MiniMapComponent({ // We need to rename the prop to be `CapitalCase` so that JSX will render it as // a component properly. nodeComponent, + bgColor, maskColor, maskStrokeColor = 'none', maskStrokeWidth = 1, @@ -130,7 +131,8 @@ function MiniMapComponent({ style={ { ...style, - '--xy-minimap-mask-color-props': typeof maskColor === 'string' ? maskColor : undefined, + '--xy-minimap-background-color-props': typeof bgColor === 'string' ? bgColor : undefined, + '--xy-minimap-mask-background-color-props': typeof maskColor === 'string' ? maskColor : undefined, '--xy-minimap-node-background-color-props': typeof nodeColor === 'string' ? nodeColor : undefined, '--xy-minimap-node-stroke-color-props': typeof nodeStrokeColor === 'string' ? nodeStrokeColor : undefined, '--xy-minimap-node-stroke-width-props': typeof nodeStrokeWidth === 'string' ? nodeStrokeWidth : undefined, @@ -143,6 +145,7 @@ function MiniMapComponent({ width={elementWidth} height={elementHeight} viewBox={`${x} ${y} ${width} ${height}`} + className="react-flow__minimap-svg" role="img" aria-labelledby={labelledBy} ref={svg} @@ -164,7 +167,7 @@ function MiniMapComponent({ M${viewBB.x},${viewBB.y}h${viewBB.width}v${viewBB.height}h${-viewBB.width}z`} fillRule="evenodd" stroke={maskStrokeColor} - strokeWidth={maskStrokeWidth} + strokeWidth={maskStrokeWidth * viewScale} pointerEvents="none" /> diff --git a/packages/react/src/additional-components/MiniMap/types.ts b/packages/react/src/additional-components/MiniMap/types.ts index 31e67428..4bfd1a51 100644 --- a/packages/react/src/additional-components/MiniMap/types.ts +++ b/packages/react/src/additional-components/MiniMap/types.ts @@ -19,6 +19,8 @@ export type MiniMapProps = Omit; + /** Background color of minimap */ + bgColor?: string; /** Color of mask representing viewport */ maskColor?: string; /** Stroke color of mask representing viewport */ diff --git a/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte b/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte index 7b2896d8..dfe3d086 100644 --- a/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte +++ b/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte @@ -27,7 +27,7 @@ export let bgColor: $$Props['bgColor'] = undefined; export let maskColor: $$Props['maskColor'] = undefined; export let maskStrokeColor: $$Props['maskStrokeColor'] = undefined; - export let maskStrokeWidth: $$Props['maskStrokeWidth'] = undefined; + export let maskStrokeWidth: $$Props['maskStrokeWidth'] = 1; export let width: $$Props['width'] = undefined; export let height: $$Props['height'] = undefined; export let pannable: $$Props['pannable'] = true; @@ -83,7 +83,7 @@ @@ -92,12 +92,10 @@ width={elementWidth} height={elementHeight} viewBox="{x} {y} {viewboxWidth} {viewboxHeight}" + class="svelte-flow__minimap-svg" role="img" aria-labelledby={labelledBy} - style:--xy-minimap-background-color-props={bgColor} - style:--xy-minimap-mask-color-props={maskColor} - style:--xy-minimap-mask-stroke-color-props={maskStrokeColor} - style:--xy-minimap-mask-stroke-width-props={maskStrokeWidth} + style:--xy-minimap-mask-background-color-props={maskColor} use:interactive={{ panZoom: $panZoom, viewport, @@ -137,6 +135,8 @@ offset * 2}h{-viewboxWidth - offset * 2}z M{viewBB.x},{viewBB.y}h{viewBB.width}v{viewBB.height}h{-viewBB.width}z" fill-rule="evenodd" + stroke={maskStrokeColor} + stroke-width={maskStrokeWidth ?? 1 * viewScale} pointer-events="none" /> diff --git a/packages/system/src/styles/init.css b/packages/system/src/styles/init.css index 14519c74..77352a74 100644 --- a/packages/system/src/styles/init.css +++ b/packages/system/src/styles/init.css @@ -310,7 +310,14 @@ svg.xy-flow__connectionline { } .xy-flow__minimap { - background: var(--xy-minimap-background-color, var(--xy-minimap-background-color-default)); + background: var( + --xy-minimap-background-color, + var(--xy-minimap-background-color-props, var(--xy-minimap-background-color-default)) + ); + + &-svg { + display: block; + } &-mask { fill: var( From ceb160fa7be405544b6458606064e61a923b9b7a Mon Sep 17 00:00:00 2001 From: peterkogo Date: Wed, 7 Feb 2024 12:19:23 +0100 Subject: [PATCH 25/38] added css variables for minimap mask, set default mask-stroke to 0 --- .../additional-components/MiniMap/MiniMap.tsx | 9 +++++---- .../src/lib/plugins/Minimap/Minimap.svelte | 8 ++++---- packages/system/src/styles/init.css | 16 ++++++++++++++-- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/react/src/additional-components/MiniMap/MiniMap.tsx b/packages/react/src/additional-components/MiniMap/MiniMap.tsx index 80410922..f841c8b6 100644 --- a/packages/react/src/additional-components/MiniMap/MiniMap.tsx +++ b/packages/react/src/additional-components/MiniMap/MiniMap.tsx @@ -51,8 +51,8 @@ function MiniMapComponent({ nodeComponent, bgColor, maskColor, - maskStrokeColor = 'none', - maskStrokeWidth = 1, + maskStrokeColor, + maskStrokeWidth, position = 'bottom-right', onClick, onNodeClick, @@ -133,6 +133,9 @@ function MiniMapComponent({ ...style, '--xy-minimap-background-color-props': typeof bgColor === 'string' ? bgColor : undefined, '--xy-minimap-mask-background-color-props': typeof maskColor === 'string' ? maskColor : undefined, + '--xy-minimap-mask-stroke-color-props': typeof maskStrokeColor === 'string' ? maskStrokeColor : undefined, + '--xy-minimap-mask-stroke-width-props': + typeof maskStrokeWidth === 'number' ? maskStrokeWidth * viewScale : undefined, '--xy-minimap-node-background-color-props': typeof nodeColor === 'string' ? nodeColor : undefined, '--xy-minimap-node-stroke-color-props': typeof nodeStrokeColor === 'string' ? nodeStrokeColor : undefined, '--xy-minimap-node-stroke-width-props': typeof nodeStrokeWidth === 'string' ? nodeStrokeWidth : undefined, @@ -166,8 +169,6 @@ function MiniMapComponent({ d={`M${x - offset},${y - offset}h${width + offset * 2}v${height + offset * 2}h${-width - offset * 2}z M${viewBB.x},${viewBB.y}h${viewBB.width}v${viewBB.height}h${-viewBB.width}z`} fillRule="evenodd" - stroke={maskStrokeColor} - strokeWidth={maskStrokeWidth * viewScale} pointerEvents="none" /> diff --git a/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte b/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte index dfe3d086..29ccd8e7 100644 --- a/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte +++ b/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte @@ -27,7 +27,7 @@ export let bgColor: $$Props['bgColor'] = undefined; export let maskColor: $$Props['maskColor'] = undefined; export let maskStrokeColor: $$Props['maskStrokeColor'] = undefined; - export let maskStrokeWidth: $$Props['maskStrokeWidth'] = 1; + export let maskStrokeWidth: $$Props['maskStrokeWidth'] = undefined; export let width: $$Props['width'] = undefined; export let height: $$Props['height'] = undefined; export let pannable: $$Props['pannable'] = true; @@ -83,7 +83,7 @@ @@ -96,6 +96,8 @@ role="img" aria-labelledby={labelledBy} style:--xy-minimap-mask-background-color-props={maskColor} + style:--xy-minimap-mask-stroke-color-props={maskStrokeColor} + style:--xy-minimap-mask-stroke-width-props={(maskStrokeWidth ?? 0) * viewScale} use:interactive={{ panZoom: $panZoom, viewport, @@ -135,8 +137,6 @@ offset * 2}h{-viewboxWidth - offset * 2}z M{viewBB.x},{viewBB.y}h{viewBB.width}v{viewBB.height}h{-viewBB.width}z" fill-rule="evenodd" - stroke={maskStrokeColor} - stroke-width={maskStrokeWidth ?? 1 * viewScale} pointer-events="none" /> diff --git a/packages/system/src/styles/init.css b/packages/system/src/styles/init.css index 77352a74..57426b1b 100644 --- a/packages/system/src/styles/init.css +++ b/packages/system/src/styles/init.css @@ -12,6 +12,8 @@ --xy-minimap-background-color-default: #fff; --xy-minimap-mask-background-color-default: rgb(240, 240, 240, 0.6); + --xy-minimap-mask-stroke-color-default: transparent; + --xy-minimap-mask-stroke-width-default: 0; --xy-minimap-node-background-color-default: #e2e2e2; --xy-minimap-node-stroke-color-default: transparent; --xy-minimap-node-stroke-width-default: 2; @@ -34,6 +36,8 @@ --xy-minimap-background-color-default: #141414; --xy-minimap-mask-background-color-default: rgb(60, 60, 60, 0.6); + --xy-minimap-mask-stroke-color-default: transparent; + --xy-minimap-mask-stroke-width-default: 0; --xy-minimap-node-background-color-default: #2b2b2b; --xy-minimap-node-stroke-color-default: transparent; --xy-minimap-node-stroke-width-default: 2; @@ -311,8 +315,8 @@ svg.xy-flow__connectionline { .xy-flow__minimap { background: var( - --xy-minimap-background-color, - var(--xy-minimap-background-color-props, var(--xy-minimap-background-color-default)) + --xy-minimap-background-color-props, + var(--xy-minimap-background-color, var(--xy-minimap-background-color-default)) ); &-svg { @@ -324,6 +328,14 @@ svg.xy-flow__connectionline { --xy-minimap-mask-background-color-props, var(--xy-minimap-mask-background-color, var(--xy-minimap-mask-background-color-default)) ); + stroke: var( + --xy-minimap-mask-stroke-color-props, + var(--xy-minimap-mask-stroke-color, var(--xy-minimap-mask-stroke-color-default)) + ); + stroke-width: var( + --xy-minimap-mask-stroke-width-props, + var(--xy-minimap-mask-stroke-width, var(--xy-minimap-mask-stroke-width-default)) + ); } &-node { From 1d87926724753e2718d0d04c12d886925a87d7d2 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Wed, 7 Feb 2024 13:07:39 +0100 Subject: [PATCH 26/38] changed maskStrokeWidth default back to 1 --- packages/svelte/src/lib/plugins/Minimap/Minimap.svelte | 4 +++- packages/system/src/styles/init.css | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte b/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte index 29ccd8e7..031235b6 100644 --- a/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte +++ b/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte @@ -97,7 +97,9 @@ aria-labelledby={labelledBy} style:--xy-minimap-mask-background-color-props={maskColor} style:--xy-minimap-mask-stroke-color-props={maskStrokeColor} - style:--xy-minimap-mask-stroke-width-props={(maskStrokeWidth ?? 0) * viewScale} + style:--xy-minimap-mask-stroke-width-props={maskStrokeWidth + ? maskStrokeWidth * viewScale + : undefined} use:interactive={{ panZoom: $panZoom, viewport, diff --git a/packages/system/src/styles/init.css b/packages/system/src/styles/init.css index 57426b1b..270a6ab2 100644 --- a/packages/system/src/styles/init.css +++ b/packages/system/src/styles/init.css @@ -13,7 +13,7 @@ --xy-minimap-background-color-default: #fff; --xy-minimap-mask-background-color-default: rgb(240, 240, 240, 0.6); --xy-minimap-mask-stroke-color-default: transparent; - --xy-minimap-mask-stroke-width-default: 0; + --xy-minimap-mask-stroke-width-default: 1; --xy-minimap-node-background-color-default: #e2e2e2; --xy-minimap-node-stroke-color-default: transparent; --xy-minimap-node-stroke-width-default: 2; @@ -37,7 +37,7 @@ --xy-minimap-background-color-default: #141414; --xy-minimap-mask-background-color-default: rgb(60, 60, 60, 0.6); --xy-minimap-mask-stroke-color-default: transparent; - --xy-minimap-mask-stroke-width-default: 0; + --xy-minimap-mask-stroke-width-default: 1; --xy-minimap-node-background-color-default: #2b2b2b; --xy-minimap-node-stroke-color-default: transparent; --xy-minimap-node-stroke-width-default: 2; From 6b18c9daeb6e095a4c0d8b0827fbea07de8e3f78 Mon Sep 17 00:00:00 2001 From: Moritz Klack Date: Wed, 7 Feb 2024 17:09:39 +0100 Subject: [PATCH 27/38] refactor defaultNodes/defaultEdges handling (#3882) * refactor(react): cleanup defaultnodes/defaultedges handling * chore(examples): cleanup * chore(examples): cleanup * fix(react): selection for uncontrolled flows --- .../react/src/examples/UseNodesData/index.tsx | 1 - .../components/ReactFlowProvider/index.tsx | 6 + .../src/components/StoreUpdater/index.tsx | 20 +-- .../react/src/container/ReactFlow/Wrapper.tsx | 6 + .../react/src/container/ReactFlow/index.tsx | 12 +- packages/react/src/store/index.ts | 142 ++++++------------ packages/react/src/store/initialState.ts | 20 ++- packages/react/src/store/utils.ts | 44 ------ packages/react/src/types/store.ts | 5 +- packages/react/src/utils/changes.ts | 12 +- 10 files changed, 96 insertions(+), 172 deletions(-) delete mode 100644 packages/react/src/store/utils.ts diff --git a/examples/react/src/examples/UseNodesData/index.tsx b/examples/react/src/examples/UseNodesData/index.tsx index d99ddb43..cbeb2478 100644 --- a/examples/react/src/examples/UseNodesData/index.tsx +++ b/examples/react/src/examples/UseNodesData/index.tsx @@ -41,7 +41,6 @@ const initNodes: MyNode[] = [ data: {}, position: { x: 100, y: 0 }, }, - { id: '2', type: 'text', diff --git a/packages/react/src/components/ReactFlowProvider/index.tsx b/packages/react/src/components/ReactFlowProvider/index.tsx index 34999c6f..829a304d 100644 --- a/packages/react/src/components/ReactFlowProvider/index.tsx +++ b/packages/react/src/components/ReactFlowProvider/index.tsx @@ -10,6 +10,8 @@ export function ReactFlowProvider({ children, initialNodes, initialEdges, + defaultNodes, + defaultEdges, initialWidth, initialHeight, fitView, @@ -17,6 +19,8 @@ export function ReactFlowProvider({ children: ReactNode; initialNodes?: Node[]; initialEdges?: Edge[]; + defaultNodes?: Node[]; + defaultEdges?: Edge[]; initialWidth?: number; initialHeight?: number; fitView?: boolean; @@ -27,6 +31,8 @@ export function ReactFlowProvider({ storeRef.current = createRFStore({ nodes: initialNodes, edges: initialEdges, + defaultNodes, + defaultEdges, width: initialWidth, height: initialHeight, fitView, diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx index 3a12594c..98ad3bac 100644 --- a/packages/react/src/components/StoreUpdater/index.tsx +++ b/packages/react/src/components/StoreUpdater/index.tsx @@ -15,8 +15,6 @@ import { initNodeOrigin } from '../../container/ReactFlow'; const reactFlowFieldsToTrack = [ 'nodes', 'edges', - 'defaultNodes', - 'defaultEdges', 'onConnect', 'onConnectStart', 'onConnectEnd', @@ -76,7 +74,6 @@ const fieldsToTrack = [...reactFlowFieldsToTrack, 'rfId'] as const; const selector = (s: ReactFlowState) => ({ setNodes: s.setNodes, setEdges: s.setEdges, - setDefaultNodesAndEdges: s.setDefaultNodesAndEdges, setMinZoom: s.setMinZoom, setMaxZoom: s.setMaxZoom, setTranslateExtent: s.setTranslateExtent, @@ -85,22 +82,13 @@ const selector = (s: ReactFlowState) => ({ }); export function StoreUpdater(props: StoreUpdaterProps) { - const { - setNodes, - setEdges, - setDefaultNodesAndEdges, - setMinZoom, - setMaxZoom, - setTranslateExtent, - setNodeExtent, - reset, - } = useStore(selector, shallow); + const { setNodes, setEdges, setMinZoom, setMaxZoom, setTranslateExtent, setNodeExtent, reset } = useStore( + selector, + shallow + ); const store = useStoreApi(); useEffect(() => { - const edgesWithDefaults = props.defaultEdges?.map((e) => ({ ...e, ...props.defaultEdgeOptions })); - setDefaultNodesAndEdges(props.defaultNodes, edgesWithDefaults); - return () => { reset(); }; diff --git a/packages/react/src/container/ReactFlow/Wrapper.tsx b/packages/react/src/container/ReactFlow/Wrapper.tsx index 4d3cb7b7..b82ca249 100644 --- a/packages/react/src/container/ReactFlow/Wrapper.tsx +++ b/packages/react/src/container/ReactFlow/Wrapper.tsx @@ -8,6 +8,8 @@ export function Wrapper({ children, nodes, edges, + defaultNodes, + defaultEdges, width, height, fitView, @@ -15,6 +17,8 @@ export function Wrapper({ children: ReactNode; nodes?: Node[]; edges?: Edge[]; + defaultNodes?: Node[]; + defaultEdges?: Edge[]; width?: number; height?: number; fitView?: boolean; @@ -31,6 +35,8 @@ export function Wrapper({ ( data-testid="rf__wrapper" id={id} > - + ( createWithEqualityFn( (set, get) => ({ - ...getInitialState({ nodes, edges, width, height, fitView }), + ...getInitialState({ nodes, edges, width, height, fitView, defaultNodes, defaultEdges }), setNodes: (nodes: Node[]) => { const { nodeLookup, nodeOrigin, elevateNodesOnSelect } = get(); // setNodes() is called exclusively in response to user actions: @@ -49,7 +52,6 @@ const createRFStore = ({ // // When this happens, we take the note objects passed by the user and extend them with fields // relevant for internal React Flow operations. - // TODO: consider updating the types to reflect the distinction between user-provided nodes and internal nodes. const nodesWithInternalData = adoptUserProvidedNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect }); set({ nodes: nodesWithInternalData }); @@ -61,38 +63,6 @@ const createRFStore = ({ set({ edges }); }, - // when the user works with an uncontrolled flow, - // we set a flag `hasDefaultNodes` / `hasDefaultEdges` - setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => { - const hasDefaultNodes = typeof nodes !== 'undefined'; - const hasDefaultEdges = typeof edges !== 'undefined'; - - const nextState: { - nodes?: Node[]; - edges?: Edge[]; - hasDefaultNodes: boolean; - hasDefaultEdges: boolean; - } = { - hasDefaultNodes, - hasDefaultEdges, - }; - - if (hasDefaultNodes) { - const { nodeLookup, nodeOrigin, elevateNodesOnSelect } = get(); - nextState.nodes = adoptUserProvidedNodes(nodes, nodeLookup, { - nodeOrigin, - elevateNodesOnSelect, - }); - } - if (hasDefaultEdges) { - const { connectionLookup, edgeLookup } = get(); - updateConnectionLookup(connectionLookup, edgeLookup, edges); - - nextState.edges = edges; - } - - set(nextState); - }, // Every node gets registerd at a ResizeObserver. Whenever a node // changes its dimensions, this function is called to measure the // new dimensions and update the nodes. @@ -166,81 +136,67 @@ const createRFStore = ({ get().triggerNodeChanges(changes); }, - triggerNodeChanges: (changes) => { - const { onNodesChange, nodeLookup, nodes, hasDefaultNodes, nodeOrigin, elevateNodesOnSelect } = get(); + const { onNodesChange, setNodes, nodes, hasDefaultNodes } = get(); if (changes?.length) { if (hasDefaultNodes) { const updatedNodes = applyNodeChanges(changes, nodes); - const nextNodes = adoptUserProvidedNodes(updatedNodes, nodeLookup, { - nodeOrigin, - elevateNodesOnSelect, - }); - set({ nodes: nextNodes }); + setNodes(updatedNodes); } onNodesChange?.(changes); } }, + triggerEdgeChanges: (changes) => { + const { onEdgesChange, setEdges, edges, hasDefaultEdges } = get(); + if (changes?.length) { + if (hasDefaultEdges) { + const updatedEdges = applyEdgeChanges(changes, edges); + setEdges(updatedEdges); + } + + onEdgesChange?.(changes); + } + }, addSelectedNodes: (selectedNodeIds) => { - const { multiSelectionActive, edges, nodes } = get(); - let changedNodes: NodeSelectionChange[]; - let changedEdges: EdgeSelectionChange[] | null = null; + const { multiSelectionActive, edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get(); if (multiSelectionActive) { - changedNodes = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)) as NodeSelectionChange[]; - } else { - changedNodes = getSelectionChanges(nodes, new Set([...selectedNodeIds]), true); - changedEdges = getSelectionChanges(edges); + const nodeChanges = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)); + triggerNodeChanges(nodeChanges as NodeSelectionChange[]); + return; } - updateNodesAndEdgesSelections({ - changedNodes, - changedEdges, - get, - set, - }); + triggerNodeChanges(getSelectionChanges(nodes, new Set([...selectedNodeIds]), true)); + triggerEdgeChanges(getSelectionChanges(edges)); }, addSelectedEdges: (selectedEdgeIds) => { - const { multiSelectionActive, edges, nodes } = get(); - let changedEdges: EdgeSelectionChange[]; - let changedNodes: NodeSelectionChange[] | null = null; + const { multiSelectionActive, edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get(); if (multiSelectionActive) { - changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)) as EdgeSelectionChange[]; - } else { - changedEdges = getSelectionChanges(edges, new Set([...selectedEdgeIds])); - changedNodes = getSelectionChanges(nodes, new Set(), true); + const changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)); + triggerEdgeChanges(changedEdges as EdgeSelectionChange[]); + return; } - updateNodesAndEdgesSelections({ - changedNodes, - changedEdges, - get, - set, - }); + triggerEdgeChanges(getSelectionChanges(edges, new Set([...selectedEdgeIds]))); + triggerNodeChanges(getSelectionChanges(nodes, new Set(), true)); }, unselectNodesAndEdges: ({ nodes, edges }: UnselectNodesAndEdgesParams = {}) => { - const { edges: storeEdges, nodes: storeNodes } = get(); + const { edges: storeEdges, nodes: storeNodes, triggerNodeChanges, triggerEdgeChanges } = get(); const nodesToUnselect = nodes ? nodes : storeNodes; const edgesToUnselect = edges ? edges : storeEdges; - const changedNodes = nodesToUnselect.map((n) => { + const nodeChanges = nodesToUnselect.map((n) => { n.selected = false; return createSelectionChange(n.id, false); - }) as NodeSelectionChange[]; - const changedEdges = edgesToUnselect.map((edge) => - createSelectionChange(edge.id, false) - ) as EdgeSelectionChange[]; - - updateNodesAndEdgesSelections({ - changedNodes, - changedEdges, - get, - set, }); + const edgeChanges = edgesToUnselect.map((edge) => createSelectionChange(edge.id, false)); + + triggerNodeChanges(nodeChanges as NodeSelectionChange[]); + triggerEdgeChanges(edgeChanges as EdgeSelectionChange[]); }, setMinZoom: (minZoom) => { const { panZoom, maxZoom } = get(); @@ -260,21 +216,19 @@ const createRFStore = ({ set({ translateExtent }); }, resetSelectedElements: () => { - const { edges, nodes } = get(); + const { edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get(); - const nodesToUnselect = nodes - .filter((e) => e.selected) - .map((n) => createSelectionChange(n.id, false)) as NodeSelectionChange[]; - const edgesToUnselect = edges - .filter((e) => e.selected) - .map((e) => createSelectionChange(e.id, false)) as EdgeSelectionChange[]; + const nodeChanges = nodes.reduce( + (res, node) => (node.selected ? [...res, createSelectionChange(node.id, false) as NodeSelectionChange] : res), + [] + ); + const edgeChanges = edges.reduce( + (res, edge) => (edge.selected ? [...res, createSelectionChange(edge.id, false) as EdgeSelectionChange] : res), + [] + ); - updateNodesAndEdgesSelections({ - changedNodes: nodesToUnselect, - changedEdges: edgesToUnselect, - get, - set, - }); + triggerNodeChanges(nodeChanges); + triggerEdgeChanges(edgeChanges); }, setNodeExtent: (nodeExtent) => { const { nodes } = get(); diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index aace0bb4..5a1f9562 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -12,14 +12,18 @@ import { import type { Edge, Node, ReactFlowStore } from '../types'; const getInitialState = ({ - nodes = [], - edges = [], + nodes, + edges, + defaultNodes, + defaultEdges, width, height, fitView, }: { nodes?: Node[]; edges?: Edge[]; + defaultNodes?: Node[]; + defaultEdges?: Edge[]; width?: number; height?: number; fitView?: boolean; @@ -27,9 +31,11 @@ const getInitialState = ({ const nodeLookup = new Map(); const connectionLookup = new Map(); const edgeLookup = new Map(); + const storeEdges = defaultEdges ?? edges ?? []; + const storeNodes = defaultNodes ?? nodes ?? []; - updateConnectionLookup(connectionLookup, edgeLookup, edges); - const nextNodes = adoptUserProvidedNodes(nodes, nodeLookup, { + updateConnectionLookup(connectionLookup, edgeLookup, storeEdges); + const nextNodes = adoptUserProvidedNodes(storeNodes, nodeLookup, { nodeOrigin: [0, 0], elevateNodesOnSelect: false, }); @@ -51,13 +57,13 @@ const getInitialState = ({ transform, nodes: nextNodes, nodeLookup, - edges, + edges: storeEdges, edgeLookup, connectionLookup, onNodesChange: null, onEdgesChange: null, - hasDefaultNodes: false, - hasDefaultEdges: false, + hasDefaultNodes: defaultNodes !== undefined, + hasDefaultEdges: defaultEdges !== undefined, panZoom: null, minZoom: 0.5, maxZoom: 2, diff --git a/packages/react/src/store/utils.ts b/packages/react/src/store/utils.ts deleted file mode 100644 index 86cddde7..00000000 --- a/packages/react/src/store/utils.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { StoreApi } from 'zustand'; -import type { Edge, EdgeSelectionChange, Node, NodeSelectionChange, ReactFlowState } from '../types'; - -export function handleControlledSelectionChange( - changes: NodeSelectionChange[] | EdgeSelectionChange[], - items: NodeOrEdge[] -): NodeOrEdge[] { - return items.map((item) => { - const change = changes.find((change) => change.id === item.id); - - if (change) { - item.selected = change.selected; - } - - return item; - }); -} - -type UpdateNodesAndEdgesParams = { - changedNodes: NodeSelectionChange[] | null; - changedEdges: EdgeSelectionChange[] | null; - get: StoreApi['getState']; - set: StoreApi['setState']; -}; - -export function updateNodesAndEdgesSelections({ changedNodes, changedEdges, get, set }: UpdateNodesAndEdgesParams) { - const { nodes, edges, onNodesChange, onEdgesChange, hasDefaultNodes, hasDefaultEdges } = get(); - - if (changedNodes?.length) { - if (hasDefaultNodes) { - set({ nodes: handleControlledSelectionChange(changedNodes, nodes) }); - } - - onNodesChange?.(changedNodes); - } - - if (changedEdges?.length) { - if (hasDefaultEdges) { - set({ edges: handleControlledSelectionChange(changedEdges, edges) }); - } - - onEdgesChange?.(changedEdges); - } -} diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index b3e0449e..46f83128 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -43,6 +43,7 @@ import type { OnDelete, OnNodeDrag, OnBeforeDelete, + EdgeChange, } from '.'; export type ReactFlowStore = { @@ -150,7 +151,6 @@ export type ReactFlowStore = { export type ReactFlowActions = { setNodes: (nodes: Node[]) => void; setEdges: (edges: Edge[]) => void; - setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => void; updateNodeDimensions: (updates: Map) => void; updateNodePositions: UpdateNodePositions; resetSelectedElements: () => void; @@ -164,7 +164,8 @@ export type ReactFlowActions = { cancelConnection: () => void; updateConnection: UpdateConnection; reset: () => void; - triggerNodeChanges: (changes: NodeChange[]) => void; + triggerNodeChanges: (changes: NodeChange[] | null) => void; + triggerEdgeChanges: (changes: EdgeChange[] | null) => void; panBy: PanBy; fitView: (nodes: Node[], options?: FitViewOptions) => boolean; }; diff --git a/packages/react/src/utils/changes.ts b/packages/react/src/utils/changes.ts index 310b784c..5789a38a 100644 --- a/packages/react/src/utils/changes.ts +++ b/packages/react/src/utils/changes.ts @@ -219,11 +219,13 @@ export function applyEdgeChanges( return applyChanges(changes, edges) as EdgeType[]; } -export const createSelectionChange = (id: string, selected: boolean): NodeSelectionChange | EdgeSelectionChange => ({ - id, - type: 'select', - selected, -}); +export function createSelectionChange(id: string, selected: boolean) { + return { + id, + type: 'select', + selected, + }; +} export function getSelectionChanges( items: any[], From f507fbcb4b07d5aef3da6c672a0ec966d8420e61 Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 7 Feb 2024 17:16:24 +0100 Subject: [PATCH 28/38] chore(react): createSelectionChange types --- packages/react/src/utils/changes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/utils/changes.ts b/packages/react/src/utils/changes.ts index 5789a38a..fa6f6f30 100644 --- a/packages/react/src/utils/changes.ts +++ b/packages/react/src/utils/changes.ts @@ -219,7 +219,7 @@ export function applyEdgeChanges( return applyChanges(changes, edges) as EdgeType[]; } -export function createSelectionChange(id: string, selected: boolean) { +export function createSelectionChange(id: string, selected: boolean): NodeSelectionChange | EdgeSelectionChange { return { id, type: 'select', From 57270f3eb413f8ea594d38365a086ad95721aa9c Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 7 Feb 2024 17:27:23 +0100 Subject: [PATCH 29/38] chore(ts-docs): use correct values for min/max zoom closes #3881 --- packages/react/src/types/component-props.ts | 4 ++-- packages/svelte/src/lib/container/SvelteFlow/types.ts | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index 6018e1f5..25778476 100644 --- a/packages/react/src/types/component-props.ts +++ b/packages/react/src/types/component-props.ts @@ -346,11 +346,11 @@ export interface ReactFlowProps extends Omit, 'on */ panOnDrag?: boolean | number[]; /** Minimum zoom level - * @default 0.1 + * @default 0.5 */ minZoom?: number; /** Maximum zoom level - * @default 1 + * @default 2 */ maxZoom?: number; /** Controlled viewport to be used instead of internal one */ diff --git a/packages/svelte/src/lib/container/SvelteFlow/types.ts b/packages/svelte/src/lib/container/SvelteFlow/types.ts index aefa6f6e..25f63833 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/types.ts +++ b/packages/svelte/src/lib/container/SvelteFlow/types.ts @@ -18,8 +18,7 @@ import type { ColorMode, OnConnect, OnConnectStart, - OnConnectEnd, - OnBeforeDelete + OnConnectEnd } from '@xyflow/system'; import type { @@ -31,7 +30,8 @@ import type { DefaultEdgeOptions, FitViewOptions, OnDelete, - OnEdgeCreate + OnEdgeCreate, + OnBeforeDelete } from '$lib/types'; import type { Writable } from 'svelte/store'; @@ -135,11 +135,11 @@ export type SvelteFlowProps = DOMAttributes & { */ nodeDragThreshold?: number; /** Minimum zoom level - * @default 0.1 + * @default 0.5 */ minZoom?: number; /** Maximum zoom level - * @default 1 + * @default 2 */ maxZoom?: number; /** Sets the initial position and zoom of the viewport. From c3d5c8cdfaf09657ce4086d7503ad3feed36ef44 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 8 Feb 2024 13:32:37 +0100 Subject: [PATCH 30/38] refactor(useHandleConnections): also return edge id --- packages/react/src/hooks/useHandleConnections.ts | 14 ++++++++++---- .../svelte/src/lib/components/Handle/Handle.svelte | 6 +++--- .../svelte/src/lib/hooks/useHandleConnections.ts | 6 +++--- packages/system/src/types/general.ts | 6 +++++- packages/system/src/utils/connections.ts | 12 ++++++------ packages/system/src/utils/store.ts | 2 +- 6 files changed, 28 insertions(+), 18 deletions(-) diff --git a/packages/react/src/hooks/useHandleConnections.ts b/packages/react/src/hooks/useHandleConnections.ts index 4f62615e..5e7a89a8 100644 --- a/packages/react/src/hooks/useHandleConnections.ts +++ b/packages/react/src/hooks/useHandleConnections.ts @@ -1,5 +1,11 @@ import { useEffect, useMemo, useRef } from 'react'; -import { Connection, HandleType, areConnectionMapsEqual, handleConnectionChange } from '@xyflow/system'; +import { + Connection, + HandleConnection, + HandleType, + areConnectionMapsEqual, + handleConnectionChange, +} from '@xyflow/system'; import { useStore } from './useStore'; import { useNodeId } from '../contexts/NodeIdContext'; @@ -21,7 +27,7 @@ type useHandleConnectionsParams = { * @param param.id - the handle id (this is only needed if the node has multiple handles of the same type) * @param param.onConnect - gets called when a connection is established * @param param.onDisconnect - gets called when a connection is removed - * @returns an array with connections + * @returns an array with handle connections */ export function useHandleConnections({ type, @@ -29,9 +35,9 @@ export function useHandleConnections({ nodeId, onConnect, onDisconnect, -}: useHandleConnectionsParams): Connection[] { +}: useHandleConnectionsParams): HandleConnection[] { const _nodeId = useNodeId(); - const prevConnections = useRef | null>(null); + const prevConnections = useRef | null>(null); const currentNodeId = nodeId || _nodeId; const connections = useStore( diff --git a/packages/svelte/src/lib/components/Handle/Handle.svelte b/packages/svelte/src/lib/components/Handle/Handle.svelte index f00ccc44..f8a43dd4 100644 --- a/packages/svelte/src/lib/components/Handle/Handle.svelte +++ b/packages/svelte/src/lib/components/Handle/Handle.svelte @@ -6,7 +6,7 @@ Position, XYHandle, isMouseEvent, - type Connection, + type HandleConnection, areConnectionMapsEqual, handleConnectionChange } from '@xyflow/system'; @@ -103,8 +103,8 @@ } } - let prevConnections: Map | null = null; - let connections: Map | undefined; + let prevConnections: Map | null = null; + let connections: Map | undefined; $: if (onconnect || ondisconnect) { // connectionLookup is not reactive, so we use edges to get notified about updates diff --git a/packages/svelte/src/lib/hooks/useHandleConnections.ts b/packages/svelte/src/lib/hooks/useHandleConnections.ts index 27e61c11..d3f51998 100644 --- a/packages/svelte/src/lib/hooks/useHandleConnections.ts +++ b/packages/svelte/src/lib/hooks/useHandleConnections.ts @@ -1,5 +1,5 @@ import { derived } from 'svelte/store'; -import { areConnectionMapsEqual, type Connection, type HandleType } from '@xyflow/system'; +import { areConnectionMapsEqual, type HandleConnection, type HandleType } from '@xyflow/system'; import { useStore } from '$lib/store'; @@ -9,7 +9,7 @@ export type useHandleConnectionsParams = { id?: string | null; }; -const initialConnections: Connection[] = []; +const initialConnections: HandleConnection[] = []; /** * Hook to check if a is connected to another and get the connections. @@ -22,7 +22,7 @@ const initialConnections: Connection[] = []; */ export function useHandleConnections({ nodeId, type, id = null }: useHandleConnectionsParams) { const { edges, connectionLookup } = useStore(); - let prevConnections: Map | undefined = undefined; + let prevConnections: Map | undefined = undefined; return derived( [edges, connectionLookup], diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index a3b5879d..b7aa248e 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -28,6 +28,10 @@ export type Connection = { targetHandle: string | null; }; +export type HandleConnection = Connection & { + edgeId: string; +}; + export type ConnectionStatus = 'valid' | 'invalid'; export enum ConnectionMode { @@ -136,7 +140,7 @@ export type UpdateConnection = (params: { export type ColorModeClass = 'light' | 'dark'; export type ColorMode = ColorModeClass | 'system'; -export type ConnectionLookup = Map>; +export type ConnectionLookup = Map>; export type OnBeforeDeleteBase = ({ nodes, diff --git a/packages/system/src/utils/connections.ts b/packages/system/src/utils/connections.ts index ded405c3..b99e6c14 100644 --- a/packages/system/src/utils/connections.ts +++ b/packages/system/src/utils/connections.ts @@ -1,9 +1,9 @@ -import { Connection } from '../types'; +import { HandleConnection } from '../types'; /** * @internal */ -export function areConnectionMapsEqual(a?: Map, b?: Map) { +export function areConnectionMapsEqual(a?: Map, b?: Map) { if (!a && !b) { return true; } @@ -31,15 +31,15 @@ export function areConnectionMapsEqual(a?: Map, b?: Map, - b: Map, - cb?: (diff: Connection[]) => void + a: Map, + b: Map, + cb?: (diff: HandleConnection[]) => void ) { if (!cb) { return; } - const diff: Connection[] = []; + const diff: HandleConnection[] = []; a.forEach((connection, key) => { if (!b?.has(key)) { diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index 51cb1329..5cb8a382 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -260,7 +260,7 @@ export function updateConnectionLookup(connectionLookup: ConnectionLookup, edgeL const prevSource = connectionLookup.get(sourceKey) || new Map(); const prevTarget = connectionLookup.get(targetKey) || new Map(); - const connection = { source, target, sourceHandle, targetHandle }; + const connection = { edgeId: edge.id, source, target, sourceHandle, targetHandle }; edgeLookup.set(edge.id, edge); connectionLookup.set(sourceKey, prevSource.set(`${target}-${targetHandle}`, connection)); From b77fe9da1f80445dc23e7d2e788109cb2159a685 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 13 Feb 2024 10:50:57 +0100 Subject: [PATCH 31/38] fix(deletion): do not delete node when user is inside input closes #3895 --- packages/react/src/hooks/useKeyPress.ts | 6 ++++++ .../src/lib/components/KeyHandler/KeyHandler.svelte | 10 +++++++++- packages/system/src/utils/dom.ts | 4 +--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/react/src/hooks/useKeyPress.ts b/packages/react/src/hooks/useKeyPress.ts index 77966207..9f738d06 100644 --- a/packages/react/src/hooks/useKeyPress.ts +++ b/packages/react/src/hooks/useKeyPress.ts @@ -92,6 +92,12 @@ export function useKeyPress( } else { pressedKeys.current.delete(event[keyOrCode]); } + + // fix for Mac: when cmd key is pressed, keyup is not triggered for any other key, see: https://stackoverflow.com/questions/27380018/when-cmd-key-is-kept-pressed-keyup-is-not-triggered-for-any-other-key + if (event.key === 'Meta') { + pressedKeys.current.clear(); + } + modifierPressed.current = false; }; diff --git a/packages/svelte/src/lib/components/KeyHandler/KeyHandler.svelte b/packages/svelte/src/lib/components/KeyHandler/KeyHandler.svelte index f6697cb2..4c4f138e 100644 --- a/packages/svelte/src/lib/components/KeyHandler/KeyHandler.svelte +++ b/packages/svelte/src/lib/components/KeyHandler/KeyHandler.svelte @@ -113,7 +113,15 @@ { ...deleteKeyDefinition, enabled: deleteKeyDefinition.key !== null, - callback: (detail) => !isInputDOMNode(detail.originalEvent) && deleteKeyPressed.set(true) + callback: (detail) => { + const isModifierKey = + detail.originalEvent.ctrlKey || + detail.originalEvent.metaKey || + detail.originalEvent.shiftKey; + if (!isModifierKey && !isInputDOMNode(detail.originalEvent)) { + deleteKeyPressed.set(true); + } + } } ], type: 'keydown' diff --git a/packages/system/src/utils/dom.ts b/packages/system/src/utils/dom.ts index 3cf5cd61..9312bc07 100644 --- a/packages/system/src/utils/dom.ts +++ b/packages/system/src/utils/dom.ts @@ -37,11 +37,9 @@ export function isInputDOMNode(event: KeyboardEvent): boolean { // using composed path for handling shadow dom const target = (event.composedPath?.()?.[0] || event.target) as HTMLElement; const isInput = inputTags.includes(target?.nodeName) || target?.hasAttribute('contenteditable'); - // we want to be able to do a multi selection event if we are in an input field - const isModifierKey = event.ctrlKey || event.metaKey || event.shiftKey; // when an input field is focused we don't want to trigger deletion or movement of nodes - return (isInput && !isModifierKey) || !!target?.closest('.nokey'); + return isInput || !!target?.closest('.nokey'); } export const isMouseEvent = (event: MouseEvent | TouchEvent): event is MouseEvent => 'clientX' in event; From 0d80b45d05940d059f041813291fa4c236419d34 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 13 Feb 2024 10:52:11 +0100 Subject: [PATCH 32/38] chore(changelog): update --- packages/react/CHANGELOG.md | 1 + packages/svelte/CHANGELOG.md | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 54c5f9ec..0308f87a 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -3,6 +3,7 @@ ## 12.0.0-next.9 - a better NodeResizer that works with subflows. Child nodes do not move when parent node gets resized and parent extent is taken into account +- don't delete node when input is focused and user presses Backspace + Ctrl (or any other mod key) ## 12.0.0-next.8 diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 123295bd..9349a2fc 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -7,6 +7,7 @@ - a better NodeResizer (child nodes do not move when parent node gets resized) - fix `on:panecontextmenu` - add `role="button"` to `` to prevent a11y warnings +- don't delete node when input is focused and user presses Backspace + Ctrl (or any other mod key) ## 0.0.35 From 2860feec112d52b39d90d4fc8f8ac1c3eecbdbcb Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 13 Feb 2024 16:12:31 +0100 Subject: [PATCH 33/38] fix(react): uncontrolled flows with Provider --- .../src/components/ReactFlowProvider/index.tsx | 1 - .../src/components/StoreUpdater/index.tsx | 18 ++++++++++++++---- .../react/src/container/ReactFlow/index.tsx | 2 ++ packages/react/src/store/index.ts | 12 ++++++++++++ packages/react/src/types/store.ts | 1 + 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/packages/react/src/components/ReactFlowProvider/index.tsx b/packages/react/src/components/ReactFlowProvider/index.tsx index 829a304d..3d2577e7 100644 --- a/packages/react/src/components/ReactFlowProvider/index.tsx +++ b/packages/react/src/components/ReactFlowProvider/index.tsx @@ -26,7 +26,6 @@ export function ReactFlowProvider({ fitView?: boolean; }) { const storeRef = useRef> | null>(null); - if (!storeRef.current) { storeRef.current = createRFStore({ nodes: initialNodes, diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx index 98ad3bac..96080368 100644 --- a/packages/react/src/components/StoreUpdater/index.tsx +++ b/packages/react/src/components/StoreUpdater/index.tsx @@ -15,6 +15,8 @@ import { initNodeOrigin } from '../../container/ReactFlow'; const reactFlowFieldsToTrack = [ 'nodes', 'edges', + 'defaultNodes', + 'defaultEdges', 'onConnect', 'onConnectStart', 'onConnectEnd', @@ -79,16 +81,24 @@ const selector = (s: ReactFlowState) => ({ setTranslateExtent: s.setTranslateExtent, setNodeExtent: s.setNodeExtent, reset: s.reset, + setDefaultNodesAndEdges: s.setDefaultNodesAndEdges, }); export function StoreUpdater(props: StoreUpdaterProps) { - const { setNodes, setEdges, setMinZoom, setMaxZoom, setTranslateExtent, setNodeExtent, reset } = useStore( - selector, - shallow - ); + const { + setNodes, + setEdges, + setMinZoom, + setMaxZoom, + setTranslateExtent, + setNodeExtent, + reset, + setDefaultNodesAndEdges, + } = useStore(selector, shallow); const store = useStoreApi(); useEffect(() => { + setDefaultNodesAndEdges(props.defaultNodes, props.defaultEdges); return () => { reset(); }; diff --git a/packages/react/src/container/ReactFlow/index.tsx b/packages/react/src/container/ReactFlow/index.tsx index 9ac7b051..df9936a1 100644 --- a/packages/react/src/container/ReactFlow/index.tsx +++ b/packages/react/src/container/ReactFlow/index.tsx @@ -242,6 +242,8 @@ const ReactFlow = forwardRef( { + if (nodes) { + const { setNodes } = get(); + setNodes(nodes); + set({ hasDefaultNodes: true }); + } + if (edges) { + const { setEdges } = get(); + setEdges(edges); + set({ hasDefaultEdges: true }); + } + }, // Every node gets registerd at a ResizeObserver. Whenever a node // changes its dimensions, this function is called to measure the // new dimensions and update the nodes. diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index 46f83128..f1c625ba 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -168,6 +168,7 @@ export type ReactFlowActions = { triggerEdgeChanges: (changes: EdgeChange[] | null) => void; panBy: PanBy; fitView: (nodes: Node[], options?: FitViewOptions) => boolean; + setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => void; }; export type ReactFlowState = ReactFlowStore & ReactFlowActions; From 04ba6e0e4ec54b23fde075e497f5ad95c833efcf Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 13 Feb 2024 17:19:32 +0100 Subject: [PATCH 34/38] fix(nodes): re-measure nodes when dimensions are missing --- .../src/examples/CustomMiniMapNode/index.tsx | 6 +-- examples/react/src/examples/Subflow/index.tsx | 40 +++++++++---------- .../src/components/NodeWrapper/index.tsx | 21 ++++++---- packages/system/src/utils/store.ts | 4 +- 4 files changed, 38 insertions(+), 33 deletions(-) diff --git a/examples/react/src/examples/CustomMiniMapNode/index.tsx b/examples/react/src/examples/CustomMiniMapNode/index.tsx index 0b62196f..7ecf1c54 100644 --- a/examples/react/src/examples/CustomMiniMapNode/index.tsx +++ b/examples/react/src/examples/CustomMiniMapNode/index.tsx @@ -27,9 +27,9 @@ const buttonStyle: CSSProperties = { zIndex: 4, }; -const CustomMiniMapNode = ({ x, y, width, height, color }: MiniMapNodeProps) => ( - -); +const CustomMiniMapNode = ({ x, y, width, height }: MiniMapNodeProps) => { + return ; +}; const CustomMiniMapNodeFlow = () => { const [nodes, setNodes, onNodesChange] = useNodesState([]); diff --git a/examples/react/src/examples/Subflow/index.tsx b/examples/react/src/examples/Subflow/index.tsx index 19dc119a..8ea28e34 100644 --- a/examples/react/src/examples/Subflow/index.tsx +++ b/examples/react/src/examples/Subflow/index.tsx @@ -90,7 +90,7 @@ const initialNodes: Node[] = [ data: { label: 'Node 5' }, position: { x: 650, y: 250 }, className: 'light', - style: { width: 400, height: 150 }, + style: { width: 100, height: 100 }, zIndex: 1000, }, { @@ -161,9 +161,12 @@ const Subflow = () => { setNodes((nds) => { return nds.map((n) => { if (!n.parentNode) { - n.position = { - x: Math.random() * 400, - y: Math.random() * 400, + return { + ...n, + position: { + x: Math.random() * 400, + y: Math.random() * 400, + }, }; } @@ -178,8 +181,10 @@ const Subflow = () => { const toggleClassnames = () => { setNodes((nds) => { return nds.map((n) => { - n.className = n.className === 'light' ? 'dark' : 'light'; - return n; + return { + ...n, + className: n.className === 'light' ? 'dark' : 'light', + }; }); }); }; @@ -187,8 +192,10 @@ const Subflow = () => { const toggleChildNodes = () => { setNodes((nds) => { return nds.map((n) => { - n.hidden = !!n.parentNode && !n.hidden; - return n; + return { + ...n, + hidden: !!n.parentNode && !n.hidden, + }; }); }); }; @@ -215,19 +222,12 @@ const Subflow = () => { - - - - + + + + + ); diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index ebf65163..fa3ec791 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -79,16 +79,27 @@ export function NodeWrapper({ const prevTargetPosition = useRef(node.targetPosition); const prevType = useRef(nodeType); + const width = node.width ?? undefined; + const height = node.height ?? undefined; + const computedWidth = node.computed?.width; + const computedHeight = node.computed?.height; + const initialized = (!!computedWidth && !!computedHeight) || (!!width && !!height); + const moveSelectedNodes = useMoveSelectedNodes(); useEffect(() => { if (nodeRef.current && !node.hidden) { const currNode = nodeRef.current; - resizeObserver?.observe(currNode); + + if (!initialized) { + resizeObserver?.observe(currNode); + } else { + resizeObserver?.unobserve(currNode); + } return () => resizeObserver?.unobserve(currNode); } - }, [node.hidden]); + }, [node.hidden, initialized]); useEffect(() => { // when the user programmatically changes the source or handle position, we re-initialize the node @@ -123,11 +134,6 @@ export function NodeWrapper({ return null; } - const width = node.width ?? undefined; - const height = node.height ?? undefined; - const computedWidth = node.computed?.width; - const computedHeight = node.computed?.height; - const positionAbsoluteOrigin = getPositionWithOrigin({ x: positionAbsoluteX, y: positionAbsoluteY, @@ -135,7 +141,6 @@ export function NodeWrapper({ height: computedHeight ?? height ?? 0, origin: node.origin || nodeOrigin, }); - const initialized = (!!computedWidth && !!computedHeight) || (!!width && !!height); const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave; const onMouseEnterHandler = onMouseEnter ? (event: MouseEvent) => onMouseEnter(event, { ...node }) : undefined; diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index 5cb8a382..c04c7e51 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -94,8 +94,8 @@ export function adoptUserProvidedNodes( ...n, computed: { positionAbsolute: n.position, - width: n.computed?.width || currentStoreNode?.computed?.width, - height: n.computed?.height || currentStoreNode?.computed?.height, + width: n.computed?.width, + height: n.computed?.height, }, }; const z = (isNumeric(n.zIndex) ? n.zIndex : 0) + (n.selected ? selectedNodeZ : 0); From 940148ba8b54728e48a0336a1c79792278991be5 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Thu, 15 Feb 2024 11:06:45 +0100 Subject: [PATCH 35/38] fix node drag start & stop being fired when draggable=false --- packages/system/src/xydrag/XYDrag.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/system/src/xydrag/XYDrag.ts b/packages/system/src/xydrag/XYDrag.ts index 5c13f825..91ed7fb8 100644 --- a/packages/system/src/xydrag/XYDrag.ts +++ b/packages/system/src/xydrag/XYDrag.ts @@ -238,7 +238,7 @@ export function XYDrag voi const onNodeOrSelectionDragStart = nodeId ? onNodeDragStart : wrapSelectionDragFunc(onSelectionDragStart); - if (dragItems && (onDragStart || onNodeOrSelectionDragStart)) { + if (dragItems.length > 0 && (onDragStart || onNodeOrSelectionDragStart)) { const [currentNode, currentNodes] = getEventHandlerParams({ nodeId, dragItems, @@ -298,7 +298,7 @@ export function XYDrag voi dragStarted = false; cancelAnimationFrame(autoPanId); - if (dragItems) { + if (dragItems.length > 0) { const { nodeLookup, updateNodePositions, onNodeDragStop, onSelectionDragStop } = getStoreItems(); const onNodeOrSelectionDragStop = nodeId ? onNodeDragStop : wrapSelectionDragFunc(onSelectionDragStop); From 99fc00f5dcc3b4c31d74e928f507b79991e873c5 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Thu, 15 Feb 2024 11:32:55 +0100 Subject: [PATCH 36/38] aligned useHandleConnections in svelte with react version --- packages/react/src/hooks/useHandleConnections.ts | 3 ++- packages/svelte/src/lib/hooks/useHandleConnections.ts | 11 ++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/react/src/hooks/useHandleConnections.ts b/packages/react/src/hooks/useHandleConnections.ts index 5e7a89a8..20b65a2d 100644 --- a/packages/react/src/hooks/useHandleConnections.ts +++ b/packages/react/src/hooks/useHandleConnections.ts @@ -37,8 +37,9 @@ export function useHandleConnections({ onDisconnect, }: useHandleConnectionsParams): HandleConnection[] { const _nodeId = useNodeId(); + const currentNodeId = nodeId ?? _nodeId; + const prevConnections = useRef | null>(null); - const currentNodeId = nodeId || _nodeId; const connections = useStore( (state) => state.connectionLookup.get(`${currentNodeId}-${type}-${id}`), diff --git a/packages/svelte/src/lib/hooks/useHandleConnections.ts b/packages/svelte/src/lib/hooks/useHandleConnections.ts index d3f51998..7aec3858 100644 --- a/packages/svelte/src/lib/hooks/useHandleConnections.ts +++ b/packages/svelte/src/lib/hooks/useHandleConnections.ts @@ -2,10 +2,11 @@ import { derived } from 'svelte/store'; import { areConnectionMapsEqual, type HandleConnection, type HandleType } from '@xyflow/system'; import { useStore } from '$lib/store'; +import { getContext } from 'svelte'; export type useHandleConnectionsParams = { - nodeId: string; type: HandleType; + nodeId?: string; id?: string | null; }; @@ -20,14 +21,18 @@ const initialConnections: HandleConnection[] = []; * @param param.id - the handle id (this is only needed if the node has multiple handles of the same type) * @returns an array with connections */ -export function useHandleConnections({ nodeId, type, id = null }: useHandleConnectionsParams) { +export function useHandleConnections({ type, nodeId, id = null }: useHandleConnectionsParams) { const { edges, connectionLookup } = useStore(); + + const _nodeId = getContext('svelteflow__node_id'); + const currentNodeId = nodeId ?? _nodeId; + let prevConnections: Map | undefined = undefined; return derived( [edges, connectionLookup], ([, connectionLookup], set) => { - const nextConnections = connectionLookup.get(`${nodeId}-${type}-${id || null}`); + const nextConnections = connectionLookup.get(`${currentNodeId}-${type}-${id || null}`); if (!areConnectionMapsEqual(nextConnections, prevConnections)) { prevConnections = nextConnections; From edba3963d86308f80dc130936eff8a1cab139117 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 15 Feb 2024 17:59:54 +0100 Subject: [PATCH 37/38] fix(react): re-measure nodes correctly --- .../src/components/EdgeWrapper/index.tsx | 4 +-- .../src/components/NodeWrapper/index.tsx | 18 +++++++---- .../src/components/StoreUpdater/index.tsx | 31 +++++++++++-------- .../react/src/container/ReactFlow/index.tsx | 18 +++-------- .../src/container/ReactFlow/init-values.ts | 4 +++ packages/react/src/store/index.ts | 8 +---- 6 files changed, 41 insertions(+), 42 deletions(-) create mode 100644 packages/react/src/container/ReactFlow/init-values.ts diff --git a/packages/react/src/components/EdgeWrapper/index.tsx b/packages/react/src/components/EdgeWrapper/index.tsx index 28323a19..5973751d 100644 --- a/packages/react/src/components/EdgeWrapper/index.tsx +++ b/packages/react/src/components/EdgeWrapper/index.tsx @@ -101,12 +101,12 @@ export function EdgeWrapper({ ); const markerStartUrl = useMemo( - () => (edge.markerStart ? `url(#${getMarkerId(edge.markerStart, rfId)})` : undefined), + () => (edge.markerStart ? `url('#${getMarkerId(edge.markerStart, rfId)}')` : undefined), [edge.markerStart, rfId] ); const markerEndUrl = useMemo( - () => (edge.markerEnd ? `url(#${getMarkerId(edge.markerEnd, rfId)})` : undefined), + () => (edge.markerEnd ? `url('#${getMarkerId(edge.markerEnd, rfId)}')` : undefined), [edge.markerEnd, rfId] ); diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index fa3ec791..548a4aa4 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -84,22 +84,28 @@ export function NodeWrapper({ const computedWidth = node.computed?.width; const computedHeight = node.computed?.height; const initialized = (!!computedWidth && !!computedHeight) || (!!width && !!height); + const hasHandleBounds = !!node[internalsSymbol]?.handleBounds; const moveSelectedNodes = useMoveSelectedNodes(); + useEffect(() => { + return () => { + if (nodeRef.current) { + resizeObserver?.unobserve(nodeRef.current); + } + }; + }, []); + useEffect(() => { if (nodeRef.current && !node.hidden) { const currNode = nodeRef.current; - if (!initialized) { - resizeObserver?.observe(currNode); - } else { + if (!initialized || !hasHandleBounds) { resizeObserver?.unobserve(currNode); + resizeObserver?.observe(currNode); } - - return () => resizeObserver?.unobserve(currNode); } - }, [node.hidden, initialized]); + }, [node.hidden, initialized, hasHandleBounds]); useEffect(() => { // when the user programmatically changes the source or handle position, we re-initialize the node diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx index 96080368..f1d68e6c 100644 --- a/packages/react/src/components/StoreUpdater/index.tsx +++ b/packages/react/src/components/StoreUpdater/index.tsx @@ -9,7 +9,7 @@ import { infiniteExtent, type CoordinateExtent } from '@xyflow/system'; import { useStore, useStoreApi } from '../../hooks/useStore'; import type { Node, Edge, ReactFlowState, ReactFlowProps, FitViewOptions } from '../../types'; -import { initNodeOrigin } from '../../container/ReactFlow'; +import { defaultNodeOrigin } from '../../container/ReactFlow/init-values'; // these fields exist in the global store and we need to keep them up to date const reactFlowFieldsToTrack = [ @@ -84,6 +84,19 @@ const selector = (s: ReactFlowState) => ({ setDefaultNodesAndEdges: s.setDefaultNodesAndEdges, }); +const initPrevValues = { + // these are values that are also passed directly to other components + // than the StoreUpdater. We can reduce the number of setStore calls + // by setting the same values here as prev fields. + translateExtent: infiniteExtent, + nodeOrigin: defaultNodeOrigin, + minZoom: 0.5, + maxZoom: 2, + elementsSelectable: true, + noPanClassName: 'nopan', + rfId: '1', +}; + export function StoreUpdater(props: StoreUpdaterProps) { const { setNodes, @@ -99,23 +112,15 @@ export function StoreUpdater(props: StoreUpdaterProps) { useEffect(() => { setDefaultNodesAndEdges(props.defaultNodes, props.defaultEdges); + return () => { + // when we reset the store we also need to reset the previous fields + previousFields.current = initPrevValues; reset(); }; }, []); - const previousFields = useRef>({ - // these are values that are also passed directly to other components - // than the StoreUpdater. We can reduce the number of setStore calls - // by setting the same values here as prev fields. - translateExtent: infiniteExtent, - nodeOrigin: initNodeOrigin, - minZoom: 0.5, - maxZoom: 2, - elementsSelectable: true, - noPanClassName: 'nopan', - rfId: '1', - }); + const previousFields = useRef>(initPrevValues); useEffect( () => { diff --git a/packages/react/src/container/ReactFlow/index.tsx b/packages/react/src/container/ReactFlow/index.tsx index df9936a1..6448abe4 100644 --- a/packages/react/src/container/ReactFlow/index.tsx +++ b/packages/react/src/container/ReactFlow/index.tsx @@ -1,14 +1,6 @@ import { forwardRef, type CSSProperties } from 'react'; import cc from 'classcat'; -import { - ConnectionLineType, - PanOnScrollMode, - SelectionMode, - infiniteExtent, - isMacOs, - type NodeOrigin, - type Viewport, -} from '@xyflow/system'; +import { ConnectionLineType, PanOnScrollMode, SelectionMode, infiniteExtent, isMacOs } from '@xyflow/system'; import { A11yDescriptions } from '../../components/A11yDescriptions'; import { Attribution } from '../../components/Attribution'; @@ -18,9 +10,7 @@ import { useColorModeClass } from '../../hooks/useColorModeClass'; import { GraphView } from '../GraphView'; import { Wrapper } from './Wrapper'; import type { ReactFlowProps, ReactFlowRefType } from '../../types'; - -export const initNodeOrigin: NodeOrigin = [0, 0]; -const initDefaultViewport: Viewport = { x: 0, y: 0, zoom: 1 }; +import { defaultViewport as initViewport, defaultNodeOrigin } from './init-values'; const wrapperStyle: CSSProperties = { width: '100%', @@ -89,11 +79,11 @@ const ReactFlow = forwardRef( nodesDraggable, nodesConnectable, nodesFocusable, - nodeOrigin = initNodeOrigin, + nodeOrigin = defaultNodeOrigin, edgesFocusable, edgesUpdatable, elementsSelectable = true, - defaultViewport = initDefaultViewport, + defaultViewport = initViewport, minZoom = 0.5, maxZoom = 2, translateExtent = infiniteExtent, diff --git a/packages/react/src/container/ReactFlow/init-values.ts b/packages/react/src/container/ReactFlow/init-values.ts new file mode 100644 index 00000000..75e7fcaa --- /dev/null +++ b/packages/react/src/container/ReactFlow/init-values.ts @@ -0,0 +1,4 @@ +import { type NodeOrigin, Viewport } from '@xyflow/system'; + +export const defaultNodeOrigin: NodeOrigin = [0, 0]; +export const defaultViewport: Viewport = { x: 0, y: 0, zoom: 1 }; diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 46447e00..71a51a1e 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -303,13 +303,7 @@ const createRFStore = ({ set(currentConnection); }, - reset: () => { - // @todo: what should we do about this? Do we still need it? - // if you are on a SPA with multiple flows, we want to make sure that the store gets resetted - // when you switch pages. Does this reset solves this? Currently it always gets called. This - // leads to an emtpy nodes array at the beginning. - // set({ ...getInitialState() }); - }, + reset: () => set({ ...getInitialState() }), }), Object.is ); From 6eea7eb2ab1e32402dc56bd864a1cd61c3c165e7 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 15 Feb 2024 18:13:46 +0100 Subject: [PATCH 38/38] chore(packages): bump --- packages/react/CHANGELOG.md | 8 ++++++-- packages/react/package.json | 2 +- packages/svelte/CHANGELOG.md | 4 +++- packages/svelte/package.json | 2 +- packages/system/package.json | 2 +- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 0308f87a..e2e23ca4 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -2,8 +2,12 @@ ## 12.0.0-next.9 -- a better NodeResizer that works with subflows. Child nodes do not move when parent node gets resized and parent extent is taken into account -- don't delete node when input is focused and user presses Backspace + Ctrl (or any other mod key) +### Patch changes + +- a better `NodeResizer` that works with subflows. Child nodes do not move when parent node gets resized and parent extent is taken into account +- refactor `setNodes` batching +- re-measure nodes when necessary +- don't trigger drag start / end when node is not draggable ## 12.0.0-next.8 diff --git a/packages/react/package.json b/packages/react/package.json index 097cba0d..ddb6d327 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@xyflow/react", - "version": "12.0.0-next.8", + "version": "12.0.0-next.9", "description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.", "keywords": [ "react", diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 9349a2fc..2f34e1a3 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -2,12 +2,14 @@ ## 0.0.36 -## Minor changes +## Patch changes - a better NodeResizer (child nodes do not move when parent node gets resized) - fix `on:panecontextmenu` - add `role="button"` to `` to prevent a11y warnings - don't delete node when input is focused and user presses Backspace + Ctrl (or any other mod key) +- `useHandleConnections`: use context node id when no node id is passed +- don't trigger drag start / end when node is not draggable ## 0.0.35 diff --git a/packages/svelte/package.json b/packages/svelte/package.json index ac688280..6fe2bec2 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@xyflow/svelte", - "version": "0.0.35", + "version": "0.0.36", "description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.", "keywords": [ "svelte", diff --git a/packages/system/package.json b/packages/system/package.json index e4fc6875..f08cdd5b 100644 --- a/packages/system/package.json +++ b/packages/system/package.json @@ -1,6 +1,6 @@ { "name": "@xyflow/system", - "version": "0.0.16", + "version": "0.0.17", "description": "xyflow core system that powers React Flow and Svelte Flow.", "keywords": [ "node-based UI",