From a147a20f6e741f8870f414a77bfed940b3578a50 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 13 Nov 2023 16:43:57 +0100 Subject: [PATCH 01/23] refactor(react/svelte): use node lookup for perf improvement --- examples/react/src/examples/Stress/utils.ts | 2 -- package.json | 2 +- .../NodeResizer/ResizeControl.tsx | 8 ++--- .../src/components/ConnectionLine/index.tsx | 2 +- .../react/src/components/Edges/wrapEdge.tsx | 6 ++-- .../src/container/NodeRenderer/index.tsx | 15 ++++++---- packages/react/src/hooks/useVisibleEdges.ts | 6 ++-- packages/react/src/store/index.ts | 26 +++++++++++----- packages/react/src/store/initialState.ts | 4 ++- packages/react/src/types/store.ts | 3 +- packages/svelte/src/lib/actions/drag/index.ts | 1 + .../NodeRenderer/NodeRenderer.svelte | 17 +++++++---- .../src/lib/store/derived-connection-props.ts | 6 ++-- packages/svelte/src/lib/store/edge-tree.ts | 17 ++++++----- packages/svelte/src/lib/store/index.ts | 3 +- .../svelte/src/lib/store/initial-store.ts | 9 ++++-- packages/svelte/src/lib/store/types.ts | 2 +- packages/svelte/src/lib/store/utils.ts | 5 ++-- packages/system/src/utils/edges/general.ts | 6 ++-- packages/system/src/utils/store.ts | 30 +++++++++++++------ packages/system/src/xydrag/XYDrag.ts | 29 ++++++++++-------- packages/system/src/xydrag/utils.ts | 6 ++-- 22 files changed, 127 insertions(+), 78 deletions(-) diff --git a/examples/react/src/examples/Stress/utils.ts b/examples/react/src/examples/Stress/utils.ts index 6278e006..9266bf9c 100644 --- a/examples/react/src/examples/Stress/utils.ts +++ b/examples/react/src/examples/Stress/utils.ts @@ -20,8 +20,6 @@ export function getNodesAndEdges(xElements = 10, yElements = 10): ElementsCollec style: { width: 50, height: 30, fontSize: 11 }, data, position, - width: 50, - height: 30, }; initialNodes.push(node); diff --git a/package.json b/package.json index 4411e137..274670c2 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "preinstall": "npx only-allow pnpm", "dev": "turbo run dev --parallel --concurrency 12", "dev:svelte": "turbo run dev --filter=svelte --filter=system", - "dev:react": "turbo run dev --filter=react", + "dev:react": "turbo run dev --filter=react-examples ", "test:svelte": "pnpm --filter=playwright run test:svelte", "test:svelte:ui": "pnpm --filter=playwright run test:svelte:ui", "test:react": "pnpm --filter=playwright run test:react", diff --git a/packages/react/src/additional-components/NodeResizer/ResizeControl.tsx b/packages/react/src/additional-components/NodeResizer/ResizeControl.tsx index 3e038fd0..74028658 100644 --- a/packages/react/src/additional-components/NodeResizer/ResizeControl.tsx +++ b/packages/react/src/additional-components/NodeResizer/ResizeControl.tsx @@ -65,8 +65,8 @@ function ResizeControl({ const dragHandler = drag() .on('start', (event: ResizeDragEvent) => { - const { nodes, transform, snapGrid, snapToGrid } = store.getState(); - const node = nodes.find((n) => n.id === id); + const { nodesLookup, transform, snapGrid, snapToGrid } = store.getState(); + const node = nodesLookup.get(id); const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); prevValues.current = { @@ -86,9 +86,9 @@ function ResizeControl({ onResizeStart?.(event, { ...prevValues.current }); }) .on('drag', (event: ResizeDragEvent) => { - const { nodes, transform, snapGrid, snapToGrid, triggerNodeChanges } = store.getState(); + const { nodesLookup, transform, snapGrid, snapToGrid, triggerNodeChanges } = store.getState(); const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); - const node = nodes.find((n) => n.id === id); + const node = nodesLookup.get(id); if (node) { const changes: NodeChange[] = []; diff --git a/packages/react/src/components/ConnectionLine/index.tsx b/packages/react/src/components/ConnectionLine/index.tsx index 439cc342..b40efa65 100644 --- a/packages/react/src/components/ConnectionLine/index.tsx +++ b/packages/react/src/components/ConnectionLine/index.tsx @@ -43,7 +43,7 @@ const ConnectionLine = ({ const { fromNode, handleId, toX, toY, connectionMode } = useStore( useCallback( (s: ReactFlowStore) => ({ - fromNode: s.nodes.find((n) => n.id === nodeId), + fromNode: s.nodesLookup.get(nodeId), handleId: s.connectionStartHandle?.handleId, toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2], toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2], diff --git a/packages/react/src/components/Edges/wrapEdge.tsx b/packages/react/src/components/Edges/wrapEdge.tsx index 750254ff..4ab6fa45 100644 --- a/packages/react/src/components/Edges/wrapEdge.tsx +++ b/packages/react/src/components/Edges/wrapEdge.tsx @@ -53,9 +53,9 @@ export default (EdgeComponent: ComponentType) => { const [updateHover, setUpdateHover] = useState(false); const [updating, setUpdating] = useState(false); const store = useStoreApi(); - const edgePosition = useStore((state) => { - const sourceNode = state.nodes.find((n) => n.id === source); - const targetNode = state.nodes.find((n) => n.id === target); + const edgePosition = useStore(function edgeSelector(state) { + const sourceNode = state.nodesLookup.get(source); + const targetNode = state.nodesLookup.get(target); if (!sourceNode || !targetNode) { return null; diff --git a/packages/react/src/container/NodeRenderer/index.tsx b/packages/react/src/container/NodeRenderer/index.tsx index f2b3ebe3..98f9fc39 100644 --- a/packages/react/src/container/NodeRenderer/index.tsx +++ b/packages/react/src/container/NodeRenderer/index.tsx @@ -48,11 +48,16 @@ const NodeRenderer = (props: NodeRendererProps) => { } const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => { - const updates = entries.map((entry: ResizeObserverEntry) => ({ - id: entry.target.getAttribute('data-id') as string, - nodeElement: entry.target as HTMLDivElement, - forceUpdate: true, - })); + const updates = new Map(); + + entries.forEach((entry: ResizeObserverEntry) => { + const id = entry.target.getAttribute('data-id') as string; + updates.set(id, { + id, + nodeElement: entry.target as HTMLDivElement, + forceUpdate: true, + }); + }); updateNodeDimensions(updates); }); diff --git a/packages/react/src/hooks/useVisibleEdges.ts b/packages/react/src/hooks/useVisibleEdges.ts index 6d7f808a..e97fe105 100644 --- a/packages/react/src/hooks/useVisibleEdges.ts +++ b/packages/react/src/hooks/useVisibleEdges.ts @@ -12,8 +12,8 @@ function useVisibleEdges(onlyRenderVisible: boolean, elevateEdgesOnSelect: boole const visibleEdges = onlyRenderVisible && s.width && s.height ? s.edges.filter((e) => { - const sourceNode = s.nodes.find((n) => n.id === e.source); - const targetNode = s.nodes.find((n) => n.id === e.target); + const sourceNode = s.nodesLookup.get(e.source); + const targetNode = s.nodesLookup.get(e.target); return ( sourceNode && @@ -29,7 +29,7 @@ function useVisibleEdges(onlyRenderVisible: boolean, elevateEdgesOnSelect: boole }) : s.edges; - return groupEdgesByZLevel(visibleEdges, s.nodes, elevateEdgesOnSelect); + return groupEdgesByZLevel(visibleEdges, s.nodesLookup, elevateEdgesOnSelect); }, [onlyRenderVisible, elevateEdgesOnSelect] ), diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index ad42fd6e..6f8ca67b 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -41,8 +41,8 @@ const createRFStore = ({ (set, get) => ({ ...getInitialState({ nodes, edges, width, height, fitView }), setNodes: (nodes: Node[]) => { - const { nodes: storeNodes, nodeOrigin, elevateNodesOnSelect } = get(); - const nextNodes = updateNodes(nodes, storeNodes, { nodeOrigin, elevateNodesOnSelect }); + const { nodesLookup, nodeOrigin, elevateNodesOnSelect } = get(); + const nextNodes = updateNodes(nodes, nodesLookup, { nodeOrigin, elevateNodesOnSelect }); set({ nodes: nextNodes }); }, @@ -68,7 +68,7 @@ const createRFStore = ({ }; if (hasDefaultNodes) { - nextState.nodes = updateNodes(nodes, [], { + nextState.nodes = updateNodes(nodes, new Map(), { nodeOrigin: get().nodeOrigin, elevateNodesOnSelect: get().elevateNodesOnSelect, }); @@ -80,13 +80,23 @@ const createRFStore = ({ set(nextState); }, updateNodeDimensions: (updates) => { - const { onNodesChange, fitView, nodes, fitViewOnInit, fitViewDone, fitViewOnInitOptions, domNode, nodeOrigin } = - get(); + const { + onNodesChange, + fitView, + nodes, + nodesLookup, + fitViewOnInit, + fitViewDone, + fitViewOnInitOptions, + domNode, + nodeOrigin, + } = get(); const changes: NodeDimensionChange[] = []; const updatedNodes = updateNodeDimensionsSystem( updates, nodes, + nodesLookup, domNode, nodeOrigin, (id: string, dimensions: Dimensions) => { @@ -102,7 +112,7 @@ const createRFStore = ({ return; } - const nextNodes = updateAbsolutePositions(updatedNodes, nodeOrigin); + const nextNodes = updateAbsolutePositions(updatedNodes, nodesLookup, nodeOrigin); let nextFitViewDone = fitViewDone; if (!fitViewDone && fitViewOnInit) { @@ -138,12 +148,12 @@ const createRFStore = ({ }, triggerNodeChanges: (changes) => { - const { onNodesChange, nodes, hasDefaultNodes, nodeOrigin, elevateNodesOnSelect } = get(); + const { onNodesChange, nodesLookup, nodes, hasDefaultNodes, nodeOrigin, elevateNodesOnSelect } = get(); if (changes?.length) { if (hasDefaultNodes) { const updatedNodes = applyNodeChanges(changes, nodes); - const nextNodes = updateNodes(updatedNodes, nodes, { + const nextNodes = updateNodes(updatedNodes, nodesLookup, { nodeOrigin, elevateNodesOnSelect, }); diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index 8f8fa450..633d9aa5 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -22,7 +22,8 @@ const getInitialState = ({ height?: number; fitView?: boolean; } = {}): ReactFlowStore => { - const nextNodes = updateNodes(nodes, [], { nodeOrigin: [0, 0], elevateNodesOnSelect: false }); + const nodesLookup = new Map(); + const nextNodes = updateNodes(nodes, nodesLookup, { nodeOrigin: [0, 0], elevateNodesOnSelect: false }); let transform: Transform = [0, 0, 1]; @@ -43,6 +44,7 @@ const getInitialState = ({ height: 0, transform, nodes: nextNodes, + nodesLookup, edges: edges, onNodesChange: null, onEdgesChange: null, diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index 3662f862..3995d208 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -46,6 +46,7 @@ export type ReactFlowStore = { height: number; transform: Transform; nodes: Node[]; + nodesLookup: Map; edges: Edge[]; onNodesChange: OnNodesChange | null; onEdgesChange: OnEdgesChange | null; @@ -141,7 +142,7 @@ export type ReactFlowActions = { getNodes: () => Node[]; setEdges: (edges: Edge[]) => void; setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => void; - updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void; + updateNodeDimensions: (updates: Map) => void; updateNodePositions: UpdateNodePositions; resetSelectedElements: () => void; unselectNodesAndEdges: (params?: UnselectNodesAndEdgesParams) => void; diff --git a/packages/svelte/src/lib/actions/drag/index.ts b/packages/svelte/src/lib/actions/drag/index.ts index e936d3a5..9e719c32 100644 --- a/packages/svelte/src/lib/actions/drag/index.ts +++ b/packages/svelte/src/lib/actions/drag/index.ts @@ -30,6 +30,7 @@ export default function drag(domNode: Element, params: UseDragParams) { return { nodes: get(store.nodes), + nodesLookup: get(store.nodesLookup), edges: get(store.edges), nodeExtent: get(store.nodeExtent), snapGrid: snapGrid ? snapGrid : [0, 0], diff --git a/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte b/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte index c4f9ec6f..6a7acd90 100644 --- a/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte +++ b/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte @@ -17,11 +17,18 @@ typeof ResizeObserver === 'undefined' ? null : new ResizeObserver((entries: ResizeObserverEntry[]) => { - const updates = entries.map((entry: ResizeObserverEntry) => ({ - id: entry.target.getAttribute('data-id') as string, - nodeElement: entry.target as HTMLDivElement, - forceUpdate: true - })); + const updates = new Map(); + + entries.forEach((entry: ResizeObserverEntry) => { + const id = entry.target.getAttribute('data-id') as string; + + updates.set(id, { + id, + nodeElement: entry.target as HTMLDivElement, + forceUpdate: true + }); + }); + updateNodeDimensions(updates); }); diff --git a/packages/svelte/src/lib/store/derived-connection-props.ts b/packages/svelte/src/lib/store/derived-connection-props.ts index 5be1598c..173362ac 100644 --- a/packages/svelte/src/lib/store/derived-connection-props.ts +++ b/packages/svelte/src/lib/store/derived-connection-props.ts @@ -56,15 +56,15 @@ export function getDerivedConnectionProps( currentConnection, store.connectionLineType, store.connectionMode, - store.nodes, + store.nodesLookup, store.viewport ], - ([connection, connectionLineType, connectionMode, nodes, viewport]) => { + ([connection, connectionLineType, connectionMode, nodesLookup, viewport]) => { if (!connection.connectionStartHandle?.nodeId) { return initConnectionProps; } - const fromNode = nodes.find((n) => n.id === connection.connectionStartHandle?.nodeId); + const fromNode = nodesLookup.get(connection.connectionStartHandle?.nodeId); const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds; const handleBoundsStrict = fromHandleBounds?.[connection.connectionStartHandle.type || 'source'] || []; diff --git a/packages/svelte/src/lib/store/edge-tree.ts b/packages/svelte/src/lib/store/edge-tree.ts index ff099868..221563ef 100644 --- a/packages/svelte/src/lib/store/edge-tree.ts +++ b/packages/svelte/src/lib/store/edge-tree.ts @@ -9,17 +9,18 @@ export function getEdgeTree(store: SvelteFlowStoreState) { [ store.edges, store.nodes, + store.nodesLookup, store.onlyRenderVisibleElements, store.viewport, store.width, store.height ], - ([edges, nodes, onlyRenderVisibleElements, viewport, width, height]) => { + ([edges, , nodesLookup, onlyRenderVisibleElements, viewport, width, height]) => { const visibleEdges = onlyRenderVisibleElements && width && height ? edges.filter((edge) => { - const sourceNode = nodes.find((node) => node.id === edge.source); - const targetNode = nodes.find((node) => node.id === edge.target); + const sourceNode = nodesLookup.get(edge.source); + const targetNode = nodesLookup.get(edge.target); return ( sourceNode && @@ -40,11 +41,11 @@ export function getEdgeTree(store: SvelteFlowStoreState) { ); return derived( - [visibleEdges, store.nodes, store.connectionMode, store.onError], - ([visibleEdges, nodes, connectionMode, onError]) => { + [visibleEdges, store.nodes, store.nodesLookup, store.connectionMode, store.onError], + ([visibleEdges, , nodesLookup, connectionMode, onError]) => { const layoutedEdges = visibleEdges.reduce((res, edge) => { - const sourceNode = nodes.find((node) => node.id === edge.source); - const targetNode = nodes.find((node) => node.id === edge.target); + const sourceNode = nodesLookup.get(edge.source); + const targetNode = nodesLookup.get(edge.target); if (!sourceNode || !targetNode) { return res; @@ -70,7 +71,7 @@ export function getEdgeTree(store: SvelteFlowStoreState) { return res; }, []); - const groupedEdges = groupEdgesByZLevel(layoutedEdges, nodes, false); + const groupedEdges = groupEdgesByZLevel(layoutedEdges, nodesLookup, false); return groupedEdges; } diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index b78e6fb2..c9140805 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -86,10 +86,11 @@ export function createStore({ }); }; - function updateNodeDimensions(updates: NodeDimensionUpdate[]) { + function updateNodeDimensions(updates: Map) { const nextNodes = updateNodeDimensionsSystem( updates, get(store.nodes), + get(store.nodesLookup), get(store.domNode), get(store.nodeOrigin) ); diff --git a/packages/svelte/src/lib/store/initial-store.ts b/packages/svelte/src/lib/store/initial-store.ts index 7744ac0c..7cabef6e 100644 --- a/packages/svelte/src/lib/store/initial-store.ts +++ b/packages/svelte/src/lib/store/initial-store.ts @@ -59,7 +59,11 @@ export const getInitialStore = ({ height?: number; fitView?: boolean; }) => { - const nextNodes = updateNodes(nodes, [], { nodeOrigin: [0, 0], elevateNodesOnSelect: false }); + const nodesLookup = new Map(); + const nextNodes = updateNodes(nodes, nodesLookup, { + nodeOrigin: [0, 0], + elevateNodesOnSelect: false + }); let viewport: Viewport = { x: 0, y: 0, zoom: 1 }; @@ -75,7 +79,8 @@ export const getInitialStore = ({ return { flowId: writable(null), - nodes: createNodesStore(nextNodes), + nodes: createNodesStore(nextNodes, nodesLookup), + nodesLookup: readable>(nodesLookup), visibleNodes: readable([]), edges: createEdgesStore(edges), edgeTree: readable[]>([]), diff --git a/packages/svelte/src/lib/store/types.ts b/packages/svelte/src/lib/store/types.ts index ae854cfd..8d41d63f 100644 --- a/packages/svelte/src/lib/store/types.ts +++ b/packages/svelte/src/lib/store/types.ts @@ -27,7 +27,7 @@ export type SvelteFlowStoreActions = { setTranslateExtent: (extent: CoordinateExtent) => void; fitView: (options?: FitViewOptions) => boolean; updateNodePositions: UpdateNodePositions; - updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void; + updateNodeDimensions: (updates: Map) => void; unselectNodesAndEdges: (params?: { nodes?: Node[]; edges?: Edge[] }) => void; addSelectedNodes: (ids: string[]) => void; addSelectedEdges: (ids: string[]) => void; diff --git a/packages/svelte/src/lib/store/utils.ts b/packages/svelte/src/lib/store/utils.ts index cfd5b343..82f8afc6 100644 --- a/packages/svelte/src/lib/store/utils.ts +++ b/packages/svelte/src/lib/store/utils.ts @@ -111,7 +111,8 @@ export type NodeStoreOptions = { // we are creating a custom store for the internals nodes in order to update the zIndex and positionAbsolute. // The user only passes in relative positions, so we need to calculate the absolute positions based on the parent nodes. export const createNodesStore = ( - nodes: Node[] + nodes: Node[], + nodesLookup: Map ): { subscribe: (this: void, run: Subscriber) => Unsubscriber; update: (this: void, updater: Updater) => void; @@ -125,7 +126,7 @@ export const createNodesStore = ( let elevateNodesOnSelect = true; const _set = (nds: Node[]): Node[] => { - const nextNodes = updateNodes(nds, value, { + const nextNodes = updateNodes(nds, nodesLookup, { elevateNodesOnSelect, defaults }); diff --git a/packages/system/src/utils/edges/general.ts b/packages/system/src/utils/edges/general.ts index e15d9269..5d4c3151 100644 --- a/packages/system/src/utils/edges/general.ts +++ b/packages/system/src/utils/edges/general.ts @@ -33,7 +33,7 @@ export type GroupedEdges = { export function groupEdgesByZLevel( edges: EdgeType[], - nodes: NodeBase[], + nodesLookup: Map, elevateEdgesOnSelect = false ): GroupedEdges[] { let maxLevel = -1; @@ -43,8 +43,8 @@ export function groupEdgesByZLevel( let z = hasZIndex ? edge.zIndex! : 0; if (elevateEdgesOnSelect) { - const targetNode = nodes.find((n) => n.id === edge.target); - const sourceNode = nodes.find((n) => n.id === edge.source); + const targetNode = nodesLookup.get(edge.target); + const sourceNode = nodesLookup.get(edge.source); const edgeOrConnectedNodeSelected = edge.selected || targetNode?.selected || sourceNode?.selected; const selectedZIndex = Math.max( sourceNode?.[internalsSymbol]?.z || 0, diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index 3fee5d02..ff168d43 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -18,19 +18,21 @@ type ParentNodes = Record; export function updateAbsolutePositions( nodes: NodeType[], + nodesLookup: Map, nodeOrigin: NodeOrigin = [0, 0], parentNodes?: ParentNodes ) { return nodes.map((node) => { - if (node.parentNode && !nodes.find((n) => n.id === node.parentNode)) { + if (node.parentNode && !nodesLookup.has(node.parentNode)) { throw new Error(`Parent node ${node.parentNode} not found`); } if (node.parentNode || parentNodes?.[node.id]) { - const parentNode = node.parentNode ? nodes.find((n) => n.id === node.parentNode) : null; + const parentNode = node.parentNode ? nodesLookup.get(node.parentNode) : null; const { x, y, z } = calculateXYZPosition( node, nodes, + nodesLookup, { ...node.position, z: node[internalsSymbol]?.z ?? 0, @@ -62,7 +64,7 @@ type UpdateNodesOptions = { export function updateNodes( nodes: NodeType[], - storeNodes: NodeType[], + nodesLookup: Map, options: UpdateNodesOptions = { nodeOrigin: [0, 0] as NodeOrigin, elevateNodesOnSelect: true, @@ -73,7 +75,7 @@ export function updateNodes( const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0; const nextNodes = nodes.map((n) => { - const currentStoreNode = storeNodes.find((storeNode) => n.id === storeNode.id); + const currentStoreNode = nodesLookup.get(n.id); const node: NodeType = { ...options.defaults, ...n, @@ -96,10 +98,12 @@ export function updateNodes( }, }); + nodesLookup.set(node.id, node); + return node; }); - const nodesWithPositions = updateAbsolutePositions(nextNodes, options.nodeOrigin, parentNodes); + const nodesWithPositions = updateAbsolutePositions(nextNodes, nodesLookup, options.nodeOrigin, parentNodes); return nodesWithPositions; } @@ -107,6 +111,7 @@ export function updateNodes( function calculateXYZPosition( node: NodeType, nodes: NodeType[], + nodesLookup: Map, result: XYZPosition, nodeOrigin: NodeOrigin ): XYZPosition { @@ -114,12 +119,13 @@ function calculateXYZPosition( return result; } - const parentNode = nodes.find((n) => n.id === node.parentNode)!; + const parentNode = nodesLookup.get(node.parentNode)!; const parentNodePosition = getNodePositionWithOrigin(parentNode, parentNode?.origin || nodeOrigin); return calculateXYZPosition( parentNode, nodes, + nodesLookup, { x: (result.x ?? 0) + parentNodePosition.x, y: (result.y ?? 0) + parentNodePosition.y, @@ -130,8 +136,9 @@ function calculateXYZPosition( } export function updateNodeDimensions( - updates: NodeDimensionUpdate[], + updates: Map, nodes: NodeBase[], + nodesLookup: Map, domNode: HTMLElement | null, nodeOrigin?: NodeOrigin, onUpdate?: (id: string, dimensions: Dimensions) => void @@ -146,7 +153,8 @@ export function updateNodeDimensions( const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform); const nextNodes = nodes.map((node) => { - const update = updates.find((u) => u.id === node.id); + const update = updates.get(node.id); + if (update) { const dimensions = getDimensions(update.nodeElement); const doUpdate = !!( @@ -158,7 +166,7 @@ export function updateNodeDimensions( if (doUpdate) { onUpdate?.(node.id, dimensions); - return { + const newNode = { ...node, ...dimensions, [internalsSymbol]: { @@ -169,6 +177,10 @@ export function updateNodeDimensions( }, }, }; + + nodesLookup.set(node.id, newNode); + + return newNode; } } diff --git a/packages/system/src/xydrag/XYDrag.ts b/packages/system/src/xydrag/XYDrag.ts index 9271cb9d..66637c70 100644 --- a/packages/system/src/xydrag/XYDrag.ts +++ b/packages/system/src/xydrag/XYDrag.ts @@ -33,6 +33,7 @@ export type OnDrag = (event: MouseEvent, dragItems: NodeDragItem[], node: NodeBa type StoreItems = { nodes: NodeBase[]; + nodesLookup: Map; edges: EdgeBase[]; nodeExtent: CoordinateExtent; snapGrid: SnapGrid; @@ -103,6 +104,7 @@ export function XYDrag({ function updateNodes({ x, y }: XYPosition) { const { nodes, + nodesLookup, nodeExtent, snapGrid, snapToGrid, @@ -163,11 +165,11 @@ export function XYDrag({ updateNodePositions(dragItems, true, true); const onNodeOrSelectionDrag = nodeId ? onNodeDrag : wrapSelectionDragFunc(onSelectionDrag); - if (dragEvent) { + if (dragEvent && (onDrag || onNodeOrSelectionDrag)) { const [currentNode, currentNodes] = getEventHandlerParams({ nodeId, dragItems, - nodes, + nodesLookup, }); onDrag?.(dragEvent as MouseEvent, dragItems, currentNode, currentNodes); onNodeOrSelectionDrag?.(dragEvent as MouseEvent, currentNode, currentNodes); @@ -197,6 +199,7 @@ export function XYDrag({ function startDrag(event: UseDragEvent) { const { nodes, + nodesLookup, multiSelectionActive, nodesDraggable, transform, @@ -227,11 +230,11 @@ export function XYDrag({ const onNodeOrSelectionDragStart = nodeId ? onNodeDragStart : wrapSelectionDragFunc(onSelectionDragStart); - if (dragItems) { + if (dragItems && (onDragStart || onNodeOrSelectionDragStart)) { const [currentNode, currentNodes] = getEventHandlerParams({ nodeId, dragItems, - nodes, + nodesLookup, }); onDragStart?.(event.sourceEvent as MouseEvent, dragItems, currentNode, currentNodes); onNodeOrSelectionDragStart?.(event.sourceEvent as MouseEvent, currentNode, currentNodes); @@ -288,18 +291,20 @@ export function XYDrag({ cancelAnimationFrame(autoPanId); if (dragItems) { - const { nodes, updateNodePositions, onNodeDragStop, onSelectionDragStop } = getStoreItems(); + const { nodesLookup, updateNodePositions, onNodeDragStop, onSelectionDragStop } = getStoreItems(); const onNodeOrSelectionDragStop = nodeId ? onNodeDragStop : wrapSelectionDragFunc(onSelectionDragStop); updateNodePositions(dragItems, false, false); - const [currentNode, currentNodes] = getEventHandlerParams({ - nodeId, - dragItems, - nodes, - }); - onDragStop?.(event.sourceEvent as MouseEvent, dragItems, currentNode, currentNodes); - onNodeOrSelectionDragStop?.(event.sourceEvent as MouseEvent, currentNode, currentNodes); + if (onDragStop || onNodeOrSelectionDragStop) { + const [currentNode, currentNodes] = getEventHandlerParams({ + nodeId, + dragItems, + nodesLookup, + }); + onDragStop?.(event.sourceEvent as MouseEvent, dragItems, currentNode, currentNodes); + onNodeOrSelectionDragStop?.(event.sourceEvent as MouseEvent, currentNode, currentNodes); + } } }) .filter((event: MouseEvent) => { diff --git a/packages/system/src/xydrag/utils.ts b/packages/system/src/xydrag/utils.ts index fbf0e8f9..d8bfdb59 100644 --- a/packages/system/src/xydrag/utils.ts +++ b/packages/system/src/xydrag/utils.ts @@ -75,14 +75,14 @@ export function getDragItems( export function getEventHandlerParams({ nodeId, dragItems, - nodes, + nodesLookup, }: { nodeId?: string; dragItems: NodeDragItem[]; - nodes: NodeType[]; + nodesLookup: Map; }): [NodeType, NodeType[]] { const extentedDragItems: NodeType[] = dragItems.map((n) => { - const node = nodes.find((node) => node.id === n.id)!; + const node = nodesLookup.get(n.id)!; return { ...node, From 0d9ac928ff7cecde276368800427d2d2a39594c8 Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 14 Nov 2023 11:03:46 +0100 Subject: [PATCH 02/23] chore(documentation) Added additional READMEs for examples --- examples/README.md | 13 +++++++++++++ examples/react/README.md | 12 +++++++++++- examples/svelte/README.md | 12 +++++++++++- 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 examples/README.md diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..abce87a2 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,13 @@ +# Examples for Testing & Development +These examples are used for feature development and E2E testing. + +## Overview +SvelteKit App with Svelte Flow examples at [examples/svelte](./svelte) + +Vite App with React Flow examples at [examples/react](./react) + +Astro App used for SSR testing at [examples/astro-xyflow](./astro-xyflow) + +## E2E + +For furhter documentation of E2E tests have a look at [/tests/playwright](/tests/playwright). \ No newline at end of file diff --git a/examples/react/README.md b/examples/react/README.md index 5a9a880c..eb7477db 100644 --- a/examples/react/README.md +++ b/examples/react/README.md @@ -1,6 +1,6 @@ # React Flow examples -This Vite app is used internally to test the library. +This Vite app is used internally to develop and test the library. ## Start local dev server @@ -8,5 +8,15 @@ This Vite app is used internally to test the library. pnpm dev ``` +## Adding new example + +Development of the library is done against `src/examples`. Feel free to add new implementations for features that you develop. + +1. Create a new folder & flow at `src/examples/` +2. Register the new route in `src/App/routes.ts` + +## Adding new E2E implementation +E2E testing is done against the flows implemented in `src/generic-tests`. Adding a new configuration file automatically adds a new route under http://localhost:3000/tests/generic/$foldername/$filename. For further documentation visit [`xyflow/tests/playwright`](/tests/playwright). + diff --git a/examples/svelte/README.md b/examples/svelte/README.md index 72cd559c..b373baea 100644 --- a/examples/svelte/README.md +++ b/examples/svelte/README.md @@ -1,4 +1,4 @@ -# Svelte Flow examples +# Svelte Flow examples This Vite app is used internally to test the library. @@ -8,3 +8,13 @@ This Vite app is used internally to test the library. pnpm dev ``` +## Adding new example + +Development of the library is done against `src/routes/examples`. Feel free to add new implementations for features that you develop. + +1. Create a new folder & flow at `src/routes/examples/` +2. Register the new route in `src/components/Header/Header.svelte` + +## Adding new E2E implementation + +E2E testing is done against the flows implemented in `src/generic-tests`. Adding a new configuration file automatically adds a new route under http://localhost:5173/tests/generic/$foldername/$filename. For further documentation visit [`xyflow/tests/playwright`](/tests/playwright). From 3dad5fe9fae8a30a75c8bd198b5ad5f0908d68d0 Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 14 Nov 2023 11:07:34 +0100 Subject: [PATCH 03/23] chore(documentation) small improvements --- examples/README.md | 8 ++++---- examples/react/README.md | 2 +- examples/svelte/README.md | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/README.md b/examples/README.md index abce87a2..df8f1670 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,12 +2,12 @@ These examples are used for feature development and E2E testing. ## Overview -SvelteKit App with Svelte Flow examples at [examples/svelte](./svelte) +SvelteKit App with Svelte Flow examples at [`examples/svelte`](./svelte) -Vite App with React Flow examples at [examples/react](./react) +Vite App with React Flow examples at [`examples/react`](./react) -Astro App used for SSR testing at [examples/astro-xyflow](./astro-xyflow) +Astro App used for SSR testing at [`examples/astro-xyflow`](./astro-xyflow) ## E2E -For furhter documentation of E2E tests have a look at [/tests/playwright](/tests/playwright). \ No newline at end of file +For furhter documentation of E2E tests have a look at [`/tests/playwright`](/tests/playwright). \ No newline at end of file diff --git a/examples/react/README.md b/examples/react/README.md index eb7477db..c1207ba7 100644 --- a/examples/react/README.md +++ b/examples/react/README.md @@ -16,7 +16,7 @@ Development of the library is done against `src/examples`. Feel free to add new 2. Register the new route in `src/App/routes.ts` ## Adding new E2E implementation -E2E testing is done against the flows implemented in `src/generic-tests`. Adding a new configuration file automatically adds a new route under http://localhost:3000/tests/generic/$foldername/$filename. For further documentation visit [`xyflow/tests/playwright`](/tests/playwright). +E2E testing is done against the flows implemented in `src/generic-tests`. Adding a new configuration file automatically adds a new route under [`http://localhost:3000/tests/generic/$foldername/$filename`](http://localhost:5173/tests/generic/nodes/general). For further documentation visit [`xyflow/tests/playwright`](/tests/playwright). diff --git a/examples/svelte/README.md b/examples/svelte/README.md index b373baea..b596832d 100644 --- a/examples/svelte/README.md +++ b/examples/svelte/README.md @@ -17,4 +17,4 @@ Development of the library is done against `src/routes/examples`. Feel free to a ## Adding new E2E implementation -E2E testing is done against the flows implemented in `src/generic-tests`. Adding a new configuration file automatically adds a new route under http://localhost:5173/tests/generic/$foldername/$filename. For further documentation visit [`xyflow/tests/playwright`](/tests/playwright). +E2E testing is done against the flows implemented in `src/generic-tests`. Adding a new configuration file automatically adds a new route under [`http://localhost:5173/tests/generic/$foldername/$filename`](http://localhost:5173/tests/generic/nodes/general). For further documentation visit [`xyflow/tests/playwright`](/tests/playwright). From e798e94275e97bef6a30f368e1e8586e45a2d556 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 14 Nov 2023 15:52:48 +0100 Subject: [PATCH 04/23] refactor(react/svelte): use nodeLookup --- examples/react/src/examples/Stress/index.tsx | 8 +--- .../additional-components/MiniMap/MiniMap.tsx | 2 +- .../NodeResizer/ResizeControl.tsx | 8 ++-- .../NodeToolbar/NodeToolbar.tsx | 4 +- .../src/components/ConnectionLine/index.tsx | 2 +- .../react/src/components/Edges/BaseEdge.tsx | 1 - .../react/src/components/Edges/wrapEdge.tsx | 42 ++++++++++--------- .../react/src/components/Handle/index.tsx | 5 +++ packages/react/src/components/Nodes/utils.ts | 6 +-- .../react/src/components/Nodes/wrapNode.tsx | 2 +- .../components/SelectionListener/index.tsx | 8 +++- .../src/components/StoreUpdater/index.tsx | 9 +++- .../src/container/EdgeRenderer/index.tsx | 6 ++- packages/react/src/hooks/useReactFlow.ts | 4 +- .../react/src/hooks/useUpdateNodeInternals.ts | 11 +++-- packages/react/src/hooks/useVisibleEdges.ts | 6 +-- packages/react/src/store/index.ts | 30 ++++++++----- packages/react/src/store/initialState.ts | 6 +-- packages/react/src/types/store.ts | 3 +- packages/react/src/utils/changes.ts | 21 ++++++++-- packages/svelte/src/lib/actions/drag/index.ts | 2 +- .../lib/components/BaseEdge/BaseEdge.svelte | 1 + .../src/lib/hooks/useUpdateNodeInternals.ts | 10 ++--- .../src/lib/store/derived-connection-props.ts | 6 +-- packages/svelte/src/lib/store/edge-tree.ts | 18 ++++---- packages/svelte/src/lib/store/index.ts | 2 +- .../svelte/src/lib/store/initial-store.ts | 8 ++-- packages/svelte/src/lib/store/utils.ts | 4 +- packages/system/src/utils/dom.ts | 4 +- packages/system/src/utils/edges/general.ts | 6 +-- packages/system/src/utils/store.ts | 26 ++++++------ packages/system/src/xydrag/XYDrag.ts | 16 +++---- packages/system/src/xydrag/utils.ts | 6 +-- 33 files changed, 166 insertions(+), 127 deletions(-) diff --git a/examples/react/src/examples/Stress/index.tsx b/examples/react/src/examples/Stress/index.tsx index 7e83fac7..c1c24d86 100644 --- a/examples/react/src/examples/Stress/index.tsx +++ b/examples/react/src/examples/Stress/index.tsx @@ -1,7 +1,6 @@ import { useState, useCallback } from 'react'; import { ReactFlow, - ReactFlowInstance, Edge, Node, NodeChange, @@ -18,11 +17,6 @@ import { import { getNodesAndEdges } from './utils'; -const onInit = (reactFlowInstance: ReactFlowInstance) => { - reactFlowInstance.fitView(); - console.log(reactFlowInstance.getNodes()); -}; - const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(25, 25); const StressFlow = () => { @@ -64,11 +58,11 @@ const StressFlow = () => { diff --git a/packages/react/src/additional-components/MiniMap/MiniMap.tsx b/packages/react/src/additional-components/MiniMap/MiniMap.tsx index 49e02573..f56ae57f 100644 --- a/packages/react/src/additional-components/MiniMap/MiniMap.tsx +++ b/packages/react/src/additional-components/MiniMap/MiniMap.tsx @@ -118,7 +118,7 @@ function MiniMap({ const onSvgNodeClick = onNodeClick ? useCallback((event: MouseEvent, nodeId: string) => { - const node = store.getState().nodes.find((n) => n.id === nodeId)!; + const node = store.getState().nodeLookup.get(nodeId)!; onNodeClick(event, node); }, []) : undefined; diff --git a/packages/react/src/additional-components/NodeResizer/ResizeControl.tsx b/packages/react/src/additional-components/NodeResizer/ResizeControl.tsx index 74028658..36783fcf 100644 --- a/packages/react/src/additional-components/NodeResizer/ResizeControl.tsx +++ b/packages/react/src/additional-components/NodeResizer/ResizeControl.tsx @@ -65,8 +65,8 @@ function ResizeControl({ const dragHandler = drag() .on('start', (event: ResizeDragEvent) => { - const { nodesLookup, transform, snapGrid, snapToGrid } = store.getState(); - const node = nodesLookup.get(id); + const { nodeLookup, transform, snapGrid, snapToGrid } = store.getState(); + const node = nodeLookup.get(id); const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); prevValues.current = { @@ -86,9 +86,9 @@ function ResizeControl({ onResizeStart?.(event, { ...prevValues.current }); }) .on('drag', (event: ResizeDragEvent) => { - const { nodesLookup, transform, snapGrid, snapToGrid, triggerNodeChanges } = store.getState(); + const { nodeLookup, transform, snapGrid, snapToGrid, triggerNodeChanges } = store.getState(); const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); - const node = nodesLookup.get(id); + const node = nodeLookup.get(id); if (node) { const changes: NodeChange[] = []; diff --git a/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx b/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx index fc0c309f..ad581bcf 100644 --- a/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx +++ b/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx @@ -87,12 +87,12 @@ function NodeToolbar({ const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId || '']; return nodeIds.reduce((acc, id) => { - const node = state.nodes.find((n) => n.id === id); + const node = state.nodeLookup.get(id); if (node) { acc.push(node); } return acc; - }, [] as Node[]); + }, []); }, [nodeId, contextNodeId] ); diff --git a/packages/react/src/components/ConnectionLine/index.tsx b/packages/react/src/components/ConnectionLine/index.tsx index b40efa65..ed1a2115 100644 --- a/packages/react/src/components/ConnectionLine/index.tsx +++ b/packages/react/src/components/ConnectionLine/index.tsx @@ -43,7 +43,7 @@ const ConnectionLine = ({ const { fromNode, handleId, toX, toY, connectionMode } = useStore( useCallback( (s: ReactFlowStore) => ({ - fromNode: s.nodesLookup.get(nodeId), + fromNode: s.nodeLookup.get(nodeId), handleId: s.connectionStartHandle?.handleId, toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2], toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2], diff --git a/packages/react/src/components/Edges/BaseEdge.tsx b/packages/react/src/components/Edges/BaseEdge.tsx index dd496f7e..91cfd58a 100644 --- a/packages/react/src/components/Edges/BaseEdge.tsx +++ b/packages/react/src/components/Edges/BaseEdge.tsx @@ -1,7 +1,6 @@ import { isNumeric } from '@xyflow/system'; import type { BaseEdgeProps } from '../../types'; - import EdgeText from './EdgeText'; const BaseEdge = ({ diff --git a/packages/react/src/components/Edges/wrapEdge.tsx b/packages/react/src/components/Edges/wrapEdge.tsx index 4ab6fa45..b0a876b0 100644 --- a/packages/react/src/components/Edges/wrapEdge.tsx +++ b/packages/react/src/components/Edges/wrapEdge.tsx @@ -1,4 +1,4 @@ -import { memo, useState, useMemo, useRef, type ComponentType, type KeyboardEvent } from 'react'; +import { memo, useState, useMemo, useRef, type ComponentType, type KeyboardEvent, useCallback } from 'react'; import cc from 'classcat'; import { shallow } from 'zustand/shallow'; import { getMarkerId, elementSelectionKeys, XYHandle, type Connection, getEdgePosition } from '@xyflow/system'; @@ -53,26 +53,30 @@ export default (EdgeComponent: ComponentType) => { const [updateHover, setUpdateHover] = useState(false); const [updating, setUpdating] = useState(false); const store = useStoreApi(); - const edgePosition = useStore(function edgeSelector(state) { - const sourceNode = state.nodesLookup.get(source); - const targetNode = state.nodesLookup.get(target); + const edgePosition = useStore( + useCallback( + (state) => { + const sourceNode = state.nodeLookup.get(source); + const targetNode = state.nodeLookup.get(target); - if (!sourceNode || !targetNode) { - return null; - } + if (!sourceNode || !targetNode) { + return null; + } - const pos = getEdgePosition({ - id, - sourceNode, - targetNode, - sourceHandle: sourceHandleId || null, - targetHandle: targetHandleId || null, - connectionMode: state.connectionMode, - onError: state.onError, - }); - - return pos; - }, shallow); + return getEdgePosition({ + id, + sourceNode, + targetNode, + sourceHandle: sourceHandleId || null, + targetHandle: targetHandleId || null, + connectionMode: state.connectionMode, + onError: state.onError, + }); + }, + [source, target] + ), + shallow + ); const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart, rfId)})`, [markerStart, rfId]); const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd, rfId)})`, [markerEnd, rfId]); diff --git a/packages/react/src/components/Handle/index.tsx b/packages/react/src/components/Handle/index.tsx index 925bc93e..29231b71 100644 --- a/packages/react/src/components/Handle/index.tsx +++ b/packages/react/src/components/Handle/index.tsx @@ -1,3 +1,8 @@ +/* + * The Handle component is used to connect nodes. When the user mousedowns a handle, we start the connection process. + * The user can then drag the connection to another handle or node. When the user releases the mouse, we check if the + * connection is valid and if so, we call the onConnect callback. + */ import { memo, HTMLAttributes, forwardRef, MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from 'react'; import cc from 'classcat'; import { shallow } from 'zustand/shallow'; diff --git a/packages/react/src/components/Nodes/utils.ts b/packages/react/src/components/Nodes/utils.ts index a70ccd34..b9a82189 100644 --- a/packages/react/src/components/Nodes/utils.ts +++ b/packages/react/src/components/Nodes/utils.ts @@ -12,7 +12,7 @@ export function getMouseHandler( return handler === undefined ? handler : (event: MouseEvent) => { - const node = getState().nodes.find((n) => n.id === id)!; + const node = getState().nodeLookup.get(id)!; handler(event, { ...node }); }; } @@ -35,8 +35,8 @@ export function handleNodeClick({ unselect?: boolean; nodeRef?: RefObject; }) { - const { addSelectedNodes, unselectNodesAndEdges, multiSelectionActive, nodes, onError } = store.getState(); - const node = nodes.find((n) => n.id === id)!; + const { addSelectedNodes, unselectNodesAndEdges, multiSelectionActive, nodeLookup, onError } = store.getState(); + const node = nodeLookup.get(id); if (!node) { onError?.('012', errorMessages['error012'](id)); diff --git a/packages/react/src/components/Nodes/wrapNode.tsx b/packages/react/src/components/Nodes/wrapNode.tsx index f96361ce..e50ffcf7 100644 --- a/packages/react/src/components/Nodes/wrapNode.tsx +++ b/packages/react/src/components/Nodes/wrapNode.tsx @@ -146,7 +146,7 @@ export default (NodeComponent: ComponentType) => { if (targetPosChanged) { prevTargetPosition.current = targetPosition; } - store.getState().updateNodeDimensions([{ id, nodeElement: nodeRef.current, forceUpdate: true }]); + store.getState().updateNodeDimensions(new Map([[id, { id, nodeElement: nodeRef.current, forceUpdate: true }]])); } }, [id, type, sourcePosition, targetPosition]); diff --git a/packages/react/src/components/SelectionListener/index.tsx b/packages/react/src/components/SelectionListener/index.tsx index b71231f6..4ef1e130 100644 --- a/packages/react/src/components/SelectionListener/index.tsx +++ b/packages/react/src/components/SelectionListener/index.tsx @@ -1,3 +1,9 @@ +/* + * This is a helper component for calling the onSelectionChange listener. + * It will only be mounted if the user has passed an onSelectionChange listener + * or is using the useOnSelectionChange hook. + * @TODO: Now that we have the onNodesChange and on EdgesChange listeners, do we still need this component? + */ import { memo, useEffect } from 'react'; import { shallow } from 'zustand/shallow'; @@ -24,8 +30,6 @@ function areEqual(a: SelectorSlice, b: SelectorSlice) { ); } -// This is just a helper component for calling the onSelectionChange listener. -// @TODO: Now that we have the onNodesChange and on EdgesChange listeners, do we still need this component? const SelectionListener = memo(({ onSelectionChange }: SelectionListenerProps) => { const store = useStoreApi(); const { selectedNodes, selectedEdges } = useStore(selector, areEqual); diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx index a0ce26dd..05a8aa50 100644 --- a/packages/react/src/components/StoreUpdater/index.tsx +++ b/packages/react/src/components/StoreUpdater/index.tsx @@ -1,3 +1,8 @@ +/* + * This component helps us to update the store with the vlues coming from the user. + * We distinguish between values we can update directly with `useDirectStoreUpdater` (like `snapGrid`) + * and values that have a dedicated setter function in the store (like `setNodes`). + */ import { useEffect } from 'react'; import { StoreApi } from 'zustand'; import { shallow } from 'zustand/shallow'; @@ -70,10 +75,10 @@ const selector = (s: ReactFlowState) => ({ reset: s.reset, }); -function useStoreUpdater(value: T | undefined, setStoreState: (param: T) => void) { +function useStoreUpdater(value: T | undefined, setStoreAction: (param: T) => void) { useEffect(() => { if (typeof value !== 'undefined') { - setStoreState(value); + setStoreAction(value); } }, [value]); } diff --git a/packages/react/src/container/EdgeRenderer/index.tsx b/packages/react/src/container/EdgeRenderer/index.tsx index fe2de112..24825fe6 100644 --- a/packages/react/src/container/EdgeRenderer/index.tsx +++ b/packages/react/src/container/EdgeRenderer/index.tsx @@ -63,6 +63,8 @@ const EdgeRenderer = ({ children, }: EdgeRendererProps) => { const { edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow); + // we are grouping edges by zIndex here in order to be able to render them in the correct order + // each zIndex gets its own svg element const edgeTree = useVisibleEdges(onlyRenderVisibleElements, elevateEdgesOnSelect); return ( @@ -70,7 +72,7 @@ const EdgeRenderer = ({ {edgeTree.map(({ level, edges, isMaxLevel }) => ( {isMaxLevel && } - + <> {edges.map((edge) => { let edgeType = edge.type || 'default'; @@ -132,7 +134,7 @@ const EdgeRenderer = ({ /> ); })} - + ))} {children} diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index 301f49f5..e72c3a4f 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -35,7 +35,7 @@ export default function useReactFlow(): ReactFlo }, []); const getNode = useCallback>((id) => { - return store.getState().nodes.find((n) => n.id === id); + return store.getState().nodeLookup.get(id); }, []); const getEdges = useCallback>(() => { @@ -181,7 +181,7 @@ export default function useReactFlow(): ReactFlo nodeOrRect: Node | { id: Node['id'] } | Rect ): [Rect | null, Node | null | undefined, boolean] => { const isRect = isRectObject(nodeOrRect); - const node = isRect ? null : store.getState().nodes.find((n) => n.id === nodeOrRect.id); + const node = isRect ? null : store.getState().nodeLookup.get(nodeOrRect.id); if (!isRect && !node) { [null, null, isRect]; diff --git a/packages/react/src/hooks/useUpdateNodeInternals.ts b/packages/react/src/hooks/useUpdateNodeInternals.ts index 9b4bcf61..d14f35f3 100644 --- a/packages/react/src/hooks/useUpdateNodeInternals.ts +++ b/packages/react/src/hooks/useUpdateNodeInternals.ts @@ -8,17 +8,16 @@ function useUpdateNodeInternals(): UpdateNodeInternals { return useCallback((id: string | string[]) => { const { domNode, updateNodeDimensions } = store.getState(); - const updateIds = Array.isArray(id) ? id : [id]; - const updates = updateIds.reduce((res, updateId) => { + const updates = new Map(); + + updateIds.forEach((updateId) => { const nodeElement = domNode?.querySelector(`.react-flow__node[data-id="${updateId}"]`) as HTMLDivElement; if (nodeElement) { - res.push({ id: updateId, nodeElement, forceUpdate: true }); + updates.set(updateId, { id: updateId, nodeElement, forceUpdate: true }); } - - return res; - }, []); + }); requestAnimationFrame(() => updateNodeDimensions(updates)); }, []); diff --git a/packages/react/src/hooks/useVisibleEdges.ts b/packages/react/src/hooks/useVisibleEdges.ts index e97fe105..67d02c92 100644 --- a/packages/react/src/hooks/useVisibleEdges.ts +++ b/packages/react/src/hooks/useVisibleEdges.ts @@ -12,8 +12,8 @@ function useVisibleEdges(onlyRenderVisible: boolean, elevateEdgesOnSelect: boole const visibleEdges = onlyRenderVisible && s.width && s.height ? s.edges.filter((e) => { - const sourceNode = s.nodesLookup.get(e.source); - const targetNode = s.nodesLookup.get(e.target); + const sourceNode = s.nodeLookup.get(e.source); + const targetNode = s.nodeLookup.get(e.target); return ( sourceNode && @@ -29,7 +29,7 @@ function useVisibleEdges(onlyRenderVisible: boolean, elevateEdgesOnSelect: boole }) : s.edges; - return groupEdgesByZLevel(visibleEdges, s.nodesLookup, elevateEdgesOnSelect); + return groupEdgesByZLevel(visibleEdges, s.nodeLookup, elevateEdgesOnSelect); }, [onlyRenderVisible, elevateEdgesOnSelect] ), diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 6f8ca67b..a6898f6d 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -41,18 +41,19 @@ const createRFStore = ({ (set, get) => ({ ...getInitialState({ nodes, edges, width, height, fitView }), setNodes: (nodes: Node[]) => { - const { nodesLookup, nodeOrigin, elevateNodesOnSelect } = get(); - const nextNodes = updateNodes(nodes, nodesLookup, { nodeOrigin, elevateNodesOnSelect }); + const { nodeLookup, nodeOrigin, elevateNodesOnSelect } = get(); + // Whenver new nodes are set, we need to calculate the absolute positions of the nodes + // and update the nodeLookup. + const nextNodes = updateNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect }); set({ nodes: nextNodes }); }, - getNodes: () => { - return get().nodes; - }, setEdges: (edges: Edge[]) => { const { defaultEdgeOptions = {} } = get(); set({ edges: edges.map((e) => ({ ...defaultEdgeOptions, ...e })) }); }, + // 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'; @@ -79,12 +80,15 @@ const createRFStore = ({ 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. updateNodeDimensions: (updates) => { const { onNodesChange, fitView, nodes, - nodesLookup, + nodeLookup, fitViewOnInit, fitViewDone, fitViewOnInitOptions, @@ -96,7 +100,7 @@ const createRFStore = ({ const updatedNodes = updateNodeDimensionsSystem( updates, nodes, - nodesLookup, + nodeLookup, domNode, nodeOrigin, (id: string, dimensions: Dimensions) => { @@ -112,8 +116,9 @@ const createRFStore = ({ return; } - const nextNodes = updateAbsolutePositions(updatedNodes, nodesLookup, nodeOrigin); + const nextNodes = updateAbsolutePositions(updatedNodes, nodeLookup, nodeOrigin); + // we call fitView once initially after all dimensions are set let nextFitViewDone = fitViewDone; if (!fitViewDone && fitViewOnInit) { nextFitViewDone = fitView(nextNodes, { @@ -122,6 +127,11 @@ const createRFStore = ({ }); } + // here we are cirmumventing the onNodesChange handler + // in order to be able to display nodes even if the user + // has not provided an onNodesChange handler. + // Nodes are only rendered if they have a width and height + // attribute which they get from this handler. set({ nodes: nextNodes, fitViewDone: nextFitViewDone }); if (changes?.length > 0) { @@ -148,12 +158,12 @@ const createRFStore = ({ }, triggerNodeChanges: (changes) => { - const { onNodesChange, nodesLookup, nodes, hasDefaultNodes, nodeOrigin, elevateNodesOnSelect } = get(); + const { onNodesChange, nodeLookup, nodes, hasDefaultNodes, nodeOrigin, elevateNodesOnSelect } = get(); if (changes?.length) { if (hasDefaultNodes) { const updatedNodes = applyNodeChanges(changes, nodes); - const nextNodes = updateNodes(updatedNodes, nodesLookup, { + const nextNodes = updateNodes(updatedNodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect, }); diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index 633d9aa5..c87c29f1 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -22,8 +22,8 @@ const getInitialState = ({ height?: number; fitView?: boolean; } = {}): ReactFlowStore => { - const nodesLookup = new Map(); - const nextNodes = updateNodes(nodes, nodesLookup, { nodeOrigin: [0, 0], elevateNodesOnSelect: false }); + const nodeLookup = new Map(); + const nextNodes = updateNodes(nodes, nodeLookup, { nodeOrigin: [0, 0], elevateNodesOnSelect: false }); let transform: Transform = [0, 0, 1]; @@ -44,7 +44,7 @@ const getInitialState = ({ height: 0, transform, nodes: nextNodes, - nodesLookup, + nodeLookup, edges: edges, onNodesChange: null, onEdgesChange: null, diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index 3995d208..02a7d162 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -46,7 +46,7 @@ export type ReactFlowStore = { height: number; transform: Transform; nodes: Node[]; - nodesLookup: Map; + nodeLookup: Map; edges: Edge[]; onNodesChange: OnNodesChange | null; onEdgesChange: OnEdgesChange | null; @@ -139,7 +139,6 @@ export type ReactFlowStore = { export type ReactFlowActions = { setNodes: (nodes: Node[]) => void; - getNodes: () => Node[]; setEdges: (edges: Edge[]) => void; setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => void; updateNodeDimensions: (updates: Map) => void; diff --git a/packages/react/src/utils/changes.ts b/packages/react/src/utils/changes.ts index a51eaba4..61ff2b19 100644 --- a/packages/react/src/utils/changes.ts +++ b/packages/react/src/utils/changes.ts @@ -42,24 +42,39 @@ export function handleParentExpand(res: any[], updateItem: any) { } } +// This function applies changes to nodes or edges that are triggered by React Flow internally. +// When you drag a node for example, React Flow will send a position change update. +// This function then applies the changes and returns the updated elements. function applyChanges(changes: any[], elements: any[]): any[] { // we need this hack to handle the setNodes and setEdges function of the useReactFlow hook for controlled flows if (changes.some((c) => c.type === 'reset')) { return changes.filter((c) => c.type === 'reset').map((c) => c.item); } + + let remainingChanges = changes; const initElements: any[] = changes.filter((c) => c.type === 'add').map((c) => c.item); return elements.reduce((res: any[], item: any) => { - const currentChanges = changes.filter((c) => c.id === item.id); + const nextChanges: any[] = []; + const _remainingChanges: any[] = []; - if (currentChanges.length === 0) { + remainingChanges.forEach((c) => { + if (c.id === item.id) { + nextChanges.push(c); + } else { + _remainingChanges.push(c); + } + }); + remainingChanges = _remainingChanges; + + if (nextChanges.length === 0) { res.push(item); return res; } const updateItem = { ...item }; - for (const currentChange of currentChanges) { + for (const currentChange of nextChanges) { if (currentChange) { switch (currentChange.type) { case 'select': { diff --git a/packages/svelte/src/lib/actions/drag/index.ts b/packages/svelte/src/lib/actions/drag/index.ts index 9e719c32..d08a17be 100644 --- a/packages/svelte/src/lib/actions/drag/index.ts +++ b/packages/svelte/src/lib/actions/drag/index.ts @@ -30,7 +30,7 @@ export default function drag(domNode: Element, params: UseDragParams) { return { nodes: get(store.nodes), - nodesLookup: get(store.nodesLookup), + nodeLookup: get(store.nodeLookup), edges: get(store.edges), nodeExtent: get(store.nodeExtent), snapGrid: snapGrid ? snapGrid : [0, 0], diff --git a/packages/svelte/src/lib/components/BaseEdge/BaseEdge.svelte b/packages/svelte/src/lib/components/BaseEdge/BaseEdge.svelte index 4e5b39d1..d1c60f8f 100644 --- a/packages/svelte/src/lib/components/BaseEdge/BaseEdge.svelte +++ b/packages/svelte/src/lib/components/BaseEdge/BaseEdge.svelte @@ -23,6 +23,7 @@ { const updateIds = Array.isArray(id) ? id : [id]; - const updates = updateIds.reduce((res, updateId) => { + const updates = new Map(); + + updateIds.forEach((updateId) => { const nodeElement = get(domNode)?.querySelector( `.svelte-flow__node[data-id="${updateId}"]` ) as HTMLDivElement; if (nodeElement) { - res.push({ id: updateId, nodeElement, forceUpdate: true }); + updates.set(updateId, { id: updateId, nodeElement, forceUpdate: true }); } - - return res; - }, []); + }); requestAnimationFrame(() => updateNodeDimensions(updates)); }; diff --git a/packages/svelte/src/lib/store/derived-connection-props.ts b/packages/svelte/src/lib/store/derived-connection-props.ts index 173362ac..dd4e805c 100644 --- a/packages/svelte/src/lib/store/derived-connection-props.ts +++ b/packages/svelte/src/lib/store/derived-connection-props.ts @@ -56,15 +56,15 @@ export function getDerivedConnectionProps( currentConnection, store.connectionLineType, store.connectionMode, - store.nodesLookup, + store.nodeLookup, store.viewport ], - ([connection, connectionLineType, connectionMode, nodesLookup, viewport]) => { + ([connection, connectionLineType, connectionMode, nodeLookup, viewport]) => { if (!connection.connectionStartHandle?.nodeId) { return initConnectionProps; } - const fromNode = nodesLookup.get(connection.connectionStartHandle?.nodeId); + const fromNode = nodeLookup.get(connection.connectionStartHandle?.nodeId); const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds; const handleBoundsStrict = fromHandleBounds?.[connection.connectionStartHandle.type || 'source'] || []; diff --git a/packages/svelte/src/lib/store/edge-tree.ts b/packages/svelte/src/lib/store/edge-tree.ts index 221563ef..0861be01 100644 --- a/packages/svelte/src/lib/store/edge-tree.ts +++ b/packages/svelte/src/lib/store/edge-tree.ts @@ -9,18 +9,18 @@ export function getEdgeTree(store: SvelteFlowStoreState) { [ store.edges, store.nodes, - store.nodesLookup, + store.nodeLookup, store.onlyRenderVisibleElements, store.viewport, store.width, store.height ], - ([edges, , nodesLookup, onlyRenderVisibleElements, viewport, width, height]) => { + ([edges, , nodeLookup, onlyRenderVisibleElements, viewport, width, height]) => { const visibleEdges = onlyRenderVisibleElements && width && height ? edges.filter((edge) => { - const sourceNode = nodesLookup.get(edge.source); - const targetNode = nodesLookup.get(edge.target); + const sourceNode = nodeLookup.get(edge.source); + const targetNode = nodeLookup.get(edge.target); return ( sourceNode && @@ -41,11 +41,11 @@ export function getEdgeTree(store: SvelteFlowStoreState) { ); return derived( - [visibleEdges, store.nodes, store.nodesLookup, store.connectionMode, store.onError], - ([visibleEdges, , nodesLookup, connectionMode, onError]) => { + [visibleEdges, store.nodes, store.nodeLookup, store.connectionMode, store.onError], + ([visibleEdges, , nodeLookup, connectionMode, onError]) => { const layoutedEdges = visibleEdges.reduce((res, edge) => { - const sourceNode = nodesLookup.get(edge.source); - const targetNode = nodesLookup.get(edge.target); + const sourceNode = nodeLookup.get(edge.source); + const targetNode = nodeLookup.get(edge.target); if (!sourceNode || !targetNode) { return res; @@ -71,7 +71,7 @@ export function getEdgeTree(store: SvelteFlowStoreState) { return res; }, []); - const groupedEdges = groupEdgesByZLevel(layoutedEdges, nodesLookup, false); + const groupedEdges = groupEdgesByZLevel(layoutedEdges, nodeLookup, false); return groupedEdges; } diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index c9140805..27a6aee0 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -90,7 +90,7 @@ export function createStore({ const nextNodes = updateNodeDimensionsSystem( updates, get(store.nodes), - get(store.nodesLookup), + get(store.nodeLookup), get(store.domNode), get(store.nodeOrigin) ); diff --git a/packages/svelte/src/lib/store/initial-store.ts b/packages/svelte/src/lib/store/initial-store.ts index 7cabef6e..4bb3d7af 100644 --- a/packages/svelte/src/lib/store/initial-store.ts +++ b/packages/svelte/src/lib/store/initial-store.ts @@ -59,8 +59,8 @@ export const getInitialStore = ({ height?: number; fitView?: boolean; }) => { - const nodesLookup = new Map(); - const nextNodes = updateNodes(nodes, nodesLookup, { + const nodeLookup = new Map(); + const nextNodes = updateNodes(nodes, nodeLookup, { nodeOrigin: [0, 0], elevateNodesOnSelect: false }); @@ -79,8 +79,8 @@ export const getInitialStore = ({ return { flowId: writable(null), - nodes: createNodesStore(nextNodes, nodesLookup), - nodesLookup: readable>(nodesLookup), + nodes: createNodesStore(nextNodes, nodeLookup), + nodeLookup: readable>(nodeLookup), visibleNodes: readable([]), edges: createEdgesStore(edges), edgeTree: readable[]>([]), diff --git a/packages/svelte/src/lib/store/utils.ts b/packages/svelte/src/lib/store/utils.ts index 82f8afc6..67c6d288 100644 --- a/packages/svelte/src/lib/store/utils.ts +++ b/packages/svelte/src/lib/store/utils.ts @@ -112,7 +112,7 @@ export type NodeStoreOptions = { // The user only passes in relative positions, so we need to calculate the absolute positions based on the parent nodes. export const createNodesStore = ( nodes: Node[], - nodesLookup: Map + nodeLookup: Map ): { subscribe: (this: void, run: Subscriber) => Unsubscriber; update: (this: void, updater: Updater) => void; @@ -126,7 +126,7 @@ export const createNodesStore = ( let elevateNodesOnSelect = true; const _set = (nds: Node[]): Node[] => { - const nextNodes = updateNodes(nds, nodesLookup, { + const nextNodes = updateNodes(nds, nodeLookup, { elevateNodesOnSelect, defaults }); diff --git a/packages/system/src/utils/dom.ts b/packages/system/src/utils/dom.ts index 4aa51174..74bb14aa 100644 --- a/packages/system/src/utils/dom.ts +++ b/packages/system/src/utils/dom.ts @@ -13,7 +13,6 @@ export function getPointerPosition( ): XYPosition & { xSnapped: number; ySnapped: number } { const { x, y } = getEventPosition(event); const pointerPos = pointToRendererPoint({ x, y }, transform); - const { x: xSnapped, y: ySnapped } = snapToGrid ? snapPosition(pointerPos, snapGrid) : pointerPos; // we need the snapped position in order to be able to skip unnecessary drag events @@ -58,6 +57,9 @@ export const getEventPosition = (event: MouseEvent | TouchEvent, bounds?: DOMRec }; }; +// The handle bounds are calculated relative to the node element. +// We store them in the internals object of the node in order to avoid +// unnecessary recalculations. export const getHandleBounds = ( selector: string, nodeElement: HTMLDivElement, diff --git a/packages/system/src/utils/edges/general.ts b/packages/system/src/utils/edges/general.ts index 5d4c3151..bdffbbd8 100644 --- a/packages/system/src/utils/edges/general.ts +++ b/packages/system/src/utils/edges/general.ts @@ -33,7 +33,7 @@ export type GroupedEdges = { export function groupEdgesByZLevel( edges: EdgeType[], - nodesLookup: Map, + nodeLookup: Map, elevateEdgesOnSelect = false ): GroupedEdges[] { let maxLevel = -1; @@ -43,8 +43,8 @@ export function groupEdgesByZLevel( let z = hasZIndex ? edge.zIndex! : 0; if (elevateEdgesOnSelect) { - const targetNode = nodesLookup.get(edge.target); - const sourceNode = nodesLookup.get(edge.source); + const targetNode = nodeLookup.get(edge.target); + const sourceNode = nodeLookup.get(edge.source); const edgeOrConnectedNodeSelected = edge.selected || targetNode?.selected || sourceNode?.selected; const selectedZIndex = Math.max( sourceNode?.[internalsSymbol]?.z || 0, diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index ff168d43..9ed4f871 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -18,21 +18,21 @@ type ParentNodes = Record; export function updateAbsolutePositions( nodes: NodeType[], - nodesLookup: Map, + nodeLookup: Map, nodeOrigin: NodeOrigin = [0, 0], parentNodes?: ParentNodes ) { return nodes.map((node) => { - if (node.parentNode && !nodesLookup.has(node.parentNode)) { + if (node.parentNode && !nodeLookup.has(node.parentNode)) { throw new Error(`Parent node ${node.parentNode} not found`); } if (node.parentNode || parentNodes?.[node.id]) { - const parentNode = node.parentNode ? nodesLookup.get(node.parentNode) : null; + const parentNode = node.parentNode ? nodeLookup.get(node.parentNode) : null; const { x, y, z } = calculateXYZPosition( node, nodes, - nodesLookup, + nodeLookup, { ...node.position, z: node[internalsSymbol]?.z ?? 0, @@ -64,7 +64,7 @@ type UpdateNodesOptions = { export function updateNodes( nodes: NodeType[], - nodesLookup: Map, + nodeLookup: Map, options: UpdateNodesOptions = { nodeOrigin: [0, 0] as NodeOrigin, elevateNodesOnSelect: true, @@ -75,7 +75,7 @@ export function updateNodes( const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0; const nextNodes = nodes.map((n) => { - const currentStoreNode = nodesLookup.get(n.id); + const currentStoreNode = nodeLookup.get(n.id); const node: NodeType = { ...options.defaults, ...n, @@ -98,12 +98,12 @@ export function updateNodes( }, }); - nodesLookup.set(node.id, node); + nodeLookup.set(node.id, node); return node; }); - const nodesWithPositions = updateAbsolutePositions(nextNodes, nodesLookup, options.nodeOrigin, parentNodes); + const nodesWithPositions = updateAbsolutePositions(nextNodes, nodeLookup, options.nodeOrigin, parentNodes); return nodesWithPositions; } @@ -111,7 +111,7 @@ export function updateNodes( function calculateXYZPosition( node: NodeType, nodes: NodeType[], - nodesLookup: Map, + nodeLookup: Map, result: XYZPosition, nodeOrigin: NodeOrigin ): XYZPosition { @@ -119,13 +119,13 @@ function calculateXYZPosition( return result; } - const parentNode = nodesLookup.get(node.parentNode)!; + const parentNode = nodeLookup.get(node.parentNode)!; const parentNodePosition = getNodePositionWithOrigin(parentNode, parentNode?.origin || nodeOrigin); return calculateXYZPosition( parentNode, nodes, - nodesLookup, + nodeLookup, { x: (result.x ?? 0) + parentNodePosition.x, y: (result.y ?? 0) + parentNodePosition.y, @@ -138,7 +138,7 @@ function calculateXYZPosition( export function updateNodeDimensions( updates: Map, nodes: NodeBase[], - nodesLookup: Map, + nodeLookup: Map, domNode: HTMLElement | null, nodeOrigin?: NodeOrigin, onUpdate?: (id: string, dimensions: Dimensions) => void @@ -178,7 +178,7 @@ export function updateNodeDimensions( }, }; - nodesLookup.set(node.id, newNode); + nodeLookup.set(node.id, newNode); return newNode; } diff --git a/packages/system/src/xydrag/XYDrag.ts b/packages/system/src/xydrag/XYDrag.ts index 66637c70..69d50b41 100644 --- a/packages/system/src/xydrag/XYDrag.ts +++ b/packages/system/src/xydrag/XYDrag.ts @@ -33,7 +33,7 @@ export type OnDrag = (event: MouseEvent, dragItems: NodeDragItem[], node: NodeBa type StoreItems = { nodes: NodeBase[]; - nodesLookup: Map; + nodeLookup: Map; edges: EdgeBase[]; nodeExtent: CoordinateExtent; snapGrid: SnapGrid; @@ -104,7 +104,7 @@ export function XYDrag({ function updateNodes({ x, y }: XYPosition) { const { nodes, - nodesLookup, + nodeLookup, nodeExtent, snapGrid, snapToGrid, @@ -169,7 +169,7 @@ export function XYDrag({ const [currentNode, currentNodes] = getEventHandlerParams({ nodeId, dragItems, - nodesLookup, + nodeLookup, }); onDrag?.(dragEvent as MouseEvent, dragItems, currentNode, currentNodes); onNodeOrSelectionDrag?.(dragEvent as MouseEvent, currentNode, currentNodes); @@ -199,7 +199,7 @@ export function XYDrag({ function startDrag(event: UseDragEvent) { const { nodes, - nodesLookup, + nodeLookup, multiSelectionActive, nodesDraggable, transform, @@ -214,7 +214,7 @@ export function XYDrag({ dragStarted = true; if ((!selectNodesOnDrag || !isSelectable) && !multiSelectionActive && nodeId) { - if (!nodes.find((n) => n.id === nodeId)?.selected) { + if (!nodeLookup.get(nodeId)?.selected) { // we need to reset selected nodes when selectNodesOnDrag=false unselectNodesAndEdges(); } @@ -234,7 +234,7 @@ export function XYDrag({ const [currentNode, currentNodes] = getEventHandlerParams({ nodeId, dragItems, - nodesLookup, + nodeLookup, }); onDragStart?.(event.sourceEvent as MouseEvent, dragItems, currentNode, currentNodes); onNodeOrSelectionDragStart?.(event.sourceEvent as MouseEvent, currentNode, currentNodes); @@ -291,7 +291,7 @@ export function XYDrag({ cancelAnimationFrame(autoPanId); if (dragItems) { - const { nodesLookup, updateNodePositions, onNodeDragStop, onSelectionDragStop } = getStoreItems(); + const { nodeLookup, updateNodePositions, onNodeDragStop, onSelectionDragStop } = getStoreItems(); const onNodeOrSelectionDragStop = nodeId ? onNodeDragStop : wrapSelectionDragFunc(onSelectionDragStop); updateNodePositions(dragItems, false, false); @@ -300,7 +300,7 @@ export function XYDrag({ const [currentNode, currentNodes] = getEventHandlerParams({ nodeId, dragItems, - nodesLookup, + nodeLookup, }); onDragStop?.(event.sourceEvent as MouseEvent, dragItems, currentNode, currentNodes); onNodeOrSelectionDragStop?.(event.sourceEvent as MouseEvent, currentNode, currentNodes); diff --git a/packages/system/src/xydrag/utils.ts b/packages/system/src/xydrag/utils.ts index d8bfdb59..274c2213 100644 --- a/packages/system/src/xydrag/utils.ts +++ b/packages/system/src/xydrag/utils.ts @@ -75,14 +75,14 @@ export function getDragItems( export function getEventHandlerParams({ nodeId, dragItems, - nodesLookup, + nodeLookup, }: { nodeId?: string; dragItems: NodeDragItem[]; - nodesLookup: Map; + nodeLookup: Map; }): [NodeType, NodeType[]] { const extentedDragItems: NodeType[] = dragItems.map((n) => { - const node = nodesLookup.get(n.id)!; + const node = nodeLookup.get(n.id)!; return { ...node, From 14caf422c30adeab3953b52ddf9dd38e7b5fbb99 Mon Sep 17 00:00:00 2001 From: Moritz Klack Date: Wed, 15 Nov 2023 19:06:49 +0100 Subject: [PATCH 05/23] refactor(svelte): add zoomActivationKey and allow null for keys #3618 (#3627) * refactor(keys): add zoomActivationKey and allow null #3618 --- package.json | 4 +- .../components/KeyHandler/KeyHandler.svelte | 113 ++++++++++++------ .../src/lib/components/KeyHandler/types.ts | 9 +- .../container/SvelteFlow/SvelteFlow.svelte | 10 +- .../src/lib/container/SvelteFlow/types.ts | 8 +- .../svelte/src/lib/container/Zoom/Zoom.svelte | 5 +- .../svelte/src/lib/store/initial-store.ts | 1 + pnpm-lock.yaml | 8 +- 8 files changed, 103 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index 274670c2..d87e4403 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "scripts": { "preinstall": "npx only-allow pnpm", "dev": "turbo run dev --parallel --concurrency 12", - "dev:svelte": "turbo run dev --filter=svelte --filter=system", - "dev:react": "turbo run dev --filter=react-examples ", + "dev:svelte": "turbo run dev --filter=svelte-examples...", + "dev:react": "turbo run dev --filter=react-examples...", "test:svelte": "pnpm --filter=playwright run test:svelte", "test:svelte:ui": "pnpm --filter=playwright run test:svelte:ui", "test:react": "pnpm --filter=playwright run test:react", diff --git a/packages/svelte/src/lib/components/KeyHandler/KeyHandler.svelte b/packages/svelte/src/lib/components/KeyHandler/KeyHandler.svelte index 77195ea2..f059ff8e 100644 --- a/packages/svelte/src/lib/components/KeyHandler/KeyHandler.svelte +++ b/packages/svelte/src/lib/components/KeyHandler/KeyHandler.svelte @@ -1,5 +1,5 @@ selectionKeyPressed.set(true) + ...selectionKeyDefinition, + callback: () => selectionKeyDefinition.key && selectionKeyPressed.set(true) } ], type: 'keydown' @@ -53,9 +79,8 @@ use:shortcut={{ trigger: [ { - key: selectionKeyString, - modifier: selectionKeyModifier, - callback: () => selectionKeyPressed.set(false) + ...selectionKeyDefinition, + callback: () => selectionKeyDefinition.key && selectionKeyPressed.set(false) } ], type: 'keyup' @@ -63,9 +88,8 @@ use:shortcut={{ trigger: [ { - key: multiSelectionKeyString, - modifier: multiSelectionKeyModifier, - callback: () => multiselectionKeyPressed.set(true) + ...multiSelectionKeyDefinition, + callback: () => multiSelectionKeyDefinition.key && multiselectionKeyPressed.set(true) } ], type: 'keydown' @@ -73,9 +97,8 @@ use:shortcut={{ trigger: [ { - key: multiSelectionKeyString, - modifier: multiSelectionKeyModifier, - callback: () => multiselectionKeyPressed.set(false) + ...multiSelectionKeyDefinition, + callback: () => multiSelectionKeyDefinition.key && multiselectionKeyPressed.set(false) } ], type: 'keyup' @@ -83,11 +106,8 @@ use:shortcut={{ trigger: [ { - key: deleteKeyString, - modifier: deleteKeyModifier, - callback: () => { - deleteKeyPressed.set(true); - } + ...deleteKeyDefinition, + callback: () => deleteKeyDefinition.key && deleteKeyPressed.set(true) } ], type: 'keydown' @@ -95,9 +115,8 @@ use:shortcut={{ trigger: [ { - key: deleteKeyString, - modifier: deleteKeyModifier, - callback: () => deleteKeyPressed.set(false) + ...deleteKeyDefinition, + callback: () => deleteKeyDefinition.key && deleteKeyPressed.set(false) } ], type: 'keyup' @@ -105,9 +124,8 @@ use:shortcut={{ trigger: [ { - key: panActivationKeyString, - modifier: panActivationKeyModifier, - callback: () => panActivationKeyPressed.set(true) + ...panActivationKeyDefinition, + callback: () => panActivationKeyDefinition.key && panActivationKeyPressed.set(true) } ], type: 'keydown' @@ -115,9 +133,26 @@ use:shortcut={{ trigger: [ { - key: panActivationKeyString, - modifier: panActivationKeyModifier, - callback: () => panActivationKeyPressed.set(false) + ...panActivationKeyDefinition, + callback: () => panActivationKeyDefinition.key && panActivationKeyPressed.set(false) + } + ], + type: 'keyup' + }} + use:shortcut={{ + trigger: [ + { + ...zoomActivationKeyDefinition, + callback: () => zoomActivationKeyDefinition.key && zoomActivationKeyPressed.set(true) + } + ], + type: 'keydown' + }} + use:shortcut={{ + trigger: [ + { + ...zoomActivationKeyDefinition, + callback: () => zoomActivationKeyDefinition.key && zoomActivationKeyPressed.set(false) } ], type: 'keyup' diff --git a/packages/svelte/src/lib/components/KeyHandler/types.ts b/packages/svelte/src/lib/components/KeyHandler/types.ts index de81d1c3..9a6ebe86 100644 --- a/packages/svelte/src/lib/components/KeyHandler/types.ts +++ b/packages/svelte/src/lib/components/KeyHandler/types.ts @@ -1,8 +1,9 @@ import type { KeyDefinition } from '$lib/types'; export type KeyHandlerProps = { - selectionKey?: KeyDefinition; - multiSelectionKey?: KeyDefinition; - deleteKey?: KeyDefinition; - panActivationKey?: KeyDefinition; + selectionKey?: KeyDefinition | null; + multiSelectionKey?: KeyDefinition | null; + deleteKey?: KeyDefinition | null; + panActivationKey?: KeyDefinition | null; + zoomActivationKey?: KeyDefinition | null; }; diff --git a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte index 0c74b4e0..c648d07b 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte @@ -34,6 +34,8 @@ export let selectionKey: $$Props['selectionKey'] = undefined; export let selectionMode: $$Props['selectionMode'] = undefined; export let panActivationKey: $$Props['panActivationKey'] = undefined; + export let multiSelectionKey: $$Props['multiSelectionKey'] = undefined; + export let zoomActivationKey: $$Props['zoomActivationKey'] = undefined; export let nodesDraggable: $$Props['nodesDraggable'] = undefined; export let nodesConnectable: $$Props['nodesConnectable'] = undefined; export let nodeDragThreshold: $$Props['nodeDragThreshold'] = undefined; @@ -166,7 +168,13 @@ {...$$restProps} role="application" > - + & { edges: Writable; nodeTypes?: NodeTypes; edgeTypes?: EdgeTypes; - selectionKey?: KeyDefinition; - panActivationKey?: KeyDefinition; - deleteKey?: KeyDefinition; + selectionKey?: KeyDefinition | null; + panActivationKey?: KeyDefinition | null; + deleteKey?: KeyDefinition | null; + multiSelectionKey?: KeyDefinition | null; + zoomActivationKey?: KeyDefinition | null; fitView?: boolean; fitViewOptions?: FitViewOptions; nodeOrigin?: NodeOrigin; diff --git a/packages/svelte/src/lib/container/Zoom/Zoom.svelte b/packages/svelte/src/lib/container/Zoom/Zoom.svelte index 116586ad..a00199a1 100644 --- a/packages/svelte/src/lib/container/Zoom/Zoom.svelte +++ b/packages/svelte/src/lib/container/Zoom/Zoom.svelte @@ -28,7 +28,8 @@ dragging, translateExtent, lib, - panActivationKeyPressed + panActivationKeyPressed, + zoomActivationKeyPressed } = useStore(); $: viewPort = initialViewport || { x: 0, y: 0, zoom: 1 }; @@ -55,7 +56,7 @@ panOnDrag: _panOnDrag, panOnScrollSpeed: 0.5, panOnScrollMode: panOnScrollMode || PanOnScrollMode.Free, - zoomActivationKeyPressed: false, + zoomActivationKeyPressed: $zoomActivationKeyPressed, preventScrolling: typeof preventScrolling === 'boolean' ? preventScrolling : true, noPanClassName: 'nopan', noWheelClassName: 'nowheel', diff --git a/packages/svelte/src/lib/store/initial-store.ts b/packages/svelte/src/lib/store/initial-store.ts index 4bb3d7af..2e40a719 100644 --- a/packages/svelte/src/lib/store/initial-store.ts +++ b/packages/svelte/src/lib/store/initial-store.ts @@ -105,6 +105,7 @@ export const getInitialStore = ({ multiselectionKeyPressed: writable(false), deleteKeyPressed: writable(false), panActivationKeyPressed: writable(false), + zoomActivationKeyPressed: writable(false), selectionRectMode: writable(null), selectionMode: writable(SelectionMode.Partial), nodeTypes: writable(initialNodeTypes), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b646fa44..05a5637d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3523,7 +3523,6 @@ packages: version: 8.11.2 engines: {node: '>=0.4.0'} hasBin: true - dev: true registry.npmjs.org/aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz} @@ -4395,7 +4394,7 @@ packages: dependencies: '@jridgewell/sourcemap-codec': registry.npmjs.org/@jridgewell/sourcemap-codec@1.4.15 '@types/estree': registry.npmjs.org/@types/estree@1.0.3 - acorn: registry.npmjs.org/acorn@8.10.0 + acorn: registry.npmjs.org/acorn@8.11.2 estree-walker: registry.npmjs.org/estree-walker@3.0.3 periscopic: registry.npmjs.org/periscopic@3.1.0 @@ -6029,6 +6028,7 @@ packages: glob-parent: registry.npmjs.org/glob-parent@5.1.2 merge2: registry.npmjs.org/merge2@1.4.1 micromatch: registry.npmjs.org/micromatch@4.0.5 + dev: false registry.npmjs.org/fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz} @@ -10957,7 +10957,7 @@ packages: dependencies: '@jridgewell/trace-mapping': registry.npmjs.org/@jridgewell/trace-mapping@0.3.20 chokidar: registry.npmjs.org/chokidar@3.5.3 - fast-glob: registry.npmjs.org/fast-glob@3.3.1 + fast-glob: registry.npmjs.org/fast-glob@3.3.2 import-fresh: registry.npmjs.org/import-fresh@3.3.0 picocolors: registry.npmjs.org/picocolors@1.0.0 sade: registry.npmjs.org/sade@1.8.1 @@ -11230,7 +11230,7 @@ packages: '@ampproject/remapping': registry.npmjs.org/@ampproject/remapping@2.2.1 '@jridgewell/sourcemap-codec': registry.npmjs.org/@jridgewell/sourcemap-codec@1.4.15 '@jridgewell/trace-mapping': registry.npmjs.org/@jridgewell/trace-mapping@0.3.20 - acorn: registry.npmjs.org/acorn@8.10.0 + acorn: registry.npmjs.org/acorn@8.11.2 aria-query: registry.npmjs.org/aria-query@5.3.0 axobject-query: registry.npmjs.org/axobject-query@3.2.1 code-red: registry.npmjs.org/code-red@1.0.4 From 99c24f86c6162aa55577aa5081d1d9db29822f53 Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 16 Nov 2023 11:18:11 +0100 Subject: [PATCH 06/23] fix(tests) adjusted FlowConfig type --- examples/react/src/app.d.ts | 10 +++++----- examples/react/src/generic-tests/Flow.tsx | 4 ++-- examples/svelte/src/app.d.ts | 10 +++++----- .../routes/tests/generic/[topic]/[example]/Flow.svelte | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/react/src/app.d.ts b/examples/react/src/app.d.ts index 3c245231..372850d2 100644 --- a/examples/react/src/app.d.ts +++ b/examples/react/src/app.d.ts @@ -2,10 +2,10 @@ import { BackgroundProps, ControlProps, Edge, MiniMapProps, Node, PanelProps, Re declare global { interface FlowConfig { - flowProps: Omit & { nodes: Node[]; edges: Edge[] }; - panelProps: PanelProps; - backgroundProps: BackgroundProps; - controlsProps: ControlProps; - minimapProps: MiniMapProps; + flowProps?: Omit & { nodes: Node[]; edges: Edge[] }; + panelProps?: PanelProps; + backgroundProps?: BackgroundProps; + controlsProps?: ControlProps; + minimapProps?: MiniMapProps; } } diff --git a/examples/react/src/generic-tests/Flow.tsx b/examples/react/src/generic-tests/Flow.tsx index 97691bd3..a6db5f5d 100644 --- a/examples/react/src/generic-tests/Flow.tsx +++ b/examples/react/src/generic-tests/Flow.tsx @@ -18,8 +18,8 @@ type FlowProps = { }; export default ({ flowConfig }: FlowProps) => { - const [nodes, setNodes] = useState(flowConfig.flowProps.nodes); - const [edges, setEdges] = useState(flowConfig.flowProps.edges); + const [nodes, setNodes] = useState(flowConfig.flowProps?.nodes); + const [edges, setEdges] = useState(flowConfig.flowProps?.edges); const props = { ...flowConfig.flowProps, nodes, edges }; const onNodesChange: OnNodesChange = useCallback((changes) => setNodes((nds) => applyNodeChanges(changes, nds)), []); diff --git a/examples/svelte/src/app.d.ts b/examples/svelte/src/app.d.ts index 3e18db2a..31fd2d84 100644 --- a/examples/svelte/src/app.d.ts +++ b/examples/svelte/src/app.d.ts @@ -20,11 +20,11 @@ declare global { } interface FlowConfig { - flowProps: Omit & { nodes: Node[]; edges: Edge[] }; - panelProps: PanelProps; - backgroundProps: BackgroundProps; - controlsProps: ControlsProps; - minimapProps: MiniMapProps; + flowProps?: Omit & { nodes: Node[]; edges: Edge[] }; + panelProps?: PanelProps; + backgroundProps?: BackgroundProps; + controlsProps?: ControlsProps; + minimapProps?: MiniMapProps; } } diff --git a/examples/svelte/src/routes/tests/generic/[topic]/[example]/Flow.svelte b/examples/svelte/src/routes/tests/generic/[topic]/[example]/Flow.svelte index 279005a0..bb04fcb8 100644 --- a/examples/svelte/src/routes/tests/generic/[topic]/[example]/Flow.svelte +++ b/examples/svelte/src/routes/tests/generic/[topic]/[example]/Flow.svelte @@ -7,8 +7,8 @@ export let flowConfig: FlowConfig; // Create writables here so it is easier to create test cases - const nodes = writable(flowConfig.flowProps.nodes); - const edges = writable(flowConfig.flowProps.edges); + const nodes = writable(flowConfig.flowProps?.nodes); + const edges = writable(flowConfig.flowProps?.edges); const flowProps = { ...flowConfig.flowProps, nodes, edges }; From b56f5ad9e14c65b543c6f49e98b03b22d55fa947 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 16 Nov 2023 11:55:59 +0100 Subject: [PATCH 07/23] chore(examples): cleanup --- examples/react/src/examples/DragNDrop/index.tsx | 4 ++-- .../src/routes/examples/add-node-on-drop/Flow.svelte | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/examples/react/src/examples/DragNDrop/index.tsx b/examples/react/src/examples/DragNDrop/index.tsx index 83e39d88..d11cd902 100644 --- a/examples/react/src/examples/DragNDrop/index.tsx +++ b/examples/react/src/examples/DragNDrop/index.tsx @@ -49,7 +49,7 @@ const DnDFlow = () => { if (reactFlowInstance) { const type = event.dataTransfer.getData('application/reactflow'); - const position = reactFlowInstance.screenToFlowCoordinate({ + const position = reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY, }); @@ -66,7 +66,7 @@ const DnDFlow = () => { return (
- +
- import { SvelteFlow, useSvelteFlow } from '@xyflow/svelte'; - import type { Edge, Node } from '@xyflow/svelte'; import { writable } from 'svelte/store'; + import { SvelteFlow, useSvelteFlow, type Edge, type Node } from '@xyflow/svelte'; import '@xyflow/svelte/dist/style.css'; @@ -22,7 +21,7 @@ let id = 1; const getId = () => `${id++}`; - const { screenToFlowCoordinate, flowToScreenCoordinate } = useSvelteFlow(); + const { screenToFlowPosition, flowToScreenPosition } = useSvelteFlow(); function handleConnectEnd({ detail: { event } }: { detail: { event: MouseEvent | TouchEvent } }) { // See of connection landed inside the flow pane @@ -34,7 +33,7 @@ y: event.clientY }; - const doubleTransformedPosition = flowToScreenCoordinate(screenToFlowCoordinate(position)); + const doubleTransformedPosition = flowToScreenPosition(screenToFlowPosition(position)); console.log( 'Is transforming in both directions (screen-flow, flow-screen) the same?', position.x === doubleTransformedPosition.x && position.y === doubleTransformedPosition.y @@ -43,8 +42,8 @@ const newNode: Node = { id, data: { label: `Node ${id}` }, - // project the screen coordinates to pane coordinates - position: screenToFlowCoordinate(position), + // project the screen position to pane position + position: screenToFlowPosition(position), // set the origin of the new node so it is centered origin: [0.5, 0.0] }; From c8881264382cf6c2729594688b01f25bc4562c8a Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 16 Nov 2023 11:56:17 +0100 Subject: [PATCH 08/23] fix(svelte): use map for dimension updates --- .../components/NodeWrapper/NodeWrapper.svelte | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index e422b444..29631836 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -87,13 +87,18 @@ if (doUpdate) { requestAnimationFrame(() => - updateNodeDimensions([ - { - id, - nodeElement: nodeRef, - forceUpdate: true - } - ]) + updateNodeDimensions( + new Map([ + [ + id, + { + id, + nodeElement: nodeRef, + forceUpdate: true + } + ] + ]) + ) ); } @@ -124,8 +129,6 @@ dispatch('nodeclick', { node, event }); } - - // @todo: add selectable state From aee289d2853c56e135bb76d58c08b9f233aa719f Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 16 Nov 2023 11:56:48 +0100 Subject: [PATCH 09/23] fix(svelte): use correct nodes for node store sync --- packages/svelte/src/lib/store/utils.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/lib/store/utils.ts b/packages/svelte/src/lib/store/utils.ts index 67c6d288..a11bf1f6 100644 --- a/packages/svelte/src/lib/store/utils.ts +++ b/packages/svelte/src/lib/store/utils.ts @@ -18,8 +18,15 @@ export function syncNodeStores( ) { const nodesStoreSetter = nodesStore.set; const userNodesStoreSetter = userNodesStore.set; + const currentNodesStore = get(nodesStore); + const currentUserNodesStore = get(userNodesStore); + // depending how the user initializes the nodes, we need to decide if we want to use + // the user nodes or the internal nodes for initialization. A user can use a SvelteFlowProvider + // without providing any nodes, in that case we want to use the nodes passed by the user. + // By default we are using the store nodes, because they already have the absolute positions. + const initWithUserNodes = currentNodesStore.length === 0 && currentUserNodesStore.length > 0; - let val = get(userNodesStore); + let val = initWithUserNodes ? currentUserNodesStore : currentNodesStore; nodesStore.set(val); const _set = (nds: Node[]) => { From 4849c90cbdfcaaef8b13b10bc12b6549e4604228 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 16 Nov 2023 13:23:55 +0100 Subject: [PATCH 10/23] refactor(system): getElementsToRemove returns unique edge items --- packages/system/src/utils/graph.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index b01d4c56..43a80ef6 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -312,7 +312,13 @@ export function getElementsToRemove (typeof e.deletable === 'boolean' ? e.deletable : true)); const initialHitEdges = deletableEdges.filter((e) => edgeIds.includes(e.id)); const connectedEdges = getConnectedEdgesBase(matchingNodes, deletableEdges); - const matchingEdges = [...initialHitEdges, ...connectedEdges]; + const matchingEdges = connectedEdges.reduce((res, edge) => { + if (!res.find((e) => e.id === edge.id)) { + res.push(edge); + } + + return res; + }, initialHitEdges); return { matchingEdges, From 05968c82a2a3818c98100b60712be7826de2f204 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 16 Nov 2023 13:24:32 +0100 Subject: [PATCH 11/23] feat(svelte): add ondelete handler #3618 --- .../svelte/src/routes/examples/edges/+page.svelte | 2 +- .../src/lib/container/SvelteFlow/SvelteFlow.svelte | 6 ++++-- .../svelte/src/lib/container/SvelteFlow/types.ts | 6 ++++-- .../svelte/src/lib/container/SvelteFlow/utils.ts | 3 ++- packages/svelte/src/lib/store/edge-tree.ts | 6 +++--- packages/svelte/src/lib/store/index.ts | 5 +++++ packages/svelte/src/lib/store/initial-store.ts | 13 +++++++++++-- packages/svelte/src/lib/types/general.ts | 3 +++ 8 files changed, 33 insertions(+), 11 deletions(-) diff --git a/examples/svelte/src/routes/examples/edges/+page.svelte b/examples/svelte/src/routes/examples/edges/+page.svelte index 403b0ef6..fc118333 100644 --- a/examples/svelte/src/routes/examples/edges/+page.svelte +++ b/examples/svelte/src/routes/examples/edges/+page.svelte @@ -145,7 +145,7 @@ ]); - + diff --git a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte index c648d07b..10c1751e 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte @@ -63,7 +63,8 @@ export let selectionOnDrag: $$Props['selectionOnDrag'] = undefined; export let autoPanOnConnect: $$Props['autoPanOnConnect'] = true; export let autoPanOnNodeDrag: $$Props['autoPanOnNodeDrag'] = true; - export let onError: $$Props['onError'] = undefined; + export let onerror: $$Props['onerror'] = undefined; + export let ondelete: $$Props['ondelete'] = undefined; export let attributionPosition: $$Props['attributionPosition'] = undefined; export let proOptions: $$Props['proOptions'] = undefined; export let defaultEdgeOptions: $$Props['defaultEdgeOptions'] = undefined; @@ -139,7 +140,8 @@ isValidConnection, autoPanOnConnect, autoPanOnNodeDrag, - onError, + onerror, + ondelete, connectionMode, nodeDragThreshold }; diff --git a/packages/svelte/src/lib/container/SvelteFlow/types.ts b/packages/svelte/src/lib/container/SvelteFlow/types.ts index b239094a..bf66a30b 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/types.ts +++ b/packages/svelte/src/lib/container/SvelteFlow/types.ts @@ -24,7 +24,8 @@ import type { KeyDefinition, EdgeTypes, DefaultEdgeOptions, - FitViewOptions + FitViewOptions, + OnDelete } from '$lib/types'; import type { Writable } from 'svelte/store'; @@ -84,5 +85,6 @@ export type SvelteFlowProps = DOMAttributes & { onMoveStart?: OnMoveStart; onMove?: OnMove; onMoveEnd?: OnMoveEnd; - onError?: OnError; + onerror?: OnError; + ondelete?: OnDelete; }; diff --git a/packages/svelte/src/lib/container/SvelteFlow/utils.ts b/packages/svelte/src/lib/container/SvelteFlow/utils.ts index 0579eaa9..fe5a15aa 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/utils.ts +++ b/packages/svelte/src/lib/container/SvelteFlow/utils.ts @@ -62,7 +62,8 @@ export type UpdatableStoreProps = { autoPanOnConnect?: UnwrapWritable; autoPanOnNodeDrag?: UnwrapWritable; connectionMode?: UnwrapWritable; - onError?: UnwrapWritable; + onerror?: UnwrapWritable; + ondelete?: UnwrapWritable; nodeDragThreshold?: UnwrapWritable; }; diff --git a/packages/svelte/src/lib/store/edge-tree.ts b/packages/svelte/src/lib/store/edge-tree.ts index 0861be01..cd5448ab 100644 --- a/packages/svelte/src/lib/store/edge-tree.ts +++ b/packages/svelte/src/lib/store/edge-tree.ts @@ -41,8 +41,8 @@ export function getEdgeTree(store: SvelteFlowStoreState) { ); return derived( - [visibleEdges, store.nodes, store.nodeLookup, store.connectionMode, store.onError], - ([visibleEdges, , nodeLookup, connectionMode, onError]) => { + [visibleEdges, store.nodes, store.nodeLookup, store.connectionMode, store.onerror], + ([visibleEdges, , nodeLookup, connectionMode, onerror]) => { const layoutedEdges = visibleEdges.reduce((res, edge) => { const sourceNode = nodeLookup.get(edge.source); const targetNode = nodeLookup.get(edge.target); @@ -58,7 +58,7 @@ export function getEdgeTree(store: SvelteFlowStoreState) { sourceHandle: edge.sourceHandle || null, targetHandle: edge.targetHandle || null, connectionMode, - onError + onError: onerror }); if (edgePosition) { diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index 27a6aee0..625191e0 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -221,6 +221,11 @@ export function createStore({ store.edges.update((eds) => eds.filter((edge) => !matchingEdges.some((mE) => mE.id === edge.id)) ); + + get(store.ondelete)?.({ + nodes: matchingNodes, + edges: matchingEdges + }); } } }); diff --git a/packages/svelte/src/lib/store/initial-store.ts b/packages/svelte/src/lib/store/initial-store.ts index 2e40a719..d6b621d9 100644 --- a/packages/svelte/src/lib/store/initial-store.ts +++ b/packages/svelte/src/lib/store/initial-store.ts @@ -28,7 +28,15 @@ import BezierEdge from '$lib/components/edges/BezierEdge.svelte'; import StraightEdge from '$lib/components/edges/StraightEdge.svelte'; import SmoothStepEdge from '$lib/components/edges/SmoothStepEdge.svelte'; import StepEdge from '$lib/components/edges/StepEdge.svelte'; -import type { NodeTypes, EdgeTypes, EdgeLayouted, Node, Edge, FitViewOptions } from '$lib/types'; +import type { + NodeTypes, + EdgeTypes, + EdgeLayouted, + Node, + Edge, + FitViewOptions, + OnDelete +} from '$lib/types'; import { createNodesStore, createEdgesStore } from './utils'; import { initConnectionProps, type ConnectionProps } from './derived-connection-props'; @@ -125,6 +133,7 @@ export const getInitialStore = ({ defaultMarkerColor: writable('#b1b1b7'), lib: readable('svelte'), onlyRenderVisibleElements: writable(false), - onError: writable(devWarn) + onerror: writable(devWarn), + ondelete: writable(undefined) }; }; diff --git a/packages/svelte/src/lib/types/general.ts b/packages/svelte/src/lib/types/general.ts index d73f4a4e..14764318 100644 --- a/packages/svelte/src/lib/types/general.ts +++ b/packages/svelte/src/lib/types/general.ts @@ -8,6 +8,7 @@ import type { } from '@xyflow/system'; import type { Node } from './nodes'; +import type { Edge } from './edges'; export type KeyModifier = ShortcutModifierDefinition; export type KeyDefinitionObject = { key: string; modifier?: KeyModifier }; @@ -32,3 +33,5 @@ export type HandleComponentProps = { }; export type FitViewOptions = FitViewOptionsBase; + +export type OnDelete = (params: { nodes: Node[]; edges: Edge[] }) => void; From b659443507bf6072cd87c184264dba5437d9de4a Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 16 Nov 2023 13:32:22 +0100 Subject: [PATCH 12/23] feat(react): add onDelete handler --- examples/react/src/examples/Edges/index.tsx | 1 + packages/react/src/components/StoreUpdater/index.tsx | 3 +++ packages/react/src/container/ReactFlow/index.tsx | 2 ++ packages/react/src/hooks/useReactFlow.ts | 3 +++ packages/react/src/types/component-props.ts | 2 ++ packages/react/src/types/general.ts | 1 + packages/react/src/types/store.ts | 2 ++ 7 files changed, 14 insertions(+) diff --git a/examples/react/src/examples/Edges/index.tsx b/examples/react/src/examples/Edges/index.tsx index b5b84fc6..d6870621 100644 --- a/examples/react/src/examples/Edges/index.tsx +++ b/examples/react/src/examples/Edges/index.tsx @@ -196,6 +196,7 @@ const EdgesFlow = () => { onEdgeMouseEnter={onEdgeMouseEnter} onEdgeMouseMove={onEdgeMouseMove} onEdgeMouseLeave={onEdgeMouseLeave} + onDelete={console.log} > diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx index 05a8aa50..65f1bb3c 100644 --- a/packages/react/src/components/StoreUpdater/index.tsx +++ b/packages/react/src/components/StoreUpdater/index.tsx @@ -43,6 +43,7 @@ type StoreUpdaterProps = Pick< | 'fitViewOptions' | 'onNodesDelete' | 'onEdgesDelete' + | 'onDelete' | 'onNodeDragStart' | 'onNodeDrag' | 'onNodeDragStop' @@ -128,6 +129,7 @@ const StoreUpdater = ({ fitViewOptions, onNodesDelete, onEdgesDelete, + onDelete, onNodeDrag, onNodeDragStart, onNodeDragStop, @@ -192,6 +194,7 @@ const StoreUpdater = ({ useDirectStoreUpdater('fitViewOnInitOptions', fitViewOptions, store.setState); useDirectStoreUpdater('onNodesDelete', onNodesDelete, store.setState); useDirectStoreUpdater('onEdgesDelete', onEdgesDelete, store.setState); + useDirectStoreUpdater('onDelete', onDelete, store.setState); useDirectStoreUpdater('onNodeDrag', onNodeDrag, store.setState); useDirectStoreUpdater('onNodeDragStart', onNodeDragStart, store.setState); useDirectStoreUpdater('onNodeDragStop', onNodeDragStop, store.setState); diff --git a/packages/react/src/container/ReactFlow/index.tsx b/packages/react/src/container/ReactFlow/index.tsx index f39650a4..d7995a53 100644 --- a/packages/react/src/container/ReactFlow/index.tsx +++ b/packages/react/src/container/ReactFlow/index.tsx @@ -82,6 +82,7 @@ const ReactFlow = forwardRef( onNodeDragStop, onNodesDelete, onEdgesDelete, + onDelete, onSelectionChange, onSelectionDragStart, onSelectionDrag, @@ -281,6 +282,7 @@ const ReactFlow = forwardRef( fitViewOptions={fitViewOptions} onNodesDelete={onNodesDelete} onEdgesDelete={onEdgesDelete} + onDelete={onDelete} onNodeDragStart={onNodeDragStart} onNodeDrag={onNodeDrag} onNodeDragStop={onNodeDragStop} diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index e72c3a4f..257dd0f2 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -127,6 +127,7 @@ export default function useReactFlow(): ReactFlo onEdgesDelete, onNodesChange, onEdgesChange, + onDelete, } = store.getState(); const { matchingNodes, matchingEdges } = getElementsToRemove({ nodesToRemove: nodesDeleted || [], @@ -171,6 +172,8 @@ export default function useReactFlow(): ReactFlo onNodesChange(nodeChanges); } } + + onDelete?.({ nodes: matchingNodes, edges: matchingEdges }); } return { deletedNodes: matchingNodes, deletedEdges: matchingEdges }; diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index 2ce0fc70..583b3d86 100644 --- a/packages/react/src/types/component-props.ts +++ b/packages/react/src/types/component-props.ts @@ -34,6 +34,7 @@ import type { FitViewOptions, OnNodesDelete, OnEdgesDelete, + OnDelete, OnNodesChange, OnEdgesChange, NodeDragHandler, @@ -70,6 +71,7 @@ export type ReactFlowProps = Omit, 'onError'> & { onEdgesChange?: OnEdgesChange; onNodesDelete?: OnNodesDelete; onEdgesDelete?: OnEdgesDelete; + onDelete?: OnDelete; onSelectionDragStart?: SelectionDragHandler; onSelectionDrag?: SelectionDragHandler; onSelectionDragStop?: SelectionDragHandler; diff --git a/packages/react/src/types/general.ts b/packages/react/src/types/general.ts index 225e159c..b2e7098a 100644 --- a/packages/react/src/types/general.ts +++ b/packages/react/src/types/general.ts @@ -21,6 +21,7 @@ export type OnEdgesChange = (changes: EdgeChange[]) => void; export type OnNodesDelete = (nodes: Node[]) => void; export type OnEdgesDelete = (edges: Edge[]) => void; +export type OnDelete = (params: { nodes: Node[]; edges: Edge[] }) => void; export type NodeTypes = { [key: string]: ComponentType }; export type NodeTypesWrapped = { [key: string]: MemoExoticComponent> }; diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index 02a7d162..c473cbd5 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -38,6 +38,7 @@ import type { OnEdgesDelete, OnSelectionChangeFunc, UnselectNodesAndEdgesParams, + OnDelete, } from '.'; export type ReactFlowStore = { @@ -118,6 +119,7 @@ export type ReactFlowStore = { onNodesDelete?: OnNodesDelete; onEdgesDelete?: OnEdgesDelete; + onDelete?: OnDelete; onError?: OnError; // event handlers From b8affc1c82b2617fca41a0625f1c49bb74194b6a Mon Sep 17 00:00:00 2001 From: peterkogo <7165378+peterkogo@users.noreply.github.com> Date: Mon, 20 Nov 2023 18:40:43 +0100 Subject: [PATCH 13/23] Svelte NodeToolbar with E2E (#3643) * Added NodeToolbar * Added comments & fixed default behaviour in svelte * Added e2e-tests for NodeToolbar component & added data-id to react NodeToolbar * refactor(svelte/NodeToolbar): cleanup * refactor(node-toolbar): add system utils --------- Co-authored-by: moklick --- .../node-toolbar/components/ToolbarNode.tsx | 19 ++++ .../src/generic-tests/node-toolbar/general.ts | 50 ++++++++++ .../src/components/Header/Header.svelte | 1 + .../components/ToolbarNode.svelte | 30 ++++++ .../src/generic-tests/node-toolbar/general.ts | 50 ++++++++++ .../routes/examples/node-toolbar/+page.svelte | 63 +++++++++++++ .../examples/node-toolbar/CustomNode.svelte | 31 ++++++ .../node-toolbar/SelectedNodesToolbar.svelte | 12 +++ .../NodeToolbar/NodeToolbar.tsx | 65 ++++--------- .../NodeToolbar/types.ts | 4 +- .../svelte/src/lib/actions/portal/index.ts | 20 +++- .../EdgeLabelRenderer.svelte | 5 +- packages/svelte/src/lib/index.ts | 1 + .../plugins/NodeToolbar/NodeToolbar.svelte | 94 +++++++++++++++++++ .../src/lib/plugins/NodeToolbar/index.ts | 2 + .../src/lib/plugins/NodeToolbar/types.ts | 9 ++ packages/system/src/types/nodes.ts | 2 + packages/system/src/utils/index.ts | 1 + packages/system/src/utils/node-toolbar.ts | 49 ++++++++++ tests/playwright/e2e/node-toolbar.spec.ts | 93 ++++++++++++++++++ tests/playwright/e2e/pane.spec.ts | 18 +--- tests/playwright/e2e/utils.ts | 16 ++++ 22 files changed, 564 insertions(+), 71 deletions(-) create mode 100644 examples/react/src/generic-tests/node-toolbar/components/ToolbarNode.tsx create mode 100644 examples/react/src/generic-tests/node-toolbar/general.ts create mode 100644 examples/svelte/src/generic-tests/node-toolbar/components/ToolbarNode.svelte create mode 100644 examples/svelte/src/generic-tests/node-toolbar/general.ts create mode 100644 examples/svelte/src/routes/examples/node-toolbar/+page.svelte create mode 100644 examples/svelte/src/routes/examples/node-toolbar/CustomNode.svelte create mode 100644 examples/svelte/src/routes/examples/node-toolbar/SelectedNodesToolbar.svelte create mode 100644 packages/svelte/src/lib/plugins/NodeToolbar/NodeToolbar.svelte create mode 100644 packages/svelte/src/lib/plugins/NodeToolbar/index.ts create mode 100644 packages/svelte/src/lib/plugins/NodeToolbar/types.ts create mode 100644 packages/system/src/utils/node-toolbar.ts create mode 100644 tests/playwright/e2e/node-toolbar.spec.ts create mode 100644 tests/playwright/e2e/utils.ts diff --git a/examples/react/src/generic-tests/node-toolbar/components/ToolbarNode.tsx b/examples/react/src/generic-tests/node-toolbar/components/ToolbarNode.tsx new file mode 100644 index 00000000..30384445 --- /dev/null +++ b/examples/react/src/generic-tests/node-toolbar/components/ToolbarNode.tsx @@ -0,0 +1,19 @@ +import { memo, FC } from 'react'; +import { Handle, Position, NodeProps, NodeToolbar } from '@xyflow/react'; + +const CustomNode: FC = ({ id, data }) => { + return ( + <> + + + + + +
{data.label}
+ + + + ); +}; + +export default memo(CustomNode); diff --git a/examples/react/src/generic-tests/node-toolbar/general.ts b/examples/react/src/generic-tests/node-toolbar/general.ts new file mode 100644 index 00000000..d7df3fa9 --- /dev/null +++ b/examples/react/src/generic-tests/node-toolbar/general.ts @@ -0,0 +1,50 @@ +import { Position, type Node } from '@xyflow/react'; +import ToolbarNode from './components/ToolbarNode'; + +const positions = ['top', 'right', 'bottom', 'left']; +const alignments = ['start', 'center', 'end']; + +const nodes: Node[] = [ + { + id: 'default-node', + type: 'ToolbarNode', + data: { label: 'toolbar top', toolbarPosition: Position.Top }, + position: { x: 0, y: -200 }, + className: 'react-flow__node-default', + }, +]; + +positions.forEach((position, posIndex) => { + alignments.forEach((align, alignIndex) => { + const id = `node-${align}-${position}`; + nodes.push({ + id, + type: 'ToolbarNode', + data: { + label: `toolbar ${position} ${align}`, + toolbarPosition: position as Position, + toolbarAlign: align, + toolbarVisible: true, + }, + className: 'react-flow__node-default', + position: { x: posIndex * 300, y: alignIndex * 100 }, + }); + }); +}); + +export default { + flowProps: { + fitView: true, + nodeTypes: { + ToolbarNode, + }, + nodes, + edges: [ + { + id: 'first-edge', + source: 'default-node', + target: 'node-start-top', + }, + ], + }, +} satisfies FlowConfig; diff --git a/examples/svelte/src/components/Header/Header.svelte b/examples/svelte/src/components/Header/Header.svelte index 0bd941b7..fde20817 100644 --- a/examples/svelte/src/components/Header/Header.svelte +++ b/examples/svelte/src/components/Header/Header.svelte @@ -12,6 +12,7 @@ 'figma', 'interaction', 'intersections', + 'node-toolbar', 'overview', 'stress', 'subflows', diff --git a/examples/svelte/src/generic-tests/node-toolbar/components/ToolbarNode.svelte b/examples/svelte/src/generic-tests/node-toolbar/components/ToolbarNode.svelte new file mode 100644 index 00000000..0a998217 --- /dev/null +++ b/examples/svelte/src/generic-tests/node-toolbar/components/ToolbarNode.svelte @@ -0,0 +1,30 @@ + + + + + + + +
+
{data.label}
+ + +
+ + diff --git a/examples/svelte/src/generic-tests/node-toolbar/general.ts b/examples/svelte/src/generic-tests/node-toolbar/general.ts new file mode 100644 index 00000000..6fce800f --- /dev/null +++ b/examples/svelte/src/generic-tests/node-toolbar/general.ts @@ -0,0 +1,50 @@ +import { Position, type Node } from '@xyflow/svelte'; +import ToolbarNode from './components/ToolbarNode.svelte'; + +const positions = ['top', 'right', 'bottom', 'left']; +const alignments = ['start', 'center', 'end']; + +const nodes: Node[] = [ + { + id: 'default-node', + type: 'ToolbarNode', + data: { label: 'toolbar top', toolbarPosition: Position.Top }, + position: { x: 0, y: -200 }, + class: 'react-flow__node-default' + } +]; + +positions.forEach((position, posIndex) => { + alignments.forEach((align, alignIndex) => { + const id = `node-${align}-${position}`; + nodes.push({ + id, + type: 'ToolbarNode', + data: { + label: `toolbar ${position} ${align}`, + toolbarPosition: position as Position, + toolbarAlign: align, + toolbarVisible: true + }, + class: 'react-flow__node-default', + position: { x: posIndex * 300, y: alignIndex * 100 } + }); + }); +}); + +export default { + flowProps: { + fitView: true, + nodeTypes: { + ToolbarNode + }, + nodes, + edges: [ + { + id: 'first-edge', + source: 'default-node', + target: 'node-start-top' + } + ] + } +} satisfies FlowConfig; diff --git a/examples/svelte/src/routes/examples/node-toolbar/+page.svelte b/examples/svelte/src/routes/examples/node-toolbar/+page.svelte new file mode 100644 index 00000000..1a4360e5 --- /dev/null +++ b/examples/svelte/src/routes/examples/node-toolbar/+page.svelte @@ -0,0 +1,63 @@ + + +
+ + + + +
diff --git a/examples/svelte/src/routes/examples/node-toolbar/CustomNode.svelte b/examples/svelte/src/routes/examples/node-toolbar/CustomNode.svelte new file mode 100644 index 00000000..af2dabaa --- /dev/null +++ b/examples/svelte/src/routes/examples/node-toolbar/CustomNode.svelte @@ -0,0 +1,31 @@ + + + + + + + +
+
{data.label}
+ + +
+ + diff --git a/examples/svelte/src/routes/examples/node-toolbar/SelectedNodesToolbar.svelte b/examples/svelte/src/routes/examples/node-toolbar/SelectedNodesToolbar.svelte new file mode 100644 index 00000000..df62b856 --- /dev/null +++ b/examples/svelte/src/routes/examples/node-toolbar/SelectedNodesToolbar.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx b/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx index ad581bcf..c880edb7 100644 --- a/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx +++ b/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx @@ -1,13 +1,13 @@ import { useCallback, CSSProperties } from 'react'; import cc from 'classcat'; import { shallow } from 'zustand/shallow'; -import { getNodesBounds, Transform, Rect, Position, internalsSymbol } from '@xyflow/system'; +import { getNodesBounds, Rect, Position, internalsSymbol, getNodeToolbarTransform } from '@xyflow/system'; import { Node, ReactFlowState } from '../../types'; import { useStore } from '../../hooks/useStore'; import { useNodeId } from '../../contexts/NodeIdContext'; import NodeToolbarPortal from './NodeToolbarPortal'; -import { Align, NodeToolbarProps } from './types'; +import { NodeToolbarProps } from './types'; const nodeEqualityFn = (a: Node | undefined, b: Node | undefined) => a?.positionAbsolute?.x === b?.positionAbsolute?.x && @@ -22,53 +22,15 @@ const nodesEqualityFn = (a: Node[], b: Node[]) => { }; const storeSelector = (state: ReactFlowState) => ({ - transform: state.transform, + viewport: { + x: state.transform[0], + y: state.transform[1], + zoom: state.transform[2], + }, nodeOrigin: state.nodeOrigin, selectedNodesCount: state.nodes.filter((node) => node.selected).length, }); -function getTransform(nodeRect: Rect, transform: Transform, position: Position, offset: number, align: Align): string { - let alignmentOffset = 0.5; - - if (align === 'start') { - alignmentOffset = 0; - } else if (align === 'end') { - alignmentOffset = 1; - } - - // position === Position.Top - // we set the x any y position of the toolbar based on the nodes position - let pos = [ - (nodeRect.x + nodeRect.width * alignmentOffset) * transform[2] + transform[0], - nodeRect.y * transform[2] + transform[1] - offset, - ]; - // and than shift it based on the alignment. The shift values are in %. - let shift = [-100 * alignmentOffset, -100]; - - switch (position) { - case Position.Right: - pos = [ - (nodeRect.x + nodeRect.width) * transform[2] + transform[0] + offset, - (nodeRect.y + nodeRect.height * alignmentOffset) * transform[2] + transform[1], - ]; - shift = [0, -100 * alignmentOffset]; - break; - case Position.Bottom: - pos[1] = (nodeRect.y + nodeRect.height) * transform[2] + transform[1] + offset; - shift[1] = 0; - break; - case Position.Left: - pos = [ - nodeRect.x * transform[2] + transform[0] - offset, - (nodeRect.y + nodeRect.height * alignmentOffset) * transform[2] + transform[1], - ]; - shift = [-100, -100 * alignmentOffset]; - break; - } - - return `translate(${pos[0]}px, ${pos[1]}px) translate(${shift[0]}%, ${shift[1]}%)`; -} - function NodeToolbar({ nodeId, children, @@ -97,7 +59,9 @@ function NodeToolbar({ [nodeId, contextNodeId] ); const nodes = useStore(nodesSelector, nodesEqualityFn); - const { transform, nodeOrigin, selectedNodesCount } = useStore(storeSelector, shallow); + const { viewport, nodeOrigin, selectedNodesCount } = useStore(storeSelector, shallow); + + // if isVisible is not set, we show the toolbar only if its node is selected and no other node is selected const isActive = typeof isVisible === 'boolean' ? isVisible : nodes.length === 1 && nodes[0].selected && selectedNodesCount === 1; @@ -110,14 +74,19 @@ function NodeToolbar({ const wrapperStyle: CSSProperties = { position: 'absolute', - transform: getTransform(nodeRect, transform, position, offset, align), + transform: getNodeToolbarTransform(nodeRect, viewport, position, offset, align), zIndex, ...style, }; return ( -
+
`${acc}${node.id} `, '').trim()} + > {children}
diff --git a/packages/react/src/additional-components/NodeToolbar/types.ts b/packages/react/src/additional-components/NodeToolbar/types.ts index 29478576..f08859dc 100644 --- a/packages/react/src/additional-components/NodeToolbar/types.ts +++ b/packages/react/src/additional-components/NodeToolbar/types.ts @@ -1,5 +1,5 @@ import type { HTMLAttributes } from 'react'; -import type { Position } from '@xyflow/system'; +import type { Position, Align } from '@xyflow/system'; export type NodeToolbarProps = HTMLAttributes & { nodeId?: string | string[]; @@ -8,5 +8,3 @@ export type NodeToolbarProps = HTMLAttributes & { offset?: number; align?: Align; }; - -export type Align = 'center' | 'start' | 'end'; diff --git a/packages/svelte/src/lib/actions/portal/index.ts b/packages/svelte/src/lib/actions/portal/index.ts index 714e8b3e..fba6562c 100644 --- a/packages/svelte/src/lib/actions/portal/index.ts +++ b/packages/svelte/src/lib/actions/portal/index.ts @@ -1,11 +1,27 @@ -export default function (node: Element, target = 'body') { - const targetEl = document.querySelector(target); +type PortalOptions = { + target?: string; + domNode: Element | null; +}; + +function tryToMount(node: Element, domNode: Element | null, target: string | undefined) { + if (!domNode) { + return; + } + + const targetEl = target ? domNode.querySelector(target) : domNode; if (targetEl) { targetEl.appendChild(node); } +} + +export default function (node: Element, { target, domNode }: PortalOptions) { + tryToMount(node, domNode, target); return { + async update({ target, domNode }: PortalOptions) { + tryToMount(node, domNode, target); + }, destroy() { if (node.parentNode) { node.parentNode.removeChild(node); diff --git a/packages/svelte/src/lib/components/EdgeLabelRenderer/EdgeLabelRenderer.svelte b/packages/svelte/src/lib/components/EdgeLabelRenderer/EdgeLabelRenderer.svelte index d681d388..f3cfeca9 100644 --- a/packages/svelte/src/lib/components/EdgeLabelRenderer/EdgeLabelRenderer.svelte +++ b/packages/svelte/src/lib/components/EdgeLabelRenderer/EdgeLabelRenderer.svelte @@ -1,9 +1,12 @@ -
+
diff --git a/packages/svelte/src/lib/index.ts b/packages/svelte/src/lib/index.ts index b8620d79..88bc483f 100644 --- a/packages/svelte/src/lib/index.ts +++ b/packages/svelte/src/lib/index.ts @@ -14,6 +14,7 @@ export * from '$lib/components/Handle'; export * from '$lib/plugins/Controls'; export * from '$lib/plugins/Background'; export * from '$lib/plugins/Minimap'; +export * from '$lib/plugins/NodeToolbar'; // store export { useStore } from '$lib/store'; diff --git a/packages/svelte/src/lib/plugins/NodeToolbar/NodeToolbar.svelte b/packages/svelte/src/lib/plugins/NodeToolbar/NodeToolbar.svelte new file mode 100644 index 00000000..b01bbb48 --- /dev/null +++ b/packages/svelte/src/lib/plugins/NodeToolbar/NodeToolbar.svelte @@ -0,0 +1,94 @@ + + +{#if $domNode && isActive && toolbarNodes} +
`${acc}${node.id} `, '').trim()} + class="svelte-flow__node-toolbar" + use:portal={{ domNode: $domNode }} + style:position="absolute" + style:transform + style:z-index={zIndex} + > + +
+{/if} diff --git a/packages/svelte/src/lib/plugins/NodeToolbar/index.ts b/packages/svelte/src/lib/plugins/NodeToolbar/index.ts new file mode 100644 index 00000000..7c38ab3d --- /dev/null +++ b/packages/svelte/src/lib/plugins/NodeToolbar/index.ts @@ -0,0 +1,2 @@ +export { default as NodeToolbar } from './NodeToolbar.svelte'; +export * from './types'; diff --git a/packages/svelte/src/lib/plugins/NodeToolbar/types.ts b/packages/svelte/src/lib/plugins/NodeToolbar/types.ts new file mode 100644 index 00000000..6c812de2 --- /dev/null +++ b/packages/svelte/src/lib/plugins/NodeToolbar/types.ts @@ -0,0 +1,9 @@ +import type { Position, Align } from '@xyflow/system'; + +export type NodeToolbarProps = { + nodeId?: string | string[]; + position?: Position; + align?: Align; + offset?: number; + isVisible?: boolean; +}; diff --git a/packages/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts index 22b4e6ab..cea41c65 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -97,3 +97,5 @@ export type OnNodeDrag = (event: MouseEvent, node: NodeBase, nodes: NodeBase[]) export type OnSelectionDrag = (event: MouseEvent, nodes: NodeBase[]) => void; export type NodeHandle = Optional; + +export type Align = 'center' | 'start' | 'end'; diff --git a/packages/system/src/utils/index.ts b/packages/system/src/utils/index.ts index 30a7e961..380b7658 100644 --- a/packages/system/src/utils/index.ts +++ b/packages/system/src/utils/index.ts @@ -3,4 +3,5 @@ export * from './edges'; export * from './graph'; export * from './general'; export * from './marker'; +export * from './node-toolbar'; export * from './store'; diff --git a/packages/system/src/utils/node-toolbar.ts b/packages/system/src/utils/node-toolbar.ts new file mode 100644 index 00000000..1813513c --- /dev/null +++ b/packages/system/src/utils/node-toolbar.ts @@ -0,0 +1,49 @@ +import { Position, type Rect, type Viewport, type Align } from '../'; + +export function getNodeToolbarTransform( + nodeRect: Rect, + viewport: Viewport, + position: Position, + offset: number, + align: Align +): string { + let alignmentOffset = 0.5; + + if (align === 'start') { + alignmentOffset = 0; + } else if (align === 'end') { + alignmentOffset = 1; + } + + // position === Position.Top + // we set the x any y position of the toolbar based on the nodes position + let pos = [ + (nodeRect.x + nodeRect.width * alignmentOffset) * viewport.zoom + viewport.x, + nodeRect.y * viewport.zoom + viewport.y - offset, + ]; + // and than shift it based on the alignment. The shift values are in %. + let shift = [-100 * alignmentOffset, -100]; + + switch (position) { + case Position.Right: + pos = [ + (nodeRect.x + nodeRect.width) * viewport.zoom + viewport.x + offset, + (nodeRect.y + nodeRect.height * alignmentOffset) * viewport.zoom + viewport.y, + ]; + shift = [0, -100 * alignmentOffset]; + break; + case Position.Bottom: + pos[1] = (nodeRect.y + nodeRect.height) * viewport.zoom + viewport.y + offset; + shift[1] = 0; + break; + case Position.Left: + pos = [ + nodeRect.x * viewport.zoom + viewport.x - offset, + (nodeRect.y + nodeRect.height * alignmentOffset) * viewport.zoom + viewport.y, + ]; + shift = [-100, -100 * alignmentOffset]; + break; + } + + return `translate(${pos[0]}px, ${pos[1]}px) translate(${shift[0]}%, ${shift[1]}%)`; +} diff --git a/tests/playwright/e2e/node-toolbar.spec.ts b/tests/playwright/e2e/node-toolbar.spec.ts new file mode 100644 index 00000000..daf76070 --- /dev/null +++ b/tests/playwright/e2e/node-toolbar.spec.ts @@ -0,0 +1,93 @@ +import { test, expect, Locator } from '@playwright/test'; +import { FRAMEWORK } from './constants'; + +type Position = 'top' | 'right' | 'bottom' | 'left'; +const positions: Position[] = ['top', 'right', 'bottom', 'left']; + +type Alignment = 'start' | 'center' | 'end'; +const alignments: Alignment[] = ['start', 'center', 'end']; +type Permutation = { + id: string; + position: Position; + align: Alignment; +}; +const permutations: Permutation[] = []; + +positions.forEach((position) => { + alignments.forEach((align) => { + permutations.push({ + id: `node-${align}-${position}`, + position, + align, + }); + }); +}); + +test.describe('Node Toolbar', async () => { + test.beforeEach(async ({ page }) => { + // Go to the starting url before each test. + await page.goto('/tests/generic/node-toolbar/general'); + // Wait till the edges are rendered + await page.waitForSelector('[data-id="first-edge"]', { timeout: 5000 }); + }); + + test('all toolbars are positioned correctly', async ({ page }) => { + permutations.forEach(async (permutation) => { + const toolbar = page + .locator(`[data-id="${permutation.id}"]`) + .and(page.locator(`.${FRAMEWORK}-flow__node-toolbar`)); + const node = page.locator(`[data-id="${permutation.id}"]`).and(page.locator(`.${FRAMEWORK}-flow__node`)); + + await expect(toolbar).toBeAttached(); + await expect(node).toBeAttached(); + + const toolbarBox = await toolbar.boundingBox(); + const nodeBox = await node.boundingBox(); + + switch (permutation.position) { + case 'top': + expect(toolbarBox!.y).toBeLessThan(nodeBox!.y); + break; + case 'right': + expect(toolbarBox!.x).toBeGreaterThan(nodeBox!.x); + break; + case 'bottom': + expect(toolbarBox!.y).toBeGreaterThan(nodeBox!.y); + break; + case 'left': + expect(toolbarBox!.x).toBeLessThan(nodeBox!.x); + break; + } + + const dimension = permutation.position === 'top' || permutation.position === 'bottom' ? 'x' : 'y'; + const extent = permutation.position === 'top' || permutation.position === 'bottom' ? 'width' : 'height'; + + switch (permutation.align) { + case 'start': + expect(Math.floor(toolbarBox![dimension])).toBe(Math.floor(nodeBox![dimension])); + break; + case 'center': + expect(Math.floor(toolbarBox![dimension] + toolbarBox![extent] * 0.5)).toBe( + Math.floor(nodeBox![dimension] + nodeBox![extent] * 0.5) + ); + break; + case 'end': + expect(Math.floor(toolbarBox![dimension] + toolbarBox![extent])).toBe( + Math.floor(nodeBox![dimension] + nodeBox![extent]) + ); + break; + } + }); + }); + + test('toolbar default behaviour', async ({ page }) => { + const node = page.locator('[data-id="default-node"]').and(page.locator(`.${FRAMEWORK}-flow__node`)); + const toolbar = page.locator('[data-id="default-node"]').and(page.locator(`.${FRAMEWORK}-flow__node-toolbar`)); + + await expect(node).toBeAttached(); + await expect(toolbar).not.toBeAttached(); + + await node.click(); + await expect(toolbar).toBeAttached(); + }); +}); diff --git a/tests/playwright/e2e/pane.spec.ts b/tests/playwright/e2e/pane.spec.ts index 31777c97..15a79655 100644 --- a/tests/playwright/e2e/pane.spec.ts +++ b/tests/playwright/e2e/pane.spec.ts @@ -1,23 +1,7 @@ import { test, expect } from '@playwright/test'; import { FRAMEWORK } from './constants'; - -const MATCH_ALL_NUMBERS = /[\d\.]+/g; - -// Type "Locator" not exported... -async function getTransform(element) { - const transformString = await element.evaluate((el) => { - return el.style.transform; - }); - - // Parses all numbers in f.ex "translate(590px, 324px) scale(2)"" - const transforms = transformString.match(MATCH_ALL_NUMBERS); - return { - translateX: parseFloat(transforms![0]), - translateY: parseFloat(transforms![1]), - scale: parseFloat(transforms![2]), - }; -} +import { getTransform } from './utils'; test.describe('PANE DEFAULT', () => { test.beforeEach(async ({ page }) => { diff --git a/tests/playwright/e2e/utils.ts b/tests/playwright/e2e/utils.ts new file mode 100644 index 00000000..76643dd7 --- /dev/null +++ b/tests/playwright/e2e/utils.ts @@ -0,0 +1,16 @@ +const MATCH_ALL_NUMBERS = /[\d\.]+/g; + +// Type "Locator" not exported... +export async function getTransform(element) { + const transformString = await element.evaluate((el) => { + return el.style.transform; + }); + + // Parses all numbers in f.ex "translate(590px, 324px) scale(2)"" + const transforms = transformString.match(MATCH_ALL_NUMBERS); + return { + translateX: parseFloat(transforms![0]), + translateY: parseFloat(transforms![1]), + scale: parseFloat(transforms![2]), + }; +} From 8a5dd652b3b6eb88bb759d3b81e991216af0a6a3 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 21 Nov 2023 12:59:12 +0100 Subject: [PATCH 14/23] refactor(svelte): node wrapper position types --- .../lib/components/NodeWrapper/NodeWrapper.svelte | 12 +++++++----- .../svelte/src/lib/components/NodeWrapper/types.ts | 7 ++++--- .../lib/container/NodeRenderer/NodeRenderer.svelte | 6 ++++-- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index 29631836..33a954b3 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -27,8 +27,10 @@ export let style: NodeWrapperProps['style'] = undefined; export let type: NodeWrapperProps['type'] = 'default'; export let isParent: NodeWrapperProps['isParent'] = false; - export let positionAbsolute: NodeWrapperProps['positionAbsolute'] = undefined; - export let positionOrigin: NodeWrapperProps['positionOrigin'] = undefined; + export let positionX: NodeWrapperProps['positionX']; + export let positionY: NodeWrapperProps['positionY']; + export let positionOriginX: NodeWrapperProps['positionOriginX']; + export let positionOriginY: NodeWrapperProps['positionOriginY']; export let sourcePosition: NodeWrapperProps['sourcePosition'] = undefined; export let targetPosition: NodeWrapperProps['targetPosition'] = undefined; export let zIndex: NodeWrapperProps['zIndex']; @@ -164,7 +166,7 @@ class:nopan={draggable} class:parent={isParent} style:z-index={zIndex} - style:transform="translate({positionOrigin?.x ?? 0}px, {positionOrigin?.y ?? 0}px)" + style:transform="translate({positionOriginX}px, {positionOriginY}px)" style:visibility={initialized ? 'visible' : 'hidden'} style="{style} {node.size?.width ? `;width=${node.size?.width}px` : ''} {node.size?.height ? `;height=${node.size?.height}px;` @@ -187,8 +189,8 @@ {dragging} {dragHandle} isConnectable={connectable} - xPos={positionAbsolute?.x ?? 0} - yPos={positionAbsolute?.y ?? 0} + xPos={positionX} + yPos={positionY} on:connectstart on:connect on:connectend diff --git a/packages/svelte/src/lib/components/NodeWrapper/types.ts b/packages/svelte/src/lib/components/NodeWrapper/types.ts index f4f5c310..e833ef54 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/types.ts +++ b/packages/svelte/src/lib/components/NodeWrapper/types.ts @@ -1,4 +1,3 @@ -import type { XYPosition } from '@xyflow/system'; import type { Node } from '$lib/types'; export type NodeWrapperProps = Pick< @@ -9,7 +8,6 @@ export type NodeWrapperProps = Pick< | 'data' | 'draggable' | 'dragging' - | 'positionAbsolute' | 'selected' | 'selectable' | 'style' @@ -21,7 +19,10 @@ export type NodeWrapperProps = Pick< | 'dragHandle' | 'hidden' > & { - positionOrigin?: XYPosition; + positionX: number; + positionY: number; + positionOriginX: number; + positionOriginY: number; 'on:nodeclick'?: (event: MouseEvent) => void; resizeObserver?: ResizeObserver | null; isParent?: boolean; diff --git a/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte b/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte index 6a7acd90..e856b70b 100644 --- a/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte +++ b/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte @@ -61,8 +61,10 @@ node.connectable || ($nodesConnectable && typeof node.connectable === 'undefined') )} - positionAbsolute={node.positionAbsolute} - positionOrigin={posOrigin} + positionX={node.positionAbsolute?.x ?? 0} + positionY={node.positionAbsolute?.y ?? 0} + positionOriginX={posOrigin.x ?? 0} + positionOriginY={posOrigin.y ?? 0} isParent={!!node[internalsSymbol]?.isParent} style={node.style} class={node.class} From d812dbbe53336f9d794ed7005f59896d42dc12e4 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 21 Nov 2023 15:06:42 +0100 Subject: [PATCH 15/23] refactor(nodes): add computed attr for width/height and absolute position --- .../react/src/examples/EasyConnect/utils.tsx | 15 +++----- .../react/src/examples/FloatingEdges/utils.ts | 4 +- .../MiniMap/MiniMapNodes.tsx | 9 +++-- .../NodeToolbar/NodeToolbar.tsx | 4 +- .../src/components/ConnectionLine/index.tsx | 8 ++-- .../react/src/components/Nodes/wrapNode.tsx | 8 ++-- .../src/container/NodeRenderer/index.tsx | 14 +++---- packages/react/src/hooks/useReactFlow.ts | 2 +- .../react/src/hooks/useUpdateNodePositions.ts | 12 ++++-- packages/react/src/store/index.ts | 7 +++- packages/react/src/store/initialState.ts | 6 +-- packages/react/src/types/nodes.ts | 4 +- packages/react/src/utils/changes.ts | 27 ++++++++----- .../components/NodeWrapper/NodeWrapper.svelte | 6 +-- .../src/lib/components/NodeWrapper/types.ts | 3 +- .../NodeRenderer/NodeRenderer.svelte | 19 ++++++---- .../svelte/src/lib/hooks/useSvelteFlow.ts | 2 +- .../src/lib/hooks/useUpdateNodeInternals.ts | 2 +- .../src/lib/plugins/Minimap/Minimap.svelte | 6 +-- .../src/lib/store/derived-connection-props.ts | 10 +++-- packages/svelte/src/lib/store/index.ts | 17 +++++---- packages/system/src/types/nodes.ts | 12 +++--- packages/system/src/utils/dom.ts | 2 + packages/system/src/utils/edges/positions.ts | 12 +++--- packages/system/src/utils/general.ts | 8 ++-- packages/system/src/utils/graph.ts | 38 ++++++++++--------- packages/system/src/utils/store.ts | 17 ++++++--- packages/system/src/xydrag/XYDrag.ts | 12 +++--- packages/system/src/xydrag/utils.ts | 21 ++++++---- packages/system/src/xyhandle/utils.ts | 4 +- 30 files changed, 177 insertions(+), 134 deletions(-) diff --git a/examples/react/src/examples/EasyConnect/utils.tsx b/examples/react/src/examples/EasyConnect/utils.tsx index cef818f5..d7f3e689 100644 --- a/examples/react/src/examples/EasyConnect/utils.tsx +++ b/examples/react/src/examples/EasyConnect/utils.tsx @@ -4,12 +4,9 @@ import { Node, Position, MarkerType, XYPosition } from '@xyflow/react'; // of the line between the center of the intersectionNode and the target node function getNodeIntersection(intersectionNode: Node, targetNode: Node) { // https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a - const { - width: intersectionNodeWidth, - height: intersectionNodeHeight, - positionAbsolute: intersectionNodePosition, - } = intersectionNode; - const targetPosition = targetNode.positionAbsolute!; + const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode; + const intersectionNodePosition = intersectionNode.computed?.positionAbsolute!; + const targetPosition = targetNode.computed?.positionAbsolute!; const w = intersectionNodeWidth! / 2; const h = intersectionNodeHeight! / 2; @@ -32,7 +29,7 @@ function getNodeIntersection(intersectionNode: Node, targetNode: Node) { // returns the position (top,right,bottom or right) passed node compared to the intersection point function getEdgePosition(node: Node, intersectionPoint: XYPosition) { - const n = { ...node.positionAbsolute, ...node }; + const n = { ...node.computed?.positionAbsolute, ...node }; const nx = Math.round(n.x!); const ny = Math.round(n.y!); const px = Math.round(intersectionPoint.x); @@ -41,13 +38,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) { if (px <= nx + 1) { return Position.Left; } - if (px >= nx + n.width! - 1) { + if (px >= nx + n.computed?.width! - 1) { return Position.Right; } if (py <= ny + 1) { return Position.Top; } - if (py >= n.y! + n.height! - 1) { + if (py >= n.y! + n.computed?.height! - 1) { return Position.Bottom; } diff --git a/examples/react/src/examples/FloatingEdges/utils.ts b/examples/react/src/examples/FloatingEdges/utils.ts index 543b0945..0a2d6c03 100644 --- a/examples/react/src/examples/FloatingEdges/utils.ts +++ b/examples/react/src/examples/FloatingEdges/utils.ts @@ -42,13 +42,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) { if (px <= nx + 1) { return Position.Left; } - if (px >= nx + (n.width ?? 0) - 1) { + if (px >= nx + (n.computed?.width ?? 0) - 1) { return Position.Right; } if (py <= ny + 1) { return Position.Top; } - if (py >= n.y + (n.height ?? 0) - 1) { + if (py >= n.y + (n.computed?.height ?? 0) - 1) { return Position.Bottom; } diff --git a/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx b/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx index 21bb8fc5..d72700a7 100644 --- a/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx +++ b/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx @@ -12,7 +12,10 @@ import type { MiniMapNodes, GetMiniMapNodeAttribute } from './types'; declare const window: any; const selector = (s: ReactFlowState) => s.nodeOrigin; -const selectorNodes = (s: ReactFlowState) => s.nodes.filter((node) => !node.hidden && node.width && node.height); +const selectorNodes = (s: ReactFlowState) => + s.nodes.filter( + (node) => !node.hidden && (node.computed?.width || node.width) && (node.computed?.height || node.height) + ); const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func); function MiniMapNodes({ @@ -44,8 +47,8 @@ function MiniMapNodes({ key={node.id} x={x} y={y} - width={node.width!} - height={node.height!} + width={node.computed?.width ?? node.width ?? 0} + height={node.computed?.height ?? node.height ?? 0} style={node.style} selected={!!node.selected} className={nodeClassNameFunc(node)} diff --git a/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx b/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx index c880edb7..b7028a76 100644 --- a/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx +++ b/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx @@ -10,8 +10,8 @@ import NodeToolbarPortal from './NodeToolbarPortal'; import { NodeToolbarProps } from './types'; const nodeEqualityFn = (a: Node | undefined, b: Node | undefined) => - a?.positionAbsolute?.x === b?.positionAbsolute?.x && - a?.positionAbsolute?.y === b?.positionAbsolute?.y && + a?.computed?.positionAbsolute?.x === b?.computed?.positionAbsolute?.x && + a?.computed?.positionAbsolute?.y === b?.computed?.positionAbsolute?.y && a?.width === b?.width && a?.height === b?.height && a?.selected === b?.selected && diff --git a/packages/react/src/components/ConnectionLine/index.tsx b/packages/react/src/components/ConnectionLine/index.tsx index ed1a2115..bdf73089 100644 --- a/packages/react/src/components/ConnectionLine/index.tsx +++ b/packages/react/src/components/ConnectionLine/index.tsx @@ -65,10 +65,10 @@ const ConnectionLine = ({ } const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0]; - const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.width ?? 0) / 2; - const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.height ?? 0; - const fromX = (fromNode.positionAbsolute?.x ?? 0) + fromHandleX; - const fromY = (fromNode.positionAbsolute?.y ?? 0) + fromHandleY; + const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.computed?.width ?? 0) / 2; + const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.computed?.height ?? 0; + const fromX = (fromNode.computed?.positionAbsolute?.x ?? 0) + fromHandleX; + const fromY = (fromNode.computed?.positionAbsolute?.y ?? 0) + fromHandleY; const fromPosition = fromHandle?.position; const toPosition = fromPosition ? oppositePosition[fromPosition] : null; diff --git a/packages/react/src/components/Nodes/wrapNode.tsx b/packages/react/src/components/Nodes/wrapNode.tsx index e50ffcf7..ab56350b 100644 --- a/packages/react/src/components/Nodes/wrapNode.tsx +++ b/packages/react/src/components/Nodes/wrapNode.tsx @@ -52,8 +52,8 @@ export default (NodeComponent: ComponentType) => { disableKeyboardA11y, ariaLabel, rfId, - sizeWidth, - sizeHeight, + width, + height, }: WrapNodeProps) => { const store = useStoreApi(); const nodeRef = useRef(null); @@ -186,8 +186,8 @@ export default (NodeComponent: ComponentType) => { transform: `translate(${xPosOrigin}px,${yPosOrigin}px)`, pointerEvents: hasPointerEvents ? 'all' : 'none', visibility: initialized ? 'visible' : 'hidden', - width: sizeWidth, - height: sizeHeight, + width, + height, ...style, }} data-id={id} diff --git a/packages/react/src/container/NodeRenderer/index.tsx b/packages/react/src/container/NodeRenderer/index.tsx index 98f9fc39..3797bc45 100644 --- a/packages/react/src/container/NodeRenderer/index.tsx +++ b/packages/react/src/container/NodeRenderer/index.tsx @@ -91,19 +91,19 @@ const NodeRenderer = (props: NodeRendererProps) => { const isFocusable = !!(node.focusable || (nodesFocusable && typeof node.focusable === 'undefined')); const clampedPosition = props.nodeExtent - ? clampPosition(node.positionAbsolute, props.nodeExtent) - : node.positionAbsolute; + ? clampPosition(node.computed?.positionAbsolute, props.nodeExtent) + : node.computed?.positionAbsolute; const posX = clampedPosition?.x ?? 0; const posY = clampedPosition?.y ?? 0; const posOrigin = getPositionWithOrigin({ x: posX, y: posY, - width: node.width ?? 0, - height: node.height ?? 0, + width: node.computed?.width ?? node.width ?? 0, + height: node.computed?.height ?? node.height ?? 0, origin: node.origin || props.nodeOrigin, }); - const initialized = (!!node.width && !!node.height) || (!!node.size?.width && !!node.size?.height); + const initialized = (!!node.computed?.width && !!node.computed?.height) || (!!node.width && !!node.height); return ( { id={node.id} className={node.className} style={node.style} - sizeWidth={node.size?.width} - sizeHeight={node.size?.height} + width={node.width ?? undefined} + height={node.height ?? undefined} type={nodeType} data={node.data} sourcePosition={node.sourcePosition || Position.Bottom} diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index 257dd0f2..44e29c14 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -206,7 +206,7 @@ export default function useReactFlow(): ReactFlo } return (nodes || store.getState().nodes).filter((n) => { - if (!isRect && (n.id === node!.id || !n.positionAbsolute)) { + if (!isRect && (n.id === node!.id || !n.computed?.positionAbsolute)) { return false; } diff --git a/packages/react/src/hooks/useUpdateNodePositions.ts b/packages/react/src/hooks/useUpdateNodePositions.ts index d303fb9b..06577601 100644 --- a/packages/react/src/hooks/useUpdateNodePositions.ts +++ b/packages/react/src/hooks/useUpdateNodePositions.ts @@ -23,8 +23,11 @@ function useUpdateNodePositions() { const yDiff = params.y * yVelo * factor; const nodeUpdates = selectedNodes.map((node) => { - if (node.positionAbsolute) { - let nextPosition = { x: node.positionAbsolute.x + xDiff, y: node.positionAbsolute.y + yDiff }; + if (node.computed?.positionAbsolute) { + let nextPosition = { + x: node.computed?.positionAbsolute.x + xDiff, + y: node.computed?.positionAbsolute.y + yDiff, + }; if (snapToGrid) { nextPosition = snapPosition(nextPosition, snapGrid); @@ -40,7 +43,10 @@ function useUpdateNodePositions() { ); node.position = position; - node.positionAbsolute = positionAbsolute; + if (!node.computed) { + node.computed = {}; + } + node.computed.positionAbsolute = positionAbsolute; } return node; diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index a6898f6d..0f50f385 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -147,7 +147,7 @@ const createRFStore = ({ }; if (positionChanged) { - change.positionAbsolute = node.positionAbsolute; + change.positionAbsolute = node.computed?.positionAbsolute; change.position = node.position; } @@ -276,7 +276,10 @@ const createRFStore = ({ return { ...node, - positionAbsolute, + computed: { + ...node.computed, + positionAbsolute, + }, }; }), }); diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index c87c29f1..48e5434f 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -28,11 +28,7 @@ const getInitialState = ({ let transform: Transform = [0, 0, 1]; if (fitView && width && height) { - const nodesWithDimensions = nextNodes.map((node) => ({ - ...node, - width: node.size?.width, - height: node.size?.height, - })); + const nodesWithDimensions = nextNodes.filter((node) => node.width && node.height); const bounds = getNodesBounds(nodesWithDimensions, [0, 0]); const { x, y, zoom } = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1); transform = [x, y, zoom]; diff --git a/packages/react/src/types/nodes.ts b/packages/react/src/types/nodes.ts index 4e648f8e..ab691af5 100644 --- a/packages/react/src/types/nodes.ts +++ b/packages/react/src/types/nodes.ts @@ -40,6 +40,6 @@ export type WrapNodeProps = Pick< noPanClassName: string; rfId: string; disableKeyboardA11y: boolean; - sizeWidth?: number; - sizeHeight?: number; + width?: number; + height?: number; }; diff --git a/packages/react/src/utils/changes.ts b/packages/react/src/utils/changes.ts index 61ff2b19..c113ebe1 100644 --- a/packages/react/src/utils/changes.ts +++ b/packages/react/src/utils/changes.ts @@ -5,14 +5,17 @@ export function handleParentExpand(res: any[], updateItem: any) { const parent = res.find((e) => e.id === updateItem.parentNode); if (parent) { - const extendWidth = updateItem.position.x + updateItem.width - parent.width; - const extendHeight = updateItem.position.y + updateItem.height - parent.height; + if (!parent.computed) { + parent.computed = {}; + } + const extendWidth = updateItem.position.x + updateItem.computed.width - parent.computed.width; + const extendHeight = updateItem.position.y + updateItem.computed.height - parent.computed.height; if (extendWidth > 0 || extendHeight > 0 || updateItem.position.x < 0 || updateItem.position.y < 0) { parent.style = { ...parent.style } || {}; - parent.style.width = parent.style.width ?? parent.width; - parent.style.height = parent.style.height ?? parent.height; + parent.style.width = parent.style.width ?? parent.computed.width; + parent.style.height = parent.style.height ?? parent.computed.height; if (extendWidth > 0) { parent.style.width += extendWidth; @@ -36,8 +39,8 @@ export function handleParentExpand(res: any[], updateItem: any) { updateItem.position.y = 0; } - parent.width = parent.style.width; - parent.height = parent.style.height; + parent.computed.width = parent.style.width; + parent.computed.height = parent.style.height; } } } @@ -87,7 +90,10 @@ function applyChanges(changes: any[], elements: any[]): any[] { } if (typeof currentChange.positionAbsolute !== 'undefined') { - updateItem.positionAbsolute = currentChange.positionAbsolute; + if (!updateItem.computed) { + updateItem.computed = {}; + } + updateItem.computed.positionAbsolute = currentChange.positionAbsolute; } if (typeof currentChange.dragging !== 'undefined') { @@ -101,8 +107,11 @@ function applyChanges(changes: any[], elements: any[]): any[] { } case 'dimensions': { if (typeof currentChange.dimensions !== 'undefined') { - updateItem.width = currentChange.dimensions.width; - updateItem.height = currentChange.dimensions.height; + if (!updateItem.computed) { + updateItem.computed = {}; + } + updateItem.computed.width = currentChange.dimensions.width; + updateItem.computed.height = currentChange.dimensions.height; } if (typeof currentChange.updateStyle !== 'undefined') { diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index 33a954b3..eb2c1472 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -34,6 +34,8 @@ export let sourcePosition: NodeWrapperProps['sourcePosition'] = undefined; export let targetPosition: NodeWrapperProps['targetPosition'] = undefined; export let zIndex: NodeWrapperProps['zIndex']; + export let width: NodeWrapperProps['width'] = undefined; + export let height: NodeWrapperProps['height'] = undefined; export let dragHandle: NodeWrapperProps['dragHandle'] = undefined; export let initialized: NodeWrapperProps['initialized'] = false; let className: string = ''; @@ -168,9 +170,7 @@ style:z-index={zIndex} style:transform="translate({positionOriginX}px, {positionOriginY}px)" style:visibility={initialized ? 'visible' : 'hidden'} - style="{style} {node.size?.width ? `;width=${node.size?.width}px` : ''} {node.size?.height - ? `;height=${node.size?.height}px;` - : ''}" + style="{style} {width ? `;width=${width}px` : ''} {height ? `;height=${height}px;` : ''}" on:click={onSelectNodeHandler} on:mouseenter={(event) => dispatch('nodemouseenter', { node, event })} on:mouseleave={(event) => dispatch('nodemouseleave', { node, event })} diff --git a/packages/svelte/src/lib/components/NodeWrapper/types.ts b/packages/svelte/src/lib/components/NodeWrapper/types.ts index e833ef54..3da65c2f 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/types.ts +++ b/packages/svelte/src/lib/components/NodeWrapper/types.ts @@ -11,14 +11,15 @@ export type NodeWrapperProps = Pick< | 'selected' | 'selectable' | 'style' - | 'type' | 'width' | 'height' + | 'type' | 'sourcePosition' | 'targetPosition' | 'dragHandle' | 'hidden' > & { + type: string; positionX: number; positionY: number; positionOriginX: number; diff --git a/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte b/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte index e856b70b..48ec792b 100644 --- a/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte +++ b/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte @@ -40,10 +40,10 @@
{#each $visibleNodes as node (node.id)} {@const posOrigin = getPositionWithOrigin({ - x: node.positionAbsolute?.x ?? 0, - y: node.positionAbsolute?.y ?? 0, - width: (node.size?.width || node.width) ?? 0, - height: (node.size?.height || node.height) ?? 0, + x: node.computed?.positionAbsolute?.x ?? 0, + y: node.computed?.positionAbsolute?.y ?? 0, + width: node.computed?.width ?? node.width ?? 0, + height: node.computed?.height ?? node.height ?? 0, origin: node.origin })} { - if (!isRect && (n.id === node.id || !n.positionAbsolute)) { + if (!isRect && (n.id === node.id || !n.computed?.positionAbsolute)) { return false; } diff --git a/packages/svelte/src/lib/hooks/useUpdateNodeInternals.ts b/packages/svelte/src/lib/hooks/useUpdateNodeInternals.ts index c66bb688..dad8ed88 100644 --- a/packages/svelte/src/lib/hooks/useUpdateNodeInternals.ts +++ b/packages/svelte/src/lib/hooks/useUpdateNodeInternals.ts @@ -1,5 +1,5 @@ import { get } from 'svelte/store'; -import type { UpdateNodeInternals, NodeDimensionUpdate } from '@xyflow/system'; +import type { UpdateNodeInternals } from '@xyflow/system'; import { useStore } from '$lib/store'; diff --git a/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte b/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte index f3fba426..b331a04e 100644 --- a/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte +++ b/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte @@ -114,13 +114,13 @@ {#if ariaLabel}{ariaLabel}{/if} {#each $nodes as node (node.id)} - {#if node.width && node.height} + {#if (node.computed?.width || node?.width) && (node.computed?.height || node.height)} {@const pos = getNodePositionWithOrigin(node).positionAbsolute} { store.nodes.update((nds) => { - return nds.map((n) => { + return nds.map((node) => { const nodeDragItem = (nodeDragItems as Array).find( - (ndi) => ndi.id === n.id + (ndi) => ndi.id === node.id ); if (nodeDragItem) { return { - ...n, - [internalsSymbol]: n[internalsSymbol], + ...node, dragging, - positionAbsolute: nodeDragItem.positionAbsolute, - position: nodeDragItem.position + position: nodeDragItem.position, + computed: { + ...node.computed, + positionAbsolute: nodeDragItem.computed?.positionAbsolute + }, + [internalsSymbol]: node[internalsSymbol] }; } - return n; + return node; }); }); }; diff --git a/packages/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts index cea41c65..1e354a55 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -25,14 +25,14 @@ export type NodeBase zIndex?: number; extent?: 'parent' | CoordinateExtent; expandParent?: boolean; - positionAbsolute?: XYPosition; ariaLabel?: string; focusable?: boolean; origin?: NodeOrigin; handles?: NodeHandle[]; - size?: { + computed?: { width?: number; height?: number; + positionAbsolute?: XYPosition; }; // only used internally @@ -78,11 +78,13 @@ export type NodeBounds = XYPosition & { export type NodeDragItem = { id: string; position: XYPosition; - positionAbsolute: XYPosition; // distance from the mouse cursor to the node when start dragging distance: XYPosition; - width?: number | null; - height?: number | null; + computed: { + width: number | null; + height: number | null; + positionAbsolute: XYPosition; + }; extent?: 'parent' | CoordinateExtent; parentNode?: string; dragging?: boolean; diff --git a/packages/system/src/utils/dom.ts b/packages/system/src/utils/dom.ts index 74bb14aa..3cf5cd61 100644 --- a/packages/system/src/utils/dom.ts +++ b/packages/system/src/utils/dom.ts @@ -73,6 +73,8 @@ export const getHandleBounds = ( } const handlesArray = Array.from(handles) as HTMLDivElement[]; + + // @todo can't we use the node dimensions here? const nodeBounds = nodeElement.getBoundingClientRect(); const nodeOffset = { x: nodeBounds.width * nodeOrigin[0], diff --git a/packages/system/src/utils/edges/positions.ts b/packages/system/src/utils/edges/positions.ts index 8159d46b..35e07834 100644 --- a/packages/system/src/utils/edges/positions.ts +++ b/packages/system/src/utils/edges/positions.ts @@ -88,20 +88,20 @@ function toHandleBounds(handles?: NodeHandle[]) { function getHandleDataByNode(node?: NodeBase): [Rect, NodeHandleBounds | null, boolean] { const handleBounds = node?.[internalsSymbol]?.handleBounds || toHandleBounds(node?.handles) || null; - const nodeWidth = node?.width || node?.size?.width; - const nodeHeight = node?.height || node?.size?.height; + const nodeWidth = node?.computed?.width || node?.width; + const nodeHeight = node?.computed?.height || node?.height; const isValid = handleBounds && nodeWidth && nodeHeight && - typeof node?.positionAbsolute?.x !== 'undefined' && - typeof node?.positionAbsolute?.y !== 'undefined'; + typeof node?.computed?.positionAbsolute?.x !== 'undefined' && + typeof node?.computed?.positionAbsolute?.y !== 'undefined'; return [ { - x: node?.positionAbsolute?.x || 0, - y: node?.positionAbsolute?.y || 0, + x: node?.computed?.positionAbsolute?.x || 0, + y: node?.computed?.positionAbsolute?.y || 0, width: nodeWidth || 0, height: nodeHeight || 0, }, diff --git a/packages/system/src/utils/general.ts b/packages/system/src/utils/general.ts index 976d105f..2db59322 100644 --- a/packages/system/src/utils/general.ts +++ b/packages/system/src/utils/general.ts @@ -64,8 +64,8 @@ export const nodeToRect = (node: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rec return { ...positionAbsolute, - width: node.width || 0, - height: node.height || 0, + width: node.computed?.width ?? node.width ?? 0, + height: node.computed?.height ?? node.height ?? 0, }; }; @@ -74,8 +74,8 @@ export const nodeToBox = (node: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Box return { ...positionAbsolute, - x2: positionAbsolute.x + (node.width || 0), - y2: positionAbsolute.y + (node.height || 0), + x2: positionAbsolute.x + (node.computed?.width ?? node.width ?? 0), + y2: positionAbsolute.y + (node.computed?.height ?? node.height ?? 0), }; }; diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index 43a80ef6..c2e276ae 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -86,8 +86,8 @@ export const getNodePositionWithOrigin = ( }; } - const offsetX = (node.width ?? 0) * nodeOrigin[0]; - const offsetY = (node.height ?? 0) * nodeOrigin[1]; + const offsetX = (node.computed?.width ?? node.width ?? 0) * nodeOrigin[0]; + const offsetY = (node.computed?.height ?? node.height ?? 0) * nodeOrigin[1]; const position: XYPosition = { x: node.position.x - offsetX, @@ -96,10 +96,10 @@ export const getNodePositionWithOrigin = ( return { ...position, - positionAbsolute: node.positionAbsolute + positionAbsolute: node.computed?.positionAbsolute ? { - x: node.positionAbsolute.x - offsetX, - y: node.positionAbsolute.y - offsetY, + x: node.computed.positionAbsolute.x - offsetX, + y: node.computed.positionAbsolute.y - offsetY, } : position, }; @@ -118,8 +118,8 @@ export const getNodesBounds = (nodes: NodeBase[], nodeOrigin: NodeOrigin = [0, 0 rectToBox({ x, y, - width: node.width || 0, - height: node.height || 0, + width: node.computed?.width ?? node.width ?? 0, + height: node.computed?.height ?? node.height ?? 0, }) ); }, @@ -145,17 +145,19 @@ export const getNodesInside = ( }; const visibleNodes = nodes.reduce((res, node) => { - const { width, height, selectable = true, hidden = false } = node; + const { computed, selectable = true, hidden = false } = node; + const width = computed?.width ?? node.width ?? null; + const height = computed?.height ?? node.height ?? null; if ((excludeNonSelectableNodes && !selectable) || hidden) { return res; } const overlappingArea = getOverlappingArea(paneRect, nodeToRect(node, nodeOrigin)); - const notInitialized = width === undefined || height === undefined || width === null || height === null; + const notInitialized = width === null || height === null; const partiallyVisible = partially && overlappingArea > 0; - const area = (width || 0) * (height || 0); + const area = (width ?? 0) * (height ?? 0); const isVisible = notInitialized || partiallyVisible || overlappingArea >= area; if (isVisible || node.dragging) { @@ -185,7 +187,7 @@ export function fitView, Options exte options?: Options ) { const filteredNodes = nodes.filter((n) => { - const isVisible = n.width && n.height && (options?.includeHiddenNodes || !n.hidden); + const isVisible = n.computed?.width && n.computed?.height && (options?.includeHiddenNodes || !n.hidden); if (options?.nodes?.length) { return isVisible && options?.nodes.some((optionNode) => optionNode.id === n.id); @@ -218,7 +220,7 @@ function clampNodeExtent(node: NodeDragItem | NodeBase, extent?: CoordinateExten if (!extent || extent === 'parent') { return extent; } - return [extent[0], [extent[1][0] - (node.width || 0), extent[1][1] - (node.height || 0)]]; + return [extent[0], [extent[1][0] - (node.computed?.width ?? 0), extent[1][1] - (node.computed?.height ?? 0)]]; } export function calcNextPosition( @@ -242,16 +244,18 @@ export function calcNextPosition( } if (node.extent === 'parent' && !node.expandParent) { - if (node.parentNode && node.width && node.height) { + const nodeWidth = node.computed?.width; + const nodeHeight = node.computed?.height; + if (node.parentNode && nodeWidth && nodeHeight) { const currNodeOrigin = node.origin || nodeOrigin; currentExtent = - parentNode && isNumeric(parentNode.width) && isNumeric(parentNode.height) + parentNode && isNumeric(parentNode.computed?.width) && isNumeric(parentNode.computed?.height) ? [ - [parentPos.x + node.width * currNodeOrigin[0], parentPos.y + node.height * currNodeOrigin[1]], + [parentPos.x + nodeWidth * currNodeOrigin[0], parentPos.y + nodeHeight * currNodeOrigin[1]], [ - parentPos.x + parentNode.width - node.width + node.width * currNodeOrigin[0], - parentPos.y + parentNode.height - node.height + node.height * currNodeOrigin[1], + parentPos.x + parentNode.computed?.width - nodeWidth + nodeWidth * currNodeOrigin[0], + parentPos.y + parentNode.computed?.height - nodeHeight + nodeHeight * currNodeOrigin[1], ], ] : currentExtent; diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index 9ed4f871..eca6b8de 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -40,7 +40,7 @@ export function updateAbsolutePositions( parentNode?.origin || nodeOrigin ); - node.positionAbsolute = { + node.computed!.positionAbsolute = { x, y, }; @@ -79,9 +79,11 @@ export function updateNodes( const node: NodeType = { ...options.defaults, ...n, - positionAbsolute: n.position, - width: n.width || currentStoreNode?.width, - height: n.height || currentStoreNode?.height, + computed: { + positionAbsolute: n.position, + width: n.computed?.width || currentStoreNode?.computed?.width, + height: n.computed?.height || currentStoreNode?.computed?.height, + }, }; const z = (isNumeric(n.zIndex) ? n.zIndex : 0) + (n.selected ? selectedNodeZ : 0); const currInternals = n?.[internalsSymbol] || currentStoreNode?.[internalsSymbol]; @@ -160,7 +162,7 @@ export function updateNodeDimensions( const doUpdate = !!( dimensions.width && dimensions.height && - (node.width !== dimensions.width || node.height !== dimensions.height || update.forceUpdate) + (node.computed?.width !== dimensions.width || node.computed?.height !== dimensions.height || update.forceUpdate) ); if (doUpdate) { @@ -168,7 +170,10 @@ export function updateNodeDimensions( const newNode = { ...node, - ...dimensions, + computed: { + ...node.computed, + ...dimensions, + }, [internalsSymbol]: { ...node[internalsSymbol], handleBounds: { diff --git a/packages/system/src/xydrag/XYDrag.ts b/packages/system/src/xydrag/XYDrag.ts index 69d50b41..79a0823a 100644 --- a/packages/system/src/xydrag/XYDrag.ts +++ b/packages/system/src/xydrag/XYDrag.ts @@ -140,11 +140,13 @@ export function XYDrag({ ]; if (dragItems.length > 1 && nodeExtent && !n.extent) { - adjustedNodeExtent[0][0] = n.positionAbsolute.x - nodesBox.x + nodeExtent[0][0]; - adjustedNodeExtent[1][0] = n.positionAbsolute.x + (n.width ?? 0) - nodesBox.x2 + nodeExtent[1][0]; + adjustedNodeExtent[0][0] = n.computed.positionAbsolute.x - nodesBox.x + nodeExtent[0][0]; + adjustedNodeExtent[1][0] = + n.computed.positionAbsolute.x + (n.computed?.width ?? 0) - nodesBox.x2 + nodeExtent[1][0]; - adjustedNodeExtent[0][1] = n.positionAbsolute.y - nodesBox.y + nodeExtent[0][1]; - adjustedNodeExtent[1][1] = n.positionAbsolute.y + (n.height ?? 0) - nodesBox.y2 + nodeExtent[1][1]; + adjustedNodeExtent[0][1] = n.computed.positionAbsolute.y - nodesBox.y + nodeExtent[0][1]; + adjustedNodeExtent[1][1] = + n.computed.positionAbsolute.y + (n.computed?.height ?? 0) - nodesBox.y2 + nodeExtent[1][1]; } const updatedPos = calcNextPosition(n, nextPosition, nodes, adjustedNodeExtent, nodeOrigin, onError); @@ -153,7 +155,7 @@ export function XYDrag({ hasChange = hasChange || n.position.x !== updatedPos.position.x || n.position.y !== updatedPos.position.y; n.position = updatedPos.position; - n.positionAbsolute = updatedPos.positionAbsolute; + n.computed.positionAbsolute = updatedPos.positionAbsolute; return n; }); diff --git a/packages/system/src/xydrag/utils.ts b/packages/system/src/xydrag/utils.ts index 274c2213..15c9af68 100644 --- a/packages/system/src/xydrag/utils.ts +++ b/packages/system/src/xydrag/utils.ts @@ -51,10 +51,9 @@ export function getDragItems( .map((n) => ({ id: n.id, position: n.position || { x: 0, y: 0 }, - positionAbsolute: n.positionAbsolute || { x: 0, y: 0 }, distance: { - x: mousePos.x - (n.positionAbsolute?.x ?? 0), - y: mousePos.y - (n.positionAbsolute?.y ?? 0), + x: mousePos.x - (n.computed?.positionAbsolute?.x ?? 0), + y: mousePos.y - (n.computed?.positionAbsolute?.y ?? 0), }, delta: { x: 0, @@ -62,10 +61,13 @@ export function getDragItems( }, extent: n.extent, parentNode: n.parentNode, - width: n.width, - height: n.height, origin: n.origin, expandParent: n.expandParent, + computed: { + positionAbsolute: n.computed?.positionAbsolute || { x: 0, y: 0 }, + width: n.computed?.width || 0, + height: n.computed?.height || 0, + }, })); } @@ -81,15 +83,18 @@ export function getEventHandlerParams({ dragItems: NodeDragItem[]; nodeLookup: Map; }): [NodeType, NodeType[]] { - const extentedDragItems: NodeType[] = dragItems.map((n) => { + const nodesFromDragItems: NodeType[] = dragItems.map((n) => { const node = nodeLookup.get(n.id)!; return { ...node, position: n.position, - positionAbsolute: n.positionAbsolute, + computed: { + ...n.computed, + positionAbsolute: n.computed.positionAbsolute, + }, }; }); - return [nodeId ? extentedDragItems.find((n) => n.id === nodeId)! : extentedDragItems[0], extentedDragItems]; + return [nodeId ? nodesFromDragItems.find((n) => n.id === nodeId)! : nodesFromDragItems[0], nodesFromDragItems]; } diff --git a/packages/system/src/xyhandle/utils.ts b/packages/system/src/xyhandle/utils.ts index 35b17601..b829f691 100644 --- a/packages/system/src/xyhandle/utils.ts +++ b/packages/system/src/xyhandle/utils.ts @@ -22,8 +22,8 @@ export function getHandles( id: h.id || null, type, nodeId: node.id, - x: (node.positionAbsolute?.x ?? 0) + h.x + h.width / 2, - y: (node.positionAbsolute?.y ?? 0) + h.y + h.height / 2, + x: (node.computed?.positionAbsolute?.x ?? 0) + h.x + h.width / 2, + y: (node.computed?.positionAbsolute?.y ?? 0) + h.y + h.height / 2, }); } return res; From 24c999a335186cd457b38f99e4ef9590447134df Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 21 Nov 2023 15:18:35 +0100 Subject: [PATCH 16/23] chore(astro): use width/height attrs --- .../src/components/ReactFlowExample/index.tsx | 6 ++--- .../components/SvelteFlowExample/index.svelte | 24 +++++++------------ .../components/NodeWrapper/NodeWrapper.svelte | 4 +++- .../svelte/src/lib/store/initial-store.ts | 6 +---- 4 files changed, 15 insertions(+), 25 deletions(-) diff --git a/examples/astro-xyflow/src/components/ReactFlowExample/index.tsx b/examples/astro-xyflow/src/components/ReactFlowExample/index.tsx index ae5a5c87..ca21eeb7 100644 --- a/examples/astro-xyflow/src/components/ReactFlowExample/index.tsx +++ b/examples/astro-xyflow/src/components/ReactFlowExample/index.tsx @@ -27,7 +27,7 @@ const initialNodes: Node[] = [ type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, - size: nodeSize, + ...nodeSize, handles: [ { type: 'source', @@ -41,7 +41,7 @@ const initialNodes: Node[] = [ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, - size: nodeSize, + ...nodeSize, handles: [ { type: 'source', @@ -65,7 +65,7 @@ const initialNodes: Node[] = [ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, - size: nodeSize, + ...nodeSize, handles: [ { type: 'source', diff --git a/examples/astro-xyflow/src/components/SvelteFlowExample/index.svelte b/examples/astro-xyflow/src/components/SvelteFlowExample/index.svelte index cfa8a449..73196d53 100644 --- a/examples/astro-xyflow/src/components/SvelteFlowExample/index.svelte +++ b/examples/astro-xyflow/src/components/SvelteFlowExample/index.svelte @@ -11,10 +11,8 @@ data: { label: 'Node 0' }, sourcePosition: Position.Right, targetPosition: Position.Left, - size: { - width: 100, - height: 40, - }, + width: 100, + height: 40, handles: [ { type: 'source', x: 100, y: 20, position: Position.Right }, { type: 'target', x: 0, y: 20, position: Position.Left }, @@ -26,10 +24,8 @@ data: { label: 'A' }, sourcePosition: Position.Right, targetPosition: Position.Left, - size: { - width: 100, - height: 40, - }, + width: 100, + height: 40, handles: [ { type: 'source', x: 100, y: 20, position: Position.Right }, { type: 'target', x: 0, y: 20, position: Position.Left }, @@ -41,10 +37,8 @@ data: { label: 'B' }, sourcePosition: Position.Right, targetPosition: Position.Left, - size: { - width: 100, - height: 40, - }, + width: 100, + height: 40, handles: [ { type: 'source', x: 100, y: 20, position: Position.Right }, { type: 'target', x: 0, y: 20, position: Position.Left }, @@ -56,10 +50,8 @@ data: { label: 'C' }, sourcePosition: Position.Right, targetPosition: Position.Left, - size: { - width: 100, - height: 40, - }, + width: 100, + height: 40, handles: [ { type: 'source', x: 100, y: 20, position: Position.Right }, { type: 'target', x: 0, y: 20, position: Position.Left }, diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index eb2c1472..a59e522d 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -170,7 +170,9 @@ style:z-index={zIndex} style:transform="translate({positionOriginX}px, {positionOriginY}px)" style:visibility={initialized ? 'visible' : 'hidden'} - style="{style} {width ? `;width=${width}px` : ''} {height ? `;height=${height}px;` : ''}" + style:width={width === undefined ? undefined : `${width}px`} + style:height={height === undefined ? undefined : `${height}px`} + {style} on:click={onSelectNodeHandler} on:mouseenter={(event) => dispatch('nodemouseenter', { node, event })} on:mouseleave={(event) => dispatch('nodemouseleave', { node, event })} diff --git a/packages/svelte/src/lib/store/initial-store.ts b/packages/svelte/src/lib/store/initial-store.ts index d6b621d9..dbbb17fc 100644 --- a/packages/svelte/src/lib/store/initial-store.ts +++ b/packages/svelte/src/lib/store/initial-store.ts @@ -76,11 +76,7 @@ export const getInitialStore = ({ let viewport: Viewport = { x: 0, y: 0, zoom: 1 }; if (fitView && width && height) { - const nodesWithDimensions = nextNodes.map((node) => ({ - ...node, - width: node.size?.width, - height: node.size?.height - })); + const nodesWithDimensions = nextNodes.filter((node) => node.width && node.height); const bounds = getNodesBounds(nodesWithDimensions, [0, 0]); viewport = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1); } From a166129547227284cde5e5031deb48d122498691 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 21 Nov 2023 16:03:07 +0100 Subject: [PATCH 17/23] fix(svelte): use node.style.width and node.style --- .../src/components/ReactFlowExample/index.tsx | 8 -------- .../src/lib/components/NodeWrapper/NodeWrapper.svelte | 6 +++--- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/examples/astro-xyflow/src/components/ReactFlowExample/index.tsx b/examples/astro-xyflow/src/components/ReactFlowExample/index.tsx index ca21eeb7..4f8ef0ab 100644 --- a/examples/astro-xyflow/src/components/ReactFlowExample/index.tsx +++ b/examples/astro-xyflow/src/components/ReactFlowExample/index.tsx @@ -48,16 +48,12 @@ const initialNodes: Node[] = [ position: Position.Bottom, x: nodeSize.width * 0.5, y: nodeSize.height, - width: 1, - height: 1, }, { type: 'target', position: Position.Top, x: nodeSize.width * 0.5, y: 0, - width: 1, - height: 1, }, ], }, @@ -72,16 +68,12 @@ const initialNodes: Node[] = [ position: Position.Bottom, x: nodeSize.width * 0.5, y: nodeSize.height, - width: 1, - height: 1, }, { type: 'target', position: Position.Top, x: nodeSize.width * 0.5, y: 0, - width: 1, - height: 1, }, ], }, diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index a59e522d..335bff71 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -170,9 +170,9 @@ style:z-index={zIndex} style:transform="translate({positionOriginX}px, {positionOriginY}px)" style:visibility={initialized ? 'visible' : 'hidden'} - style:width={width === undefined ? undefined : `${width}px`} - style:height={height === undefined ? undefined : `${height}px`} - {style} + style="{style ?? ''}; {!width ? '' : `width:${width}px;`} {!height + ? '' + : `height:${height}px;`}" on:click={onSelectNodeHandler} on:mouseenter={(event) => dispatch('nodemouseenter', { node, event })} on:mouseleave={(event) => dispatch('nodemouseleave', { node, event })} From 39b0deb9a45e94c971198deaae1a7f85512fa171 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 21 Nov 2023 16:08:13 +0100 Subject: [PATCH 18/23] fix(react): node resizer use new width/height attr --- .../src/additional-components/NodeResizer/ResizeControl.tsx | 4 ++-- packages/system/src/utils/graph.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react/src/additional-components/NodeResizer/ResizeControl.tsx b/packages/react/src/additional-components/NodeResizer/ResizeControl.tsx index 36783fcf..1df32e06 100644 --- a/packages/react/src/additional-components/NodeResizer/ResizeControl.tsx +++ b/packages/react/src/additional-components/NodeResizer/ResizeControl.tsx @@ -70,8 +70,8 @@ function ResizeControl({ const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); prevValues.current = { - width: node?.width ?? 0, - height: node?.height ?? 0, + width: node?.computed?.width ?? 0, + height: node?.computed?.height ?? 0, x: node?.position.x ?? 0, y: node?.position.y ?? 0, }; diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index c2e276ae..fe50e1be 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -254,8 +254,8 @@ export function calcNextPosition( ? [ [parentPos.x + nodeWidth * currNodeOrigin[0], parentPos.y + nodeHeight * currNodeOrigin[1]], [ - parentPos.x + parentNode.computed?.width - nodeWidth + nodeWidth * currNodeOrigin[0], - parentPos.y + parentNode.computed?.height - nodeHeight + nodeHeight * currNodeOrigin[1], + parentPos.x + parentNode.computed.width - nodeWidth + nodeWidth * currNodeOrigin[0], + parentPos.y + parentNode.computed.height - nodeHeight + nodeHeight * currNodeOrigin[1], ], ] : currentExtent; From 51e98ea44d29e7a6607dfbb8cabd09c9338436f2 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 21 Nov 2023 16:37:38 +0100 Subject: [PATCH 19/23] fix(svelte): node toolbar use new width/height attrs --- .../svelte/src/lib/plugins/NodeToolbar/NodeToolbar.svelte | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/lib/plugins/NodeToolbar/NodeToolbar.svelte b/packages/svelte/src/lib/plugins/NodeToolbar/NodeToolbar.svelte index b01bbb48..fab6ce2d 100644 --- a/packages/svelte/src/lib/plugins/NodeToolbar/NodeToolbar.svelte +++ b/packages/svelte/src/lib/plugins/NodeToolbar/NodeToolbar.svelte @@ -51,10 +51,11 @@ let nodeRect: Rect | undefined = undefined; if (toolbarNodes.length === 1) { + const toolbarNode = toolbarNodes[0]; nodeRect = { - ...toolbarNodes[0].position, - width: toolbarNodes[0].width ?? 0, - height: toolbarNodes[0].height ?? 0 + ...toolbarNode.position, + width: toolbarNode.computed?.width ?? toolbarNode.width ?? 0, + height: toolbarNode.computed?.height ?? toolbarNode.height ?? 0 }; } else if (toolbarNodes.length > 1) { nodeRect = getNodesBounds(toolbarNodes, $nodeOrigin); From ab5eef220f65881e94464f6a332e25f614672edb Mon Sep 17 00:00:00 2001 From: Moritz Klack Date: Thu, 23 Nov 2023 12:15:36 +0100 Subject: [PATCH 20/23] Feat: dark mode (#3652) * feat(react/svelte): add dark mode defaults * refactor(darkmode): minimap, edges, edge labels * chore(style): edge label color * feat(colorMode): add colorMode prop light/dark/system * chore(examples): cleanup * test(colorMode): add tests * chore(base.css): add dark base * chore(examples): cleanup --- examples/react/src/App/routes.ts | 6 ++ .../react/src/examples/ColorMode/index.tsx | 88 +++++++++++++++++++ .../react/src/examples/EasyConnect/index.tsx | 1 - .../react/src/examples/NodeResizer/index.tsx | 2 - .../react/src/examples/Overview/index.tsx | 6 +- examples/react/src/main.tsx | 2 +- .../src/components/Header/Header.svelte | 1 + .../routes/examples/color-mode/+page.svelte | 54 ++++++++++++ .../src/routes/examples/edges/+page.svelte | 2 +- .../Background/Patterns.tsx | 2 +- .../additional-components/MiniMap/MiniMap.tsx | 21 +++-- .../MiniMap/MiniMapNode.tsx | 8 +- .../MiniMap/MiniMapNodes.tsx | 12 +-- .../additional-components/MiniMap/types.ts | 6 +- .../react/src/container/ReactFlow/index.tsx | 5 +- packages/react/src/hooks/useColorModeClass.ts | 35 ++++++++ packages/react/src/index.ts | 2 + packages/react/src/styles/base.css | 18 ++++ packages/react/src/styles/style.css | 18 ++++ packages/react/src/types/component-props.ts | 2 + .../container/SvelteFlow/SvelteFlow.svelte | 13 ++- .../src/lib/container/SvelteFlow/types.ts | 4 +- .../src/lib/container/SvelteFlow/utils.ts | 14 ++- .../svelte/src/lib/hooks/useColorModeClass.ts | 32 +++++++ packages/svelte/src/lib/index.ts | 4 +- .../src/lib/plugins/Minimap/Minimap.svelte | 30 +------ .../lib/plugins/Minimap/MinimapNode.svelte | 10 +-- packages/svelte/src/styles/base.css | 9 ++ packages/svelte/src/styles/style.css | 9 ++ packages/system/src/styles/base.css | 5 ++ packages/system/src/styles/init.css | 68 +++++++++++--- packages/system/src/styles/style.css | 31 ++++++- packages/system/src/types/general.ts | 3 + tests/playwright/e2e/edges.spec.ts | 2 +- tests/playwright/e2e/node-toolbar.spec.ts | 2 +- tests/playwright/e2e/nodes.spec.ts | 2 +- tests/playwright/e2e/pane.spec.ts | 6 +- tests/playwright/e2e/props.spec.ts | 22 +++++ 38 files changed, 474 insertions(+), 83 deletions(-) create mode 100644 examples/react/src/examples/ColorMode/index.tsx create mode 100644 examples/svelte/src/routes/examples/color-mode/+page.svelte create mode 100644 packages/react/src/hooks/useColorModeClass.ts create mode 100644 packages/svelte/src/lib/hooks/useColorModeClass.ts create mode 100644 tests/playwright/e2e/props.spec.ts diff --git a/examples/react/src/App/routes.ts b/examples/react/src/App/routes.ts index 6c6b596c..2665abfa 100644 --- a/examples/react/src/App/routes.ts +++ b/examples/react/src/App/routes.ts @@ -1,5 +1,6 @@ import Basic from '../examples/Basic'; import Backgrounds from '../examples/Backgrounds'; +import ColorMode from '../examples/ColorMode'; import ControlledUncontrolled from '../examples/ControlledUncontrolled'; import ControlledViewport from '../examples/ControlledViewport'; import CustomConnectionLine from '../examples/CustomConnectionLine'; @@ -62,6 +63,11 @@ const routes: IRoute[] = [ path: 'backgrounds', component: Backgrounds, }, + { + name: 'Color Mode', + path: 'color-mode', + component: ColorMode, + }, { name: 'Cancel Connection', path: 'cancel-connection', diff --git a/examples/react/src/examples/ColorMode/index.tsx b/examples/react/src/examples/ColorMode/index.tsx new file mode 100644 index 00000000..f867c650 --- /dev/null +++ b/examples/react/src/examples/ColorMode/index.tsx @@ -0,0 +1,88 @@ +import { ChangeEventHandler, useCallback, useState } from 'react'; +import { + ReactFlow, + addEdge, + Node, + useNodesState, + useEdgesState, + OnConnect, + Edge, + MiniMap, + Background, + Controls, + Panel, + ColorMode, + Position, +} from '@xyflow/react'; + +const nodeDefaults = { + sourcePosition: Position.Right, + targetPosition: Position.Left, +}; + +const initialNodes: Node[] = [ + { id: 'A', type: 'input', position: { x: 0, y: 150 }, data: { label: 'A' }, ...nodeDefaults }, + { id: 'B', position: { x: 250, y: 0 }, data: { label: 'B' }, ...nodeDefaults }, + { id: 'C', position: { x: 250, y: 150 }, data: { label: 'C' }, ...nodeDefaults }, + { id: 'D', position: { x: 250, y: 300 }, data: { label: 'D' }, ...nodeDefaults }, +]; + +const initialEdges: Edge[] = [ + { + id: 'A-B', + source: 'A', + target: 'B', + }, + { + id: 'A-C', + source: 'A', + target: 'C', + }, + { + id: 'A-D', + source: 'A', + target: 'D', + }, +]; + +const ColorModeFlow = () => { + const [colorMode, setColorMode] = useState('light'); + const [nodes, , onNodesChange] = useNodesState(initialNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + + const onConnect: OnConnect = useCallback( + (params) => { + console.log('on connect', params); + setEdges((eds) => addEdge(params, eds)); + }, + [setEdges] + ); + + const onChange: ChangeEventHandler = (evt) => setColorMode(evt.target.value as ColorMode); + + return ( + + + + + + + + + + ); +}; + +export default ColorModeFlow; diff --git a/examples/react/src/examples/EasyConnect/index.tsx b/examples/react/src/examples/EasyConnect/index.tsx index 7e600848..11e347f1 100644 --- a/examples/react/src/examples/EasyConnect/index.tsx +++ b/examples/react/src/examples/EasyConnect/index.tsx @@ -5,7 +5,6 @@ import CustomNode from './CustomNode'; import FloatingEdge from './FloatingEdge'; import CustomConnectionLine from './CustomConnectionLine'; -import '@xyflow/react/dist/style.css'; import './style.css'; const initialNodes: Node[] = [ diff --git a/examples/react/src/examples/NodeResizer/index.tsx b/examples/react/src/examples/NodeResizer/index.tsx index e276996c..11ce29d2 100644 --- a/examples/react/src/examples/NodeResizer/index.tsx +++ b/examples/react/src/examples/NodeResizer/index.tsx @@ -16,8 +16,6 @@ import CustomResizer from './CustomResizer'; import VerticalResizer from './VerticalResizer'; import HorizontalResizer from './HorizontalResizer'; -import '@xyflow/react/dist/style.css'; - const nodeTypes = { defaultResizer: DefaultResizer, customResizer: CustomResizer, diff --git a/examples/react/src/examples/Overview/index.tsx b/examples/react/src/examples/Overview/index.tsx index 09110529..e4879ace 100644 --- a/examples/react/src/examples/Overview/index.tsx +++ b/examples/react/src/examples/Overview/index.tsx @@ -23,7 +23,7 @@ const onNodeDragStop = (_: ReactMouseEvent, node: Node, nodes: Node[]) => consol const onNodeDoubleClick = (_: ReactMouseEvent, node: Node) => console.log('node double click', node); const onPaneClick = (event: ReactMouseEvent) => console.log('pane click', event); const onPaneScroll = (event?: ReactMouseEvent) => console.log('pane scroll', event); -const onPaneContextMenu = (event: ReactMouseEvent) => console.log('pane context menu', event); +const onPaneContextMenu = (event: ReactMouseEvent | MouseEvent) => console.log('pane context menu', event); const onSelectionDrag = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag', nodes); const onSelectionDragStart = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag start', nodes); const onSelectionDragStop = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag stop', nodes); @@ -230,9 +230,9 @@ const OverviewFlow = () => { onEdgesDelete={onEdgesDelete} onPaneMouseMove={onPaneMouseMove} > - + - + ); }; diff --git a/examples/react/src/main.tsx b/examples/react/src/main.tsx index 663a29b5..4fdcd68a 100644 --- a/examples/react/src/main.tsx +++ b/examples/react/src/main.tsx @@ -1,7 +1,7 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; -import App from './App'; +import App from './App/index'; import '@xyflow/react/dist/style.css'; import './index.css'; diff --git a/examples/svelte/src/components/Header/Header.svelte b/examples/svelte/src/components/Header/Header.svelte index fde20817..7d229796 100644 --- a/examples/svelte/src/components/Header/Header.svelte +++ b/examples/svelte/src/components/Header/Header.svelte @@ -4,6 +4,7 @@ const routes = [ 'add-node-on-drop', + 'colormode', 'custom-connection-line', 'customnode', 'dagre', diff --git a/examples/svelte/src/routes/examples/color-mode/+page.svelte b/examples/svelte/src/routes/examples/color-mode/+page.svelte new file mode 100644 index 00000000..59ddfdfd --- /dev/null +++ b/examples/svelte/src/routes/examples/color-mode/+page.svelte @@ -0,0 +1,54 @@ + + + + + + + + + + + diff --git a/examples/svelte/src/routes/examples/edges/+page.svelte b/examples/svelte/src/routes/examples/edges/+page.svelte index fc118333..403b0ef6 100644 --- a/examples/svelte/src/routes/examples/edges/+page.svelte +++ b/examples/svelte/src/routes/examples/edges/+page.svelte @@ -145,7 +145,7 @@ ]); - + diff --git a/packages/react/src/additional-components/Background/Patterns.tsx b/packages/react/src/additional-components/Background/Patterns.tsx index 1fc71ae5..61715c5a 100644 --- a/packages/react/src/additional-components/Background/Patterns.tsx +++ b/packages/react/src/additional-components/Background/Patterns.tsx @@ -26,6 +26,6 @@ type DotPatternProps = { export function DotPattern({ radius, className }: DotPatternProps) { return ( - + ); } diff --git a/packages/react/src/additional-components/MiniMap/MiniMap.tsx b/packages/react/src/additional-components/MiniMap/MiniMap.tsx index f56ae57f..33848836 100644 --- a/packages/react/src/additional-components/MiniMap/MiniMap.tsx +++ b/packages/react/src/additional-components/MiniMap/MiniMap.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { memo, useEffect, useRef, type MouseEvent, useCallback } from 'react'; +import { memo, useEffect, useRef, type MouseEvent, useCallback, CSSProperties } from 'react'; import cc from 'classcat'; import { shallow } from 'zustand/shallow'; import { getNodesBounds, getBoundsOfRects, XYMinimap, type Rect, type XYMinimapInstance } from '@xyflow/system'; @@ -40,15 +40,15 @@ const ARIA_LABEL_KEY = 'react-flow__minimap-desc'; function MiniMap({ style, className, - nodeStrokeColor = 'transparent', - nodeColor = '#e2e2e2', + nodeStrokeColor, + nodeColor, nodeClassName = '', nodeBorderRadius = 5, - nodeStrokeWidth = 2, + nodeStrokeWidth, // We need to rename the prop to be `CapitalCase` so that JSX will render it as // a component properly. nodeComponent, - maskColor = 'rgb(240, 240, 240, 0.6)', + maskColor, maskStrokeColor = 'none', maskStrokeWidth = 1, position = 'bottom-right', @@ -126,7 +126,15 @@ function MiniMap({ return ( @@ -153,7 +161,6 @@ function MiniMap({ className="react-flow__minimap-mask" 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`} - fill={maskColor} fillRule="evenodd" stroke={maskStrokeColor} strokeWidth={maskStrokeWidth} diff --git a/packages/react/src/additional-components/MiniMap/MiniMapNode.tsx b/packages/react/src/additional-components/MiniMap/MiniMapNode.tsx index 5fe70462..76edf5e6 100644 --- a/packages/react/src/additional-components/MiniMap/MiniMapNode.tsx +++ b/packages/react/src/additional-components/MiniMap/MiniMapNode.tsx @@ -31,9 +31,11 @@ function MiniMapNode({ ry={borderRadius} width={width} height={height} - fill={fill} - stroke={strokeColor} - strokeWidth={strokeWidth} + style={{ + fill, + stroke: strokeColor, + strokeWidth, + }} shapeRendering={shapeRendering} onClick={onClick ? (event) => onClick(event, id) : undefined} /> diff --git a/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx b/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx index d72700a7..80ac09be 100644 --- a/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx +++ b/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx @@ -19,11 +19,11 @@ const selectorNodes = (s: ReactFlowState) => const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func); function MiniMapNodes({ - nodeStrokeColor = 'transparent', - nodeColor = '#e2e2e2', + nodeStrokeColor, + nodeColor, nodeClassName = '', nodeBorderRadius = 5, - nodeStrokeWidth = 2, + nodeStrokeWidth, // We need to rename the prop to be `CapitalCase` so that JSX will render it as // a component properly. nodeComponent: NodeComponent = MiniMapNode, @@ -41,6 +41,8 @@ function MiniMapNodes({ <> {nodes.map((node) => { const { x, y } = getNodePositionWithOrigin(node, node.origin || nodeOrigin).positionAbsolute; + const color = nodeColor === undefined ? undefined : nodeColorFunc(node); + const strokeColor = nodeStrokeColor === undefined ? undefined : nodeStrokeColorFunc(node); return ( void; diff --git a/packages/react/src/container/ReactFlow/index.tsx b/packages/react/src/container/ReactFlow/index.tsx index d7995a53..1b3dc796 100644 --- a/packages/react/src/container/ReactFlow/index.tsx +++ b/packages/react/src/container/ReactFlow/index.tsx @@ -23,6 +23,7 @@ import A11yDescriptions from '../../components/A11yDescriptions'; import GraphView from '../GraphView'; import Wrapper from './Wrapper'; import type { EdgeTypes, NodeTypes, ReactFlowProps, ReactFlowRefType } from '../../types'; +import useColorModeClass from '../../hooks/useColorModeClass'; const defaultNodeTypes: NodeTypes = { input: InputNode, @@ -169,18 +170,20 @@ const ReactFlow = forwardRef( onViewportChange, width, height, + colorMode = 'light', ...rest }, ref ) => { const rfId = id || '1'; + const colorModeClassName = useColorModeClass(colorMode); return (
diff --git a/packages/react/src/hooks/useColorModeClass.ts b/packages/react/src/hooks/useColorModeClass.ts new file mode 100644 index 00000000..b66f8cea --- /dev/null +++ b/packages/react/src/hooks/useColorModeClass.ts @@ -0,0 +1,35 @@ +import { useEffect, useState } from 'react'; +import type { ColorMode, ColorModeClass } from '@xyflow/system'; + +function getMediaQuery() { + if (typeof window === 'undefined' || !window.matchMedia) { + return null; + } + + return window.matchMedia('(prefers-color-scheme: dark)'); +} + +export default function useColorModeClass(colorMode: ColorMode): ColorModeClass { + const [colorModeClass, setColorModeClass] = useState( + colorMode === 'system' ? null : colorMode + ); + + useEffect(() => { + if (colorMode !== 'system') { + setColorModeClass(colorMode); + return; + } + + const mediaQuery = getMediaQuery(); + const updateColorModeClass = () => setColorModeClass(mediaQuery?.matches ? 'dark' : 'light'); + + updateColorModeClass(); + mediaQuery?.addEventListener('change', updateColorModeClass); + + return () => { + mediaQuery?.removeEventListener('change', updateColorModeClass); + }; + }, [colorMode]); + + return colorModeClass !== null ? colorModeClass : getMediaQuery()?.matches ? 'dark' : 'light'; +} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index be3de1b5..4fe922e5 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -73,6 +73,8 @@ export { type Box, type Transform, type CoordinateExtent, + type ColorMode, + type ColorModeClass, } from '@xyflow/system'; // system utils diff --git a/packages/react/src/styles/base.css b/packages/react/src/styles/base.css index d6be5b10..4a7281dc 100644 --- a/packages/react/src/styles/base.css +++ b/packages/react/src/styles/base.css @@ -1,3 +1,21 @@ /* this will be exported as base.css and can be used for a basic styling */ @import '../../../system/src/styles/init.css'; @import '../../../system/src/styles/base.css'; + +.react-flow { + --edge-label-background-color-default: #ffffff; + --edge-label-color-default: inherit; +} + +.react-flow.dark { + --edge-label-background-color-default: #141414; + --edge-label-color-default: #f8f8f8; +} + +.react-flow__edge-textbg { + fill: var(--edge-label-background-color, var(--edge-label-background-color-default)); +} + +.react-flow__edge-text { + fill: var(--edge-label-color, var(--edge-label-color-default)); +} diff --git a/packages/react/src/styles/style.css b/packages/react/src/styles/style.css index 406af4e9..2ff7f6c1 100644 --- a/packages/react/src/styles/style.css +++ b/packages/react/src/styles/style.css @@ -2,3 +2,21 @@ @import '../../../system/src/styles/init.css'; @import '../../../system/src/styles/style.css'; @import '../../../system/src/styles/node-resizer.css'; + +.react-flow { + --edge-label-background-color-default: #ffffff; + --edge-label-color-default: inherit; +} + +.react-flow.dark { + --edge-label-background-color-default: #141414; + --edge-label-color-default: #f8f8f8; +} + +.react-flow__edge-textbg { + fill: var(--edge-label-background-color, var(--edge-label-background-color-default)); +} + +.react-flow__edge-text { + fill: var(--edge-label-color, var(--edge-label-color-default)); +} diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index 583b3d86..770432f5 100644 --- a/packages/react/src/types/component-props.ts +++ b/packages/react/src/types/component-props.ts @@ -19,6 +19,7 @@ import type { SelectionMode, OnError, IsValidConnection, + ColorMode, } from '@xyflow/system'; import type { @@ -155,6 +156,7 @@ export type ReactFlowProps = Omit, 'onError'> & { nodeDragThreshold?: number; width?: number; height?: number; + colorMode?: ColorMode; }; export type ReactFlowRefType = HTMLDivElement; diff --git a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte index 10c1751e..dd72ec3b 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte @@ -15,8 +15,14 @@ import { Attribution } from '$lib/components/Attribution'; import { key, useStore, createStoreContext } from '$lib/store'; import type { SvelteFlowProps } from './types'; - import { updateStore, updateStoreByKeys, type UpdatableStoreProps } from './utils'; + import { + updateStore, + updateStoreByKeys, + type UpdatableStoreProps, + getColorModeClass + } from './utils'; import { get } from 'svelte/store'; + import { useColorModeClass } from '$lib/hooks/useColorModeClass'; type $$Props = SvelteFlowProps; @@ -70,6 +76,7 @@ export let defaultEdgeOptions: $$Props['defaultEdgeOptions'] = undefined; export let width: $$Props['width'] = undefined; export let height: $$Props['height'] = undefined; + export let colorMode: $$Props['colorMode'] = 'light'; export let defaultMarkerColor = '#b1b1b7'; @@ -156,6 +163,8 @@ maxZoom, translateExtent }); + + $: colorModeClass = useColorModeClass(colorMode);
& { defaultEdgeOptions?: DefaultEdgeOptions; width?: number; height?: number; + colorMode?: ColorMode; class?: string; style?: string; diff --git a/packages/svelte/src/lib/container/SvelteFlow/utils.ts b/packages/svelte/src/lib/container/SvelteFlow/utils.ts index fe5a15aa..9abdb3e9 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/utils.ts +++ b/packages/svelte/src/lib/container/SvelteFlow/utils.ts @@ -1,6 +1,6 @@ import type { SvelteFlowStore } from '$lib/store/types'; import type { EdgeTypes, NodeTypes } from '$lib/types'; -import type { CoordinateExtent } from '@xyflow/system'; +import type { ColorMode, CoordinateExtent } from '@xyflow/system'; import type { Writable } from 'svelte/store'; // this is helper function for updating the store @@ -77,3 +77,15 @@ export function updateStoreByKeys(store: SvelteFlowStore, keys: UpdatableStorePr } }); } + +export function getColorModeClass(colorMode?: ColorMode) { + if (colorMode !== 'system') { + return colorMode; + } + + if (!colorMode || typeof window === 'undefined' || !window.matchMedia) { + return 'light'; + } + + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; +} diff --git a/packages/svelte/src/lib/hooks/useColorModeClass.ts b/packages/svelte/src/lib/hooks/useColorModeClass.ts new file mode 100644 index 00000000..ad163a3f --- /dev/null +++ b/packages/svelte/src/lib/hooks/useColorModeClass.ts @@ -0,0 +1,32 @@ +import type { ColorMode, ColorModeClass } from '@xyflow/system'; +import { readable, type Readable } from 'svelte/store'; + +function getMediaQuery() { + if (typeof window === 'undefined' || !window.matchMedia) { + return null; + } + + return window.matchMedia('(prefers-color-scheme: dark)'); +} + +export function useColorModeClass(colorMode: ColorMode = 'light'): Readable { + const colorModeClass = readable('light', (set) => { + if (colorMode !== 'system') { + set(colorMode); + + return; + } + + const mediaQuery = getMediaQuery(); + const updateColorModeClass = () => set(mediaQuery?.matches ? 'dark' : 'light'); + + set(mediaQuery?.matches ? 'dark' : 'light'); + mediaQuery?.addEventListener('change', updateColorModeClass); + + return () => { + mediaQuery?.removeEventListener('change', updateColorModeClass); + }; + }); + + return colorModeClass; +} diff --git a/packages/svelte/src/lib/index.ts b/packages/svelte/src/lib/index.ts index 88bc483f..d9e0885f 100644 --- a/packages/svelte/src/lib/index.ts +++ b/packages/svelte/src/lib/index.ts @@ -75,7 +75,9 @@ export { type Rect, type Box, type Transform, - type CoordinateExtent + type CoordinateExtent, + type ColorMode, + type ColorModeClass } from '@xyflow/system'; // system utils diff --git a/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte b/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte index b331a04e..67f93698 100644 --- a/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte +++ b/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte @@ -20,7 +20,7 @@ export let position: $$Props['position'] = 'bottom-right'; export let ariaLabel: $$Props['ariaLabel'] = 'Mini map'; export let nodeStrokeColor: $$Props['nodeStrokeColor'] = 'transparent'; - export let nodeColor: $$Props['nodeColor'] = '#e2e2e2'; + export let nodeColor: $$Props['nodeColor'] = undefined; export let nodeClass: $$Props['nodeClass'] = ''; export let nodeBorderRadius: $$Props['nodeBorderRadius'] = 5; export let nodeStrokeWidth: $$Props['nodeStrokeWidth'] = 2; @@ -51,7 +51,7 @@ translateExtent } = useStore(); - const nodeColorFunc = getAttrFunction(nodeColor); + const nodeColorFunc = nodeColor === undefined ? undefined : getAttrFunction(nodeColor); const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor); const nodeClassFunc = getAttrFunction(nodeClass); const shapeRendering = @@ -122,7 +122,7 @@ width={node.computed?.width ?? node.width ?? 0} height={node.computed?.height ?? node.height ?? 0} selected={node.selected} - color={nodeColorFunc(node)} + color={nodeColorFunc?.(node)} borderRadius={nodeBorderRadius} strokeColor={nodeStrokeColorFunc(node)} strokeWidth={nodeStrokeWidth} @@ -142,27 +142,3 @@ {/if} - - diff --git a/packages/svelte/src/lib/plugins/Minimap/MinimapNode.svelte b/packages/svelte/src/lib/plugins/Minimap/MinimapNode.svelte index ff0b1f0a..b17165a0 100644 --- a/packages/svelte/src/lib/plugins/Minimap/MinimapNode.svelte +++ b/packages/svelte/src/lib/plugins/Minimap/MinimapNode.svelte @@ -6,9 +6,9 @@ export let width: number = 0; export let height: number = 0; export let borderRadius: number = 5; - export let color: string; + export let color: string | undefined = undefined; export let shapeRendering: string; - export let strokeColor: string; + export let strokeColor: string | undefined = undefined; export let strokeWidth: number = 2; export let selected: boolean = false; let className: string = ''; @@ -24,8 +24,8 @@ ry={borderRadius} {width} {height} - fill={color} - stroke={strokeColor} - stroke-width={strokeWidth} + style={`${color ? `fill: ${color};` : ''}${strokeColor ? `stroke: ${strokeColor};` : ''}${ + strokeWidth ? `stroke-width: ${strokeWidth};` : '' + }`} shape-rendering={shapeRendering} /> diff --git a/packages/svelte/src/styles/base.css b/packages/svelte/src/styles/base.css index 80bc953d..b9861c98 100644 --- a/packages/svelte/src/styles/base.css +++ b/packages/svelte/src/styles/base.css @@ -2,7 +2,16 @@ @import '../../../system/src/styles/init.css'; @import '../../../system/src/styles/base.css'; +.svelte-flow { + --edge-label-color-default: inherit; +} + +.svelte-flow.dark { + --edge-label-color-default: #f8f8f8; +} + .svelte-flow__edge-label { text-align: center; position: absolute; + color: var(--edge-label-color, var(--edge-label-color-default)); } diff --git a/packages/svelte/src/styles/style.css b/packages/svelte/src/styles/style.css index c65dc982..cd89ee84 100644 --- a/packages/svelte/src/styles/style.css +++ b/packages/svelte/src/styles/style.css @@ -2,10 +2,19 @@ @import '../../../system/src/styles/init.css'; @import '../../../system/src/styles/style.css'; +.svelte-flow { + --edge-label-color-default: inherit; +} + +.svelte-flow.dark { + --edge-label-color-default: #f8f8f8; +} + .svelte-flow__edge-label { text-align: center; position: absolute; font-size: 10px; + color: var(--edge-label-color, var(--edge-label-color-default)); } .svelte-flow__nodes { diff --git a/packages/system/src/styles/base.css b/packages/system/src/styles/base.css index 31afe71a..1cb026b6 100644 --- a/packages/system/src/styles/base.css +++ b/packages/system/src/styles/base.css @@ -8,6 +8,10 @@ --selection-border-default: 1px dotted rgba(155, 155, 155, 0.8); } +.xy-flow.dark { + --node-color-default: #f8f8f8; +} + .xy-flow__handle { background-color: var(--handle-background-color, var(--handle-background-color-default)); } @@ -17,6 +21,7 @@ .xy-flow__node-output, .xy-flow__node-group { border: var(--node-border, var(--node-border-default)); + color: var(--node-color, var(--node-color-default)); &.selected, &:focus, diff --git a/packages/system/src/styles/init.css b/packages/system/src/styles/init.css index 9fed0d7b..474b1322 100644 --- a/packages/system/src/styles/init.css +++ b/packages/system/src/styles/init.css @@ -11,12 +11,43 @@ --attribution-background-color-default: rgba(255, 255, 255, 0.5); --minimap-background-color-default: #fff; + --minimap-mask-background-color-default: rgb(240, 240, 240, 0.6); + --minimap-node-background-color-default: #e2e2e2; + --minimap-node-stroke-color-default: transparent; + --minimap-node-stroke-width-default: 2; - --background-pattern-dot-color-default: #91919a; - --background-pattern-line-color-default: #eee; + --background-color-default: transparent; + --background-pattern-dots-color-default: #91919a; + --background-pattern-lines-color-default: #eee; --background-pattern-cross-color-default: #e2e2e2; } +.xy-flow.dark { + --edge-stroke-default: #3c3c3c; + --edge-stroke-width-default: 1; + --edge-stroke-selected-default: #727272; + + --connectionline-stroke-default: #b1b1b7; + --connectionline-stroke-width-default: 1; + + --attribution-background-color-default: rgba(150, 150, 150, 0.25); + + --minimap-background-color-default: #141414; + --minimap-mask-background-color-default: rgb(60, 60, 60, 0.6); + --minimap-node-background-color-default: #2b2b2b; + --minimap-node-stroke-color-default: transparent; + --minimap-node-stroke-width-default: 2; + + --background-color-default: #141414; + --background-pattern-dots-color-default: #777; + --background-pattern-lines-color-default: #777; + --background-pattern-cross-color-default: #777; +} + +.xy-flow { + background-color: var(--background-color-props, var(--background-color-default, 'transparent')); +} + .xy-flow__container { position: absolute; width: 100%; @@ -108,16 +139,11 @@ pointer-events: all; } - &-textbg { - fill: white; - } - .xy-flow__edge-text { pointer-events: none; user-select: none; } } - .xy-flow__connection { pointer-events: none; @@ -268,26 +294,47 @@ .xy-flow__minimap { background: var(--minimap-background-color, var(--minimap-background-color-default)); + + &-mask { + fill: var( + --minimap-mask-background-color-props, + var(--minimap-mask-background-color, var(--minimap-mask-background-color-default)) + ); + } + + &-node { + fill: var( + --minimap-node-background-color-props, + var(--minimap-node-background-color, var(--minimap-node-background-color-default)) + ); + stroke: var( + --minimap-node-stroke-color-props, + var(--minimap-node-stroke-color, var(--minimap-node-stroke-color-default)) + ); + stroke-width: var( + --minimap-node-stroke-width-props, + var(--minimap-node-stroke-width, var(--minimap-node-stroke-width-default)) + ); + } } .xy-flow__background { pointer-events: none; z-index: -1; - background-color: var(--background-color-props, 'transparent'); } .xy-flow__background-pattern { &.dots { fill: var( --background-pattern-color-props, - var(--background-pattern-color, var(--background-pattern-dot-color-default)) + var(--background-pattern-color, var(--background-pattern-dots-color-default)) ); } &.lines { stroke: var( --background-pattern-color-props, - var(--background-pattern-color, var(--background-pattern-line-color-default)) + var(--background-pattern-color, var(--background-pattern-lines-color-default)) ); } @@ -312,6 +359,7 @@ width: 100%; max-width: 12px; max-height: 12px; + fill: currentColor; } } } diff --git a/packages/system/src/styles/style.css b/packages/system/src/styles/style.css index 99fa8876..e88cc3f1 100644 --- a/packages/system/src/styles/style.css +++ b/packages/system/src/styles/style.css @@ -5,6 +5,7 @@ --node-group-background-color-default: rgba(240, 240, 240, 0.25); --node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, 0.08); --node-boxshadow-selected-default: 0 0 0 0.5px #1a192b; + --node-border-radius-default: 3px; --handle-background-color-default: #1a192b; --handle-border-color-default: #fff; @@ -20,6 +21,28 @@ --controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08); } +.xy-flow.dark { + --node-color-default: #f8f8f8; + --node-border-default: 1px solid #3c3c3c; + --node-background-color-default: #1e1e1e; + --node-group-background-color-default: rgba(240, 240, 240, 0.25); + --node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, 0.08); + --node-boxshadow-selected-default: 0 0 0 0.5px #999; + + --handle-background-color-default: #bebebe; + --handle-border-color-default: #1e1e1e; + + --selection-background-color-default: rgba(200, 200, 220, 0.08); + --selection-border-default: 1px dotted rgba(200, 200, 220, 0.8); + + --controls-button-background-color-default: #2b2b2b; + --controls-button-background-color-hover-default: #3e3e3e; + --controls-button-color-default: #f8f8f8; + --controls-button-color-hover-default: #fff; + --controls-button-border-color-default: #5b5b5b; + --controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08); +} + .xy-flow__edge { &.updating { .xy-flow__edge-path { @@ -44,7 +67,7 @@ .xy-flow__node-output, .xy-flow__node-group { padding: 10px; - border-radius: 3px; + border-radius: var(--node-border-radius, var(--node-border-radius-default)); width: 150px; font-size: 12px; color: var(--node-color, var(--node-color-default)); @@ -110,7 +133,7 @@ ); color: var( --controls-button-color-hover-props, - var(--controls-button-hover-color, var(--controls-button-hover-color-default)) + var(--controls-button-color-hover, var(--controls-button-color-hover-default)) ); } @@ -122,4 +145,8 @@ } } } + + &-button:last-child { + border-bottom: none; + } } diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index bc86442d..22ad889e 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -136,3 +136,6 @@ export type UpdateConnection = (params: { connectionStartHandle: ConnectingHandle | null; connectionEndHandle: ConnectingHandle | null; }) => void; + +export type ColorModeClass = 'light' | 'dark'; +export type ColorMode = ColorModeClass | 'system'; diff --git a/tests/playwright/e2e/edges.spec.ts b/tests/playwright/e2e/edges.spec.ts index af2cf980..16695e63 100644 --- a/tests/playwright/e2e/edges.spec.ts +++ b/tests/playwright/e2e/edges.spec.ts @@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test'; import { FRAMEWORK } from './constants'; -test.describe('EDGES', () => { +test.describe('Edges', () => { test.beforeEach(async ({ page }) => { // Go to the starting url before each test. await page.goto('/tests/generic/edges/general'); diff --git a/tests/playwright/e2e/node-toolbar.spec.ts b/tests/playwright/e2e/node-toolbar.spec.ts index daf76070..38722e16 100644 --- a/tests/playwright/e2e/node-toolbar.spec.ts +++ b/tests/playwright/e2e/node-toolbar.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, Locator } from '@playwright/test'; +import { test, expect } from '@playwright/test'; import { FRAMEWORK } from './constants'; type Position = 'top' | 'right' | 'bottom' | 'left'; diff --git a/tests/playwright/e2e/nodes.spec.ts b/tests/playwright/e2e/nodes.spec.ts index 5afa41de..decd2c4f 100644 --- a/tests/playwright/e2e/nodes.spec.ts +++ b/tests/playwright/e2e/nodes.spec.ts @@ -2,7 +2,7 @@ import { test, expect, Locator } from '@playwright/test'; import { FRAMEWORK } from './constants'; -test.describe('NODES', () => { +test.describe('Nodes', () => { test.beforeEach(async ({ page }) => { // Go to the starting url before each test. await page.goto('/tests/generic/nodes/general'); diff --git a/tests/playwright/e2e/pane.spec.ts b/tests/playwright/e2e/pane.spec.ts index 15a79655..d4d66988 100644 --- a/tests/playwright/e2e/pane.spec.ts +++ b/tests/playwright/e2e/pane.spec.ts @@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test'; import { FRAMEWORK } from './constants'; import { getTransform } from './utils'; -test.describe('PANE DEFAULT', () => { +test.describe('Pane default', () => { test.beforeEach(async ({ page }) => { // Go to the starting url before each test. await page.goto('/tests/generic/pane/general'); @@ -126,7 +126,7 @@ test.describe('PANE DEFAULT', () => { }); }); -test.describe('PANE NON-DEFAULT', () => { +test.describe('Pane non-default', () => { test.beforeEach(async ({ page }) => { // Go to the starting url before each test. await page.goto('/tests/generic/pane/non-defaults'); @@ -168,7 +168,7 @@ test.describe('PANE NON-DEFAULT', () => { }); }); -test.describe('PANE ACTIVATION KEYS', () => { +test.describe('Pane activation keys', () => { test.beforeEach(async ({ page }) => { // Go to the starting url before each test. await page.goto('/tests/generic/pane/activation-keys'); diff --git a/tests/playwright/e2e/props.spec.ts b/tests/playwright/e2e/props.spec.ts new file mode 100644 index 00000000..b8104633 --- /dev/null +++ b/tests/playwright/e2e/props.spec.ts @@ -0,0 +1,22 @@ +import { test, expect } from '@playwright/test'; + +import { FRAMEWORK } from './constants'; + +test.describe('Props', () => { + test.describe('colorMode', async () => { + test('render default light color mode', async ({ page }) => { + await page.goto('/examples/color-mode'); + const locator = page.locator(`.${FRAMEWORK}-flow`); + + await expect(locator).not.toHaveClass(/dark/); + }); + + test('render dark color mode', async ({ page }) => { + await page.goto('/examples/color-mode'); + const locator = page.locator(`.${FRAMEWORK}-flow`); + await page.getByTestId('colormode-select').selectOption({ label: 'dark' }); + + await expect(locator).toHaveClass(/dark/); + }); + }); +}); From 38b5051179413d1a2439621aefb10143e9f61fba Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 23 Nov 2023 12:49:06 +0100 Subject: [PATCH 21/23] feat(svelte): add on:selectionclick and on:selectioncontextmenu closes #3646 --- .../src/routes/examples/overview/+page.svelte | 2 ++ .../NodeSelection/NodeSelection.svelte | 17 +++++++++++++++++ .../lib/container/SvelteFlow/SvelteFlow.svelte | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/examples/svelte/src/routes/examples/overview/+page.svelte b/examples/svelte/src/routes/examples/overview/+page.svelte index dddbf398..68fd4aaa 100644 --- a/examples/svelte/src/routes/examples/overview/+page.svelte +++ b/examples/svelte/src/routes/examples/overview/+page.svelte @@ -174,6 +174,8 @@ event.preventDefault(); console.log('on edge contextmenu', edge); }} + on:selectionclick={(event) => console.log('on selection click', event)} + on:selectioncontextmenu={(event) => console.log('on selection contextmenu', event)} autoPanOnConnect autoPanOnNodeDrag connectionMode={ConnectionMode.Strict} diff --git a/packages/svelte/src/lib/components/NodeSelection/NodeSelection.svelte b/packages/svelte/src/lib/components/NodeSelection/NodeSelection.svelte index 98c48d8c..b314e2ad 100644 --- a/packages/svelte/src/lib/components/NodeSelection/NodeSelection.svelte +++ b/packages/svelte/src/lib/components/NodeSelection/NodeSelection.svelte @@ -1,15 +1,30 @@ {#if selectedNodes && $selectionRectMode === 'nodes'} @@ -17,6 +32,8 @@ class="selection-wrapper nopan" style="width: {bounds.width}px; height: {bounds.height}px; transform: translate({bounds.x}px, {bounds.y}px)" use:drag={{ disabled: false, store }} + on:contextmenu={onContextMenu} + on:click={onClick} >
diff --git a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte index dd72ec3b..59bff1f4 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte @@ -223,7 +223,7 @@ on:nodedragstop on:nodecontextmenu /> - + From 1dfff1bf5bec36adce648219cec20f8b9c7b37a7 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 23 Nov 2023 13:50:28 +0100 Subject: [PATCH 22/23] refactor(libs) add width, height and positionAbsolute props to NodeProps --- .../src/components/ReactFlowExample/CustomNode.tsx | 4 ++-- examples/react/src/examples/Subflow/DebugNode.tsx | 4 ++-- .../src/routes/examples/overview/CustomNode.svelte | 7 +++---- .../src/routes/examples/subflows/DebugNode.svelte | 14 +++----------- packages/react/src/components/Nodes/wrapNode.tsx | 6 ++++-- .../react/src/container/NodeRenderer/index.tsx | 1 + packages/react/src/types/nodes.ts | 3 ++- .../lib/components/NodeWrapper/NodeWrapper.svelte | 5 +++-- .../svelte/src/lib/components/NodeWrapper/types.ts | 4 ++-- .../src/lib/components/nodes/DefaultNode.svelte | 13 +++++++++---- .../src/lib/components/nodes/GroupNode.svelte | 13 +++++++++---- .../src/lib/components/nodes/InputNode.svelte | 13 +++++++++---- .../src/lib/components/nodes/OutputNode.svelte | 10 ++++++---- .../lib/container/NodeRenderer/NodeRenderer.svelte | 6 +++--- packages/system/src/types/nodes.ts | 5 +++-- packages/system/src/utils/graph.ts | 4 ++-- 16 files changed, 63 insertions(+), 49 deletions(-) diff --git a/examples/astro-xyflow/src/components/ReactFlowExample/CustomNode.tsx b/examples/astro-xyflow/src/components/ReactFlowExample/CustomNode.tsx index dc371239..86aef137 100644 --- a/examples/astro-xyflow/src/components/ReactFlowExample/CustomNode.tsx +++ b/examples/astro-xyflow/src/components/ReactFlowExample/CustomNode.tsx @@ -7,7 +7,7 @@ const sourceHandleStyleB: CSSProperties = { left: 'auto', }; -const CustomNode: FC = ({ data, xPos, yPos }) => { +const CustomNode: FC = ({ data, positionAbsolute }) => { return ( <> @@ -18,7 +18,7 @@ const CustomNode: FC = ({ data, xPos, yPos }) => {
Position:{' '} - {xPos.toFixed(2)},{yPos.toFixed(2)} + {positionAbsolute.x.toFixed(2)},{positionAbsolute.y.toFixed(2)}
diff --git a/examples/react/src/examples/Subflow/DebugNode.tsx b/examples/react/src/examples/Subflow/DebugNode.tsx index 9170ef65..46bae224 100644 --- a/examples/react/src/examples/Subflow/DebugNode.tsx +++ b/examples/react/src/examples/Subflow/DebugNode.tsx @@ -11,13 +11,13 @@ const idStyle: CSSProperties = { left: 2, }; -const DebugNode: FC = ({ zIndex, xPos, yPos, id }) => { +const DebugNode: FC = ({ zIndex, positionAbsolute, id }) => { return ( <>
{id}
- x:{Math.round(xPos || 0)} y:{Math.round(yPos || 0)} z:{zIndex} + x:{Math.round(positionAbsolute.x)} y:{Math.round(positionAbsolute.y)} z:{zIndex}
diff --git a/examples/svelte/src/routes/examples/overview/CustomNode.svelte b/examples/svelte/src/routes/examples/overview/CustomNode.svelte index a8f6a226..1197ce62 100644 --- a/examples/svelte/src/routes/examples/overview/CustomNode.svelte +++ b/examples/svelte/src/routes/examples/overview/CustomNode.svelte @@ -1,16 +1,15 @@
{data.label}
-
{~~xPos}, {~~yPos}
+
{~~positionAbsolute.x}, {~~positionAbsolute.y}
diff --git a/examples/svelte/src/routes/examples/subflows/DebugNode.svelte b/examples/svelte/src/routes/examples/subflows/DebugNode.svelte index 7b4434be..9676957e 100644 --- a/examples/svelte/src/routes/examples/subflows/DebugNode.svelte +++ b/examples/svelte/src/routes/examples/subflows/DebugNode.svelte @@ -1,24 +1,16 @@
{id}
- x:{Math.round(xPos || 0)} y:{Math.round(yPos || 0)} z:{zIndex} + x:{Math.round(positionAbsolute.x)} y:{Math.round(positionAbsolute.y)} z:{zIndex}
- - diff --git a/packages/react/src/components/Nodes/wrapNode.tsx b/packages/react/src/components/Nodes/wrapNode.tsx index ab56350b..cc1537d1 100644 --- a/packages/react/src/components/Nodes/wrapNode.tsx +++ b/packages/react/src/components/Nodes/wrapNode.tsx @@ -52,6 +52,7 @@ export default (NodeComponent: ComponentType) => { disableKeyboardA11y, ariaLabel, rfId, + positionAbsolute, width, height, }: WrapNodeProps) => { @@ -209,8 +210,9 @@ export default (NodeComponent: ComponentType) => { id={id} data={data} type={type} - xPos={xPos} - yPos={yPos} + width={width} + height={height} + positionAbsolute={positionAbsolute} selected={selected} isConnectable={isConnectable} sourcePosition={sourcePosition} diff --git a/packages/react/src/container/NodeRenderer/index.tsx b/packages/react/src/container/NodeRenderer/index.tsx index 3797bc45..73808da1 100644 --- a/packages/react/src/container/NodeRenderer/index.tsx +++ b/packages/react/src/container/NodeRenderer/index.tsx @@ -122,6 +122,7 @@ const NodeRenderer = (props: NodeRendererProps) => { yPos={posY} xPosOrigin={posOrigin.x} yPosOrigin={posOrigin.y} + positionAbsolute={clampedPosition || { x: 0, y: 0 }} onClick={props.onNodeClick} onMouseEnter={props.onNodeMouseEnter} onMouseMove={props.onNodeMouseMove} diff --git a/packages/react/src/types/nodes.ts b/packages/react/src/types/nodes.ts index ab691af5..29548666 100644 --- a/packages/react/src/types/nodes.ts +++ b/packages/react/src/types/nodes.ts @@ -1,5 +1,5 @@ import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react'; -import type { NodeBase } from '@xyflow/system'; +import type { NodeBase, XYPosition } from '@xyflow/system'; export type Node = NodeBase< NodeData, @@ -24,6 +24,7 @@ export type WrapNodeProps = Pick< yPos: number; xPosOrigin: number; yPosOrigin: number; + positionAbsolute: XYPosition; initialized: boolean; isSelectable: boolean; isDraggable: boolean; diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index 335bff71..9a73ffa8 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -191,8 +191,9 @@ {dragging} {dragHandle} isConnectable={connectable} - xPos={positionX} - yPos={positionY} + positionAbsolute={{ x: positionX, y: positionY }} + {width} + {height} on:connectstart on:connect on:connectend diff --git a/packages/svelte/src/lib/components/NodeWrapper/types.ts b/packages/svelte/src/lib/components/NodeWrapper/types.ts index 3da65c2f..39fb0c10 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/types.ts +++ b/packages/svelte/src/lib/components/NodeWrapper/types.ts @@ -11,14 +11,14 @@ export type NodeWrapperProps = Pick< | 'selected' | 'selectable' | 'style' - | 'width' - | 'height' | 'type' | 'sourcePosition' | 'targetPosition' | 'dragHandle' | 'hidden' > & { + width?: number; + height?: number; type: string; positionX: number; positionY: number; diff --git a/packages/svelte/src/lib/components/nodes/DefaultNode.svelte b/packages/svelte/src/lib/components/nodes/DefaultNode.svelte index c802de0c..f4fc7270 100644 --- a/packages/svelte/src/lib/components/nodes/DefaultNode.svelte +++ b/packages/svelte/src/lib/components/nodes/DefaultNode.svelte @@ -11,24 +11,29 @@ // unused props - we need to list them here in order to prevent warnings export let id: $$Props['id'] = ''; + export let width: $$Props['width'] = undefined; + export let height: $$Props['height'] = undefined; export let selected: $$Props['selected'] = undefined; export let type: $$Props['type'] = undefined; export let zIndex: $$Props['zIndex'] = undefined; export let dragging: $$Props['dragging'] = false; export let dragHandle: $$Props['dragHandle'] = undefined; - export let xPos: $$Props['xPos'] = 0; - export let yPos: $$Props['yPos'] = 0; + export let positionAbsolute: $$Props['positionAbsolute'] = { + x: 0, + y: 0 + }; export let isConnectable: $$Props['isConnectable'] = undefined; // @todo: there must be a better way to do this id; + width; + height; selected; type; zIndex; dragging; dragHandle; - xPos; - yPos; + positionAbsolute; isConnectable; diff --git a/packages/svelte/src/lib/components/nodes/GroupNode.svelte b/packages/svelte/src/lib/components/nodes/GroupNode.svelte index cd11b299..22dd0754 100644 --- a/packages/svelte/src/lib/components/nodes/GroupNode.svelte +++ b/packages/svelte/src/lib/components/nodes/GroupNode.svelte @@ -5,6 +5,8 @@ // unused props - we need to list them here in order to prevent warnings export let id: $$Props['id'] = ''; + export let width: $$Props['width'] = undefined; + export let height: $$Props['height'] = undefined; export let data: $$Props['data'] = {}; export let selected: $$Props['selected'] = undefined; export let sourcePosition: $$Props['sourcePosition'] = undefined; @@ -13,12 +15,16 @@ export let zIndex: $$Props['zIndex'] = undefined; export let dragging: $$Props['dragging'] = false; export let dragHandle: $$Props['dragHandle'] = undefined; - export let xPos: $$Props['xPos'] = 0; - export let yPos: $$Props['yPos'] = 0; + export let positionAbsolute: $$Props['positionAbsolute'] = { + x: 0, + y: 0 + }; export let isConnectable: $$Props['isConnectable'] = undefined; // @todo: there must be a better way to do this id; + width; + height; data; selected; sourcePosition; @@ -27,7 +33,6 @@ zIndex; dragging; dragHandle; - xPos; - yPos; + positionAbsolute; isConnectable; diff --git a/packages/svelte/src/lib/components/nodes/InputNode.svelte b/packages/svelte/src/lib/components/nodes/InputNode.svelte index 71e40096..aa911d1c 100644 --- a/packages/svelte/src/lib/components/nodes/InputNode.svelte +++ b/packages/svelte/src/lib/components/nodes/InputNode.svelte @@ -10,26 +10,31 @@ // unused props - we need to list them here in order to prevent warnings export let id: $$Props['id'] = ''; + export let width: $$Props['width'] = undefined; + export let height: $$Props['height'] = undefined; export let selected: $$Props['selected'] = undefined; export let targetPosition: $$Props['targetPosition'] = undefined; export let type: $$Props['type'] = undefined; export let zIndex: $$Props['zIndex'] = undefined; export let dragging: $$Props['dragging'] = false; export let dragHandle: $$Props['dragHandle'] = undefined; - export let xPos: $$Props['xPos'] = 0; - export let yPos: $$Props['yPos'] = 0; + export let positionAbsolute: $$Props['positionAbsolute'] = { + x: 0, + y: 0 + }; export let isConnectable: $$Props['isConnectable'] = undefined; // @todo: there must be a better way to do this id; + width; + height; selected; targetPosition; type; zIndex; dragging; dragHandle; - xPos; - yPos; + positionAbsolute; isConnectable; diff --git a/packages/svelte/src/lib/components/nodes/OutputNode.svelte b/packages/svelte/src/lib/components/nodes/OutputNode.svelte index a9bde54b..b5ebc2b1 100644 --- a/packages/svelte/src/lib/components/nodes/OutputNode.svelte +++ b/packages/svelte/src/lib/components/nodes/OutputNode.svelte @@ -10,26 +10,28 @@ // unused props - we need to list them here in order to prevent warnings export let id: $$Props['id'] = ''; + export let width: $$Props['width'] = undefined; + export let height: $$Props['height'] = undefined; export let selected: $$Props['selected'] = undefined; export let sourcePosition: $$Props['sourcePosition'] = undefined; export let type: $$Props['type'] = undefined; export let zIndex: $$Props['zIndex'] = undefined; export let dragging: $$Props['dragging'] = false; export let dragHandle: $$Props['dragHandle'] = undefined; - export let xPos: $$Props['xPos'] = 0; - export let yPos: $$Props['yPos'] = 0; + export let positionAbsolute: $$Props['positionAbsolute'] = { x: 0, y: 0 }; export let isConnectable: $$Props['isConnectable'] = undefined; // @todo: there must be a better way to do this id; + width; + height; selected; sourcePosition; type; zIndex; dragging; dragHandle; - xPos; - yPos; + positionAbsolute; isConnectable; diff --git a/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte b/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte index 48ec792b..b4b05a1b 100644 --- a/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte +++ b/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte @@ -68,14 +68,14 @@ isParent={!!node[internalsSymbol]?.isParent} style={node.style} class={node.class} - type={node.type || 'default'} + type={node.type ?? 'default'} sourcePosition={node.sourcePosition} targetPosition={node.targetPosition} dragging={node.dragging} zIndex={node[internalsSymbol]?.z ?? 0} dragHandle={node.dragHandle} - width={node.width} - height={node.height} + width={node.width ?? undefined} + height={node.height ?? undefined} initialized={(!!node.computed?.width && !!node.computed?.height) || (!!node.width && !!node.height)} {resizeObserver} diff --git a/packages/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts index 1e354a55..b55f3f54 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -52,8 +52,9 @@ export type NodeProps = { selected: NodeBase['selected']; isConnectable: NodeBase['connectable']; zIndex: NodeBase['zIndex']; - xPos: number; - yPos: number; + positionAbsolute: XYPosition; + width?: number; + height?: number; dragging: boolean; targetPosition?: Position; sourcePosition?: Position; diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index fe50e1be..6a946a10 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -254,8 +254,8 @@ export function calcNextPosition( ? [ [parentPos.x + nodeWidth * currNodeOrigin[0], parentPos.y + nodeHeight * currNodeOrigin[1]], [ - parentPos.x + parentNode.computed.width - nodeWidth + nodeWidth * currNodeOrigin[0], - parentPos.y + parentNode.computed.height - nodeHeight + nodeHeight * currNodeOrigin[1], + parentPos.x + (parentNode.computed?.width ?? 0) - nodeWidth + nodeWidth * currNodeOrigin[0], + parentPos.y + (parentNode.computed?.height ?? 0) - nodeHeight + nodeHeight * currNodeOrigin[1], ], ] : currentExtent; From 8fe8f73ed3d7fc2a14b2ecbefffb1b988b2edaf8 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 23 Nov 2023 13:51:49 +0100 Subject: [PATCH 23/23] chore(svelte/system): bump --- packages/svelte/CHANGELOG.md | 15 +++++++++++++++ packages/svelte/package.json | 2 +- packages/system/package.json | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index f32356f5..60674c25 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,3 +1,18 @@ +## 0.0.28 + +There are some breaking changes again (sorry!) but we are very close to the final API for Svelte Flow 1.0.0. The biggest change is that we group node attriubutes (`width`, `height`, `positionAbsolute`) that are added by the library under `node.computed`. This makes it easier to understand, that this stuff comes from the library itself. `node.width` and `node.height` is still an optional node option and can be used to set certain dimensions for SSR or on the client. + +- add `` component +- add `on:selectionclick` and `on:selectioncontextmenu` event handlers +- add `ondelete({ nodes, edges })` handler +- add `zoomActivationKey` prop +- add `width` and `height` prop to custom `NodeProps` type +- ⚠️ replace `xPos` and `yPos` with `positionAbsolute` prop to custom `NodeProps` type +- ⚠️ `node.width/height` and `node.positionAbsolute` can now be found under `node.computed.width/height/positionAbsolute` +- ⚠️ `node.width/height` is still optional an can be used for forcing certain dimensions and SSR +- refactor keys: you can now disable keys by setting them to `null` (e.g. `selectionKey={null}`) +- performance optimization with internal node lookup + ## 0.0.27 - add `selectionOnDrag` prop - can be used to create figma-like controls in combination with `panOnDrag={false}` / `panOnDrag={[1, 2]}` + `panOnScroll={true}` diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 3beae0b8..5ed9f8e9 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@xyflow/svelte", - "version": "0.0.27", + "version": "0.0.28", "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 d68f2a13..10729670 100644 --- a/packages/system/package.json +++ b/packages/system/package.json @@ -1,6 +1,6 @@ { "name": "@xyflow/system", - "version": "0.0.10", + "version": "0.0.11", "description": "xyflow core system that powers React Flow and Svelte Flow.", "keywords": [ "node-based UI",