From 1fb668d93651b1c2cd9152bf4b1d71ec69aa4179 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Thu, 18 Jan 2024 12:14:12 +0100 Subject: [PATCH 01/32] Added onInit and useInitialized hooks --- .../components/CallOnMount/CallOnMount.svelte | 14 +++++++++ .../src/lib/components/CallOnMount/index.ts | 1 + .../EdgeRenderer/EdgeRenderer.svelte | 14 ++++++++- .../container/SvelteFlow/SvelteFlow.svelte | 11 +++++++ .../src/lib/container/SvelteFlow/types.ts | 2 ++ .../svelte/src/lib/container/Zoom/Zoom.svelte | 8 ++++- .../svelte/src/lib/hooks/useInitialized.ts | 28 +++++++++++++++++ packages/svelte/src/lib/index.ts | 1 + packages/svelte/src/lib/store/index.ts | 31 +++++++++++++++++++ .../svelte/src/lib/store/initial-store.ts | 6 +++- 10 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 packages/svelte/src/lib/components/CallOnMount/CallOnMount.svelte create mode 100644 packages/svelte/src/lib/components/CallOnMount/index.ts create mode 100644 packages/svelte/src/lib/hooks/useInitialized.ts diff --git a/packages/svelte/src/lib/components/CallOnMount/CallOnMount.svelte b/packages/svelte/src/lib/components/CallOnMount/CallOnMount.svelte new file mode 100644 index 00000000..4184da6f --- /dev/null +++ b/packages/svelte/src/lib/components/CallOnMount/CallOnMount.svelte @@ -0,0 +1,14 @@ + diff --git a/packages/svelte/src/lib/components/CallOnMount/index.ts b/packages/svelte/src/lib/components/CallOnMount/index.ts new file mode 100644 index 00000000..3dee0d92 --- /dev/null +++ b/packages/svelte/src/lib/components/CallOnMount/index.ts @@ -0,0 +1 @@ +export { default as CallOnMount } from './CallOnMount.svelte'; diff --git a/packages/svelte/src/lib/container/EdgeRenderer/EdgeRenderer.svelte b/packages/svelte/src/lib/container/EdgeRenderer/EdgeRenderer.svelte index 663b72ea..420259c3 100644 --- a/packages/svelte/src/lib/container/EdgeRenderer/EdgeRenderer.svelte +++ b/packages/svelte/src/lib/container/EdgeRenderer/EdgeRenderer.svelte @@ -1,6 +1,7 @@
createMarkerIds(edges, { defaultColor, id }) ), + initialized: (() => { + console.log('This closure gets called'); + let initialized = false; + const initialNodesLength = get(store.nodes).length; + const initialEdgesLength = get(store.edges).length; + return derived( + [store.nodesInitialized, store.edgesInitialized, store.viewportInitialized], + ([nodesInitialized, edgesInitialized, viewportInitialized]) => { + console.log('Get the derived store even called?'); + // If it was already initialized once return true from then on + if (initialized) return initialized; + + // if it hasn't been initialised check if is now + if (initialNodesLength === 0) { + initialized = viewportInitialized; + return initialized; + } + if (initialEdgesLength === 0) { + initialized = viewportInitialized && nodesInitialized; + return initialized; + } + + initialized = viewportInitialized && nodesInitialized && edgesInitialized; + return initialized; + } + ); + })(), // actions syncNodeStores: (nodes) => syncNodeStores(store.nodes, nodes), diff --git a/packages/svelte/src/lib/store/initial-store.ts b/packages/svelte/src/lib/store/initial-store.ts index 26c57c86..85d5e84b 100644 --- a/packages/svelte/src/lib/store/initial-store.ts +++ b/packages/svelte/src/lib/store/initial-store.ts @@ -152,6 +152,10 @@ export const getInitialStore = ({ onconnect: writable(undefined), onconnectstart: writable(undefined), onconnectend: writable(undefined), - onbeforedelete: writable(undefined) + onbeforedelete: writable(undefined), + nodesInitialized: writable(false), + edgesInitialized: writable(false), + viewportInitialized: writable(false), + initialized: readable(false) }; }; From a4145a33bd8daa7b13f43746aa2dba06abca8e28 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Thu, 18 Jan 2024 12:28:01 +0100 Subject: [PATCH 02/32] Optimized useNodesInitialized selector --- packages/react/src/hooks/useNodesInitialized.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/react/src/hooks/useNodesInitialized.ts b/packages/react/src/hooks/useNodesInitialized.ts index e70ba7cd..3a278f6c 100644 --- a/packages/react/src/hooks/useNodesInitialized.ts +++ b/packages/react/src/hooks/useNodesInitialized.ts @@ -12,9 +12,15 @@ const selector = (options: UseNodesInitializedOptions) => (s: ReactFlowState) => return false; } - return s.nodes - .filter((n) => (options.includeHiddenNodes ? true : !n.hidden)) - .every((n) => n[internalsSymbol]?.handleBounds !== undefined); + for (const node of s.nodes) { + if (options.includeHiddenNodes || !node.hidden) { + if (node[internalsSymbol]?.handleBounds === undefined) { + return false; + } + } + } + + return true; }; const defaultOptions = { From 6bb9c5360d7831c8f9e2a34b45877e88bd4758fe Mon Sep 17 00:00:00 2001 From: peterkogo Date: Thu, 18 Jan 2024 13:14:44 +0100 Subject: [PATCH 03/32] Cleaned up edge label styles and added default edge label background in svelte flow --- packages/react/src/styles/base.css | 18 ------------------ packages/react/src/styles/style.css | 14 ++------------ packages/svelte/src/styles/base.css | 9 --------- packages/svelte/src/styles/style.css | 12 +++--------- packages/system/src/styles/style.css | 6 ++++++ 5 files changed, 11 insertions(+), 48 deletions(-) diff --git a/packages/react/src/styles/base.css b/packages/react/src/styles/base.css index 4a7281dc..d6be5b10 100644 --- a/packages/react/src/styles/base.css +++ b/packages/react/src/styles/base.css @@ -1,21 +1,3 @@ /* 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 2ff7f6c1..71b4d08c 100644 --- a/packages/react/src/styles/style.css +++ b/packages/react/src/styles/style.css @@ -3,20 +3,10 @@ @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)); + fill: var(--xy-edge-label-background-color, var(--xy-edge-label-background-color-default)); } .react-flow__edge-text { - fill: var(--edge-label-color, var(--edge-label-color-default)); + fill: var(--xy-edge-label-color, var(--xy-edge-label-color-default)); } diff --git a/packages/svelte/src/styles/base.css b/packages/svelte/src/styles/base.css index b9861c98..80bc953d 100644 --- a/packages/svelte/src/styles/base.css +++ b/packages/svelte/src/styles/base.css @@ -2,16 +2,7 @@ @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 308a6a1e..e607d0f8 100644 --- a/packages/svelte/src/styles/style.css +++ b/packages/svelte/src/styles/style.css @@ -3,19 +3,13 @@ @import '../../../system/src/styles/style.css'; @import '../../../system/src/styles/node-resizer.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; + padding: 2px; font-size: 10px; - color: var(--edge-label-color, var(--edge-label-color-default)); + color: var(--xy-edge-label-color, var(--xy-edge-label-color-default)); + background: var(--xy-edge-label-background-color, var(--xy-edge-label-background-color-default)); } .svelte-flow__nodes { diff --git a/packages/system/src/styles/style.css b/packages/system/src/styles/style.css index 880ebaf7..cb6f2b7c 100644 --- a/packages/system/src/styles/style.css +++ b/packages/system/src/styles/style.css @@ -19,6 +19,9 @@ --xy-controls-button-color-hover-default: inherit; --xy-controls-button-border-color-default: #eee; --xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08); + + --xy-edge-label-background-color-default: #ffffff; + --xy-edge-label-color-default: inherit; } .xy-flow.dark { @@ -41,6 +44,9 @@ --xy-controls-button-color-hover-default: #fff; --xy-controls-button-border-color-default: #5b5b5b; --xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08); + + --xy-edge-label-background-color-default: #141414; + --xy-edge-label-color-default: #f8f8f8; } .xy-flow__edge { From 07ee0c5bba582199127828f01640d2aaf72a1d19 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 18 Jan 2024 14:10:04 +0100 Subject: [PATCH 04/32] chore(types): handle ReactFlow type annotations --- packages/react/src/components/Handle/index.tsx | 5 ++++- packages/react/src/types/component-props.ts | 4 ++-- packages/svelte/src/lib/components/Handle/Handle.svelte | 4 ++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/react/src/components/Handle/index.tsx b/packages/react/src/components/Handle/index.tsx index 362027ab..36778039 100644 --- a/packages/react/src/components/Handle/index.tsx +++ b/packages/react/src/components/Handle/index.tsx @@ -22,7 +22,7 @@ import { useStore, useStoreApi } from '../../hooks/useStore'; import { useNodeId } from '../../contexts/NodeIdContext'; import { type ReactFlowState } from '../../types'; -export type HandleComponentProps = HandleProps & Omit, 'id'>; +export interface HandleComponentProps extends HandleProps, Omit, 'id'> {} const selector = (s: ReactFlowState) => ({ connectOnClick: s.connectOnClick, @@ -221,4 +221,7 @@ const HandleComponent = forwardRef( HandleComponent.displayName = 'Handle'; +/** + * The Handle component is the part of a node that can be used to connect nodes. + */ export const Handle = memo(HandleComponent); diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index 213b9d8c..50f07303 100644 --- a/packages/react/src/types/component-props.ts +++ b/packages/react/src/types/component-props.ts @@ -50,7 +50,7 @@ import type { * ReactFlow component props. * @public */ -export type ReactFlowProps = Omit, 'onError'> & { +export interface ReactFlowProps extends Omit, 'onError'> { /** An array of nodes to render in a controlled flow. * @example * const nodes = [ @@ -502,6 +502,6 @@ export type ReactFlowProps = Omit, 'onError'> & { * @example 'system' | 'light' | 'dark' */ colorMode?: ColorMode; -}; +} export type ReactFlowRefType = HTMLDivElement; diff --git a/packages/svelte/src/lib/components/Handle/Handle.svelte b/packages/svelte/src/lib/components/Handle/Handle.svelte index 212efe56..87b6c9d2 100644 --- a/packages/svelte/src/lib/components/Handle/Handle.svelte +++ b/packages/svelte/src/lib/components/Handle/Handle.svelte @@ -126,6 +126,10 @@ // @todo implement connectablestart, connectableend +
Date: Thu, 18 Jan 2024 14:26:35 +0100 Subject: [PATCH 05/32] Added handleEdgeSelect hook & click on edge-label now selects edge --- .../lib/components/BaseEdge/BaseEdge.svelte | 14 ++---- .../lib/components/EdgeLabel/EdgeLabel.svelte | 27 +++++++++++ .../src/lib/components/EdgeLabel/index.ts | 1 + .../components/EdgeWrapper/EdgeWrapper.svelte | 45 ++++++------------- .../EdgeRenderer/EdgeRenderer.svelte | 9 +--- .../src/lib/hooks/useHandleEdgeSelect.ts | 39 ++++++++++++++++ packages/svelte/src/styles/style.css | 1 + 7 files changed, 86 insertions(+), 50 deletions(-) create mode 100644 packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte create mode 100644 packages/svelte/src/lib/components/EdgeLabel/index.ts create mode 100644 packages/svelte/src/lib/hooks/useHandleEdgeSelect.ts diff --git a/packages/svelte/src/lib/components/BaseEdge/BaseEdge.svelte b/packages/svelte/src/lib/components/BaseEdge/BaseEdge.svelte index d1c60f8f..b0591ce2 100644 --- a/packages/svelte/src/lib/components/BaseEdge/BaseEdge.svelte +++ b/packages/svelte/src/lib/components/BaseEdge/BaseEdge.svelte @@ -1,7 +1,7 @@ + + +
{ + if (id) handleEdgeSelect(id); + }} + > + +
+
diff --git a/packages/svelte/src/lib/components/EdgeLabel/index.ts b/packages/svelte/src/lib/components/EdgeLabel/index.ts new file mode 100644 index 00000000..2e0a457c --- /dev/null +++ b/packages/svelte/src/lib/components/EdgeLabel/index.ts @@ -0,0 +1 @@ +export { default as EdgeLabel } from './EdgeLabel.svelte'; diff --git a/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte b/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte index c68ea1d5..8809eec7 100644 --- a/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte +++ b/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte @@ -2,13 +2,13 @@ @@ -43,7 +42,7 @@ {/if} {#if label} - + {label} {/if} diff --git a/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte b/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte index 98467787..eb0c794f 100644 --- a/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte +++ b/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte @@ -4,9 +4,9 @@ import { getContext } from 'svelte'; import type { BaseEdgeProps } from '../BaseEdge/types'; - export let labelStyle: BaseEdgeProps['labelStyle'] = undefined; - export let labelX: BaseEdgeProps['labelX'] = undefined; - export let labelY: BaseEdgeProps['labelY'] = undefined; + export let style: BaseEdgeProps['labelStyle'] = undefined; + export let x: BaseEdgeProps['labelX'] = undefined; + export let y: BaseEdgeProps['labelY'] = undefined; const handleEdgeSelect = useHandleEdgeSelect(); @@ -16,8 +16,8 @@
{ if (id) handleEdgeSelect(id); }} From e0d7dbe8a104d6cb29933a8c32784238b1f48e87 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Thu, 18 Jan 2024 15:50:21 +0100 Subject: [PATCH 07/32] fixed focused, selectable css --- packages/react/src/components/EdgeWrapper/index.tsx | 1 + packages/system/src/styles/init.css | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/EdgeWrapper/index.tsx b/packages/react/src/components/EdgeWrapper/index.tsx index cb9b9c8b..28323a19 100644 --- a/packages/react/src/components/EdgeWrapper/index.tsx +++ b/packages/react/src/components/EdgeWrapper/index.tsx @@ -186,6 +186,7 @@ export function EdgeWrapper({ animated: edge.animated, inactive: !isSelectable && !onClick, updating: updateHover, + selectable: isSelectable, }, ])} onClick={onEdgeClick} diff --git a/packages/system/src/styles/init.css b/packages/system/src/styles/init.css index a688f6b9..14519c74 100644 --- a/packages/system/src/styles/init.css +++ b/packages/system/src/styles/init.css @@ -139,8 +139,8 @@ } &.selected .xy-flow__edge-path, - &:focus .xy-flow__edge-path, - &:focus-visible .xy-flow__edge-path { + &.selectable:focus .xy-flow__edge-path, + &.selectable:focus-visible .xy-flow__edge-path { stroke: var(--xy-edge-stroke-selected, var(--xy-edge-stroke-selected-default)); } From aee1b6b9f80efd6de9762a76ad89c60d602419cf Mon Sep 17 00:00:00 2001 From: peterkogo Date: Thu, 18 Jan 2024 16:27:51 +0100 Subject: [PATCH 08/32] add getNode, getNodes, getEdge, getEdges --- packages/svelte/src/lib/hooks/useSvelteFlow.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/lib/hooks/useSvelteFlow.ts b/packages/svelte/src/lib/hooks/useSvelteFlow.ts index d47ee45b..0cf53f5c 100644 --- a/packages/svelte/src/lib/hooks/useSvelteFlow.ts +++ b/packages/svelte/src/lib/hooks/useSvelteFlow.ts @@ -29,6 +29,10 @@ import { isNode } from '$lib/utils'; export function useSvelteFlow(): { zoomIn: ZoomInOut; zoomOut: ZoomInOut; + getNode: (id: string) => Node | undefined; + getNodes: (ids: string[]) => (Node | undefined)[]; + getEdge: (id: string) => Edge | undefined; + getEdges: (ids: string[]) => (Edge | undefined)[]; setZoom: (zoomLevel: number, options?: ViewportHelperFunctionOptions) => void; getZoom: () => number; setCenter: (x: number, y: number, options?: SetCenterOptions) => void; @@ -82,7 +86,9 @@ export function useSvelteFlow(): { panZoom, nodes, edges, - domNode + domNode, + nodeLookup, + edgeLookup } = useStore(); const getNodeRect = ( @@ -121,6 +127,10 @@ export function useSvelteFlow(): { return { zoomIn, zoomOut, + getNode: (id) => get(nodeLookup).get(id), + getNodes: (ids) => ids.map((id) => get(nodeLookup).get(id)), + getEdge: (id) => get(edgeLookup).get(id), + getEdges: (ids) => ids.map((id) => get(edgeLookup).get(id)), setZoom: (zoomLevel, options) => { get(panZoom)?.scaleTo(zoomLevel, { duration: options?.duration }); }, From 601c7f1c15377d56de68be4d9a26e6eda64258f8 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Mon, 22 Jan 2024 11:26:02 +0100 Subject: [PATCH 09/32] Selection process is not interrupted by selectionKey being let go --- packages/react/src/container/FlowRenderer/index.tsx | 8 +++++--- packages/svelte/src/lib/container/Pane/Pane.svelte | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/react/src/container/FlowRenderer/index.tsx b/packages/react/src/container/FlowRenderer/index.tsx index 06d6fab6..2670be70 100644 --- a/packages/react/src/container/FlowRenderer/index.tsx +++ b/packages/react/src/container/FlowRenderer/index.tsx @@ -28,7 +28,9 @@ export type FlowRendererProps = Omit< children: ReactNode; }; -const selector = (s: ReactFlowState) => s.nodesSelectionActive; +const selector = (s: ReactFlowState) => { + return { nodesSelectionActive: s.nodesSelectionActive, userSelectionActive: s.userSelectionActive }; +}; const FlowRendererComponent = ({ children, @@ -67,13 +69,13 @@ const FlowRendererComponent = ({ onViewportChange, isControlledViewport, }: FlowRendererProps) => { - const nodesSelectionActive = useStore(selector); + const { nodesSelectionActive, userSelectionActive } = useStore(selector); const selectionKeyPressed = useKeyPress(selectionKeyCode); const panActivationKeyPressed = useKeyPress(panActivationKeyCode); const panOnDrag = panActivationKeyPressed || _panOnDrag; const panOnScroll = panActivationKeyPressed || _panOnScroll; - const isSelecting = selectionKeyPressed || (selectionOnDrag && panOnDrag !== true); + const isSelecting = selectionKeyPressed || userSelectionActive || (selectionOnDrag && panOnDrag !== true); useGlobalKeyHandler({ deleteKeyCode, multiSelectionKeyCode }); diff --git a/packages/svelte/src/lib/container/Pane/Pane.svelte b/packages/svelte/src/lib/container/Pane/Pane.svelte index cce95636..f0df0980 100644 --- a/packages/svelte/src/lib/container/Pane/Pane.svelte +++ b/packages/svelte/src/lib/container/Pane/Pane.svelte @@ -73,7 +73,8 @@ let selectedNodes: Node[] = []; $: _panOnDrag = $panActivationKeyPressed || panOnDrag; - $: isSelecting = $selectionKeyPressed || (selectionOnDrag && _panOnDrag !== true); + $: isSelecting = + $selectionKeyPressed || $selectionRect || (selectionOnDrag && _panOnDrag !== true); $: hasActiveSelection = $elementsSelectable && (isSelecting || $selectionRectMode === 'user'); function onClick(event: MouseEvent | TouchEvent) { From efbf2b35824cdf68f82d1d4332602cb9785f5368 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Mon, 22 Jan 2024 16:34:01 +0100 Subject: [PATCH 10/32] useInitialized and useNodesInitialized now return a readable, useEdgesInitialized is not exported --- packages/svelte/src/lib/hooks/useInitialized.ts | 9 +++++++-- packages/svelte/src/lib/index.ts | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/lib/hooks/useInitialized.ts b/packages/svelte/src/lib/hooks/useInitialized.ts index 93e6921b..09b0307c 100644 --- a/packages/svelte/src/lib/hooks/useInitialized.ts +++ b/packages/svelte/src/lib/hooks/useInitialized.ts @@ -1,4 +1,5 @@ import { useStore } from '$lib/store'; +import type { Readable } from 'svelte/store'; /** * Hook for seeing if nodes are initialized @@ -6,7 +7,9 @@ import { useStore } from '$lib/store'; */ export function useNodesInitialized() { const { nodesInitialized } = useStore(); - return nodesInitialized; + return { + subscribe: nodesInitialized.subscribe + } as Readable; } /** @@ -24,5 +27,7 @@ export function useEdgesInitialized() { */ export function useInitialized() { const { initialized } = useStore(); - return initialized; + return { + subscribe: initialized.subscribe + } as Readable; } diff --git a/packages/svelte/src/lib/index.ts b/packages/svelte/src/lib/index.ts index c88059b7..75f0092b 100644 --- a/packages/svelte/src/lib/index.ts +++ b/packages/svelte/src/lib/index.ts @@ -31,7 +31,7 @@ export * from '$lib/hooks/useConnection'; export * from '$lib/hooks/useNodesEdges'; export * from '$lib/hooks/useHandleConnections'; export * from '$lib/hooks/useNodesData'; -export * from '$lib/hooks/useInitialized'; +export { useInitialized, useNodesInitialized } from '$lib/hooks/useInitialized'; // types export type { From 08bb803ef2a6879e1e6342a46eb6e2c0ac604541 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Mon, 22 Jan 2024 16:38:00 +0100 Subject: [PATCH 11/32] getNodes & getEdges can be called without specifying ids --- packages/svelte/src/lib/hooks/useSvelteFlow.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/lib/hooks/useSvelteFlow.ts b/packages/svelte/src/lib/hooks/useSvelteFlow.ts index 0cf53f5c..5cc414bc 100644 --- a/packages/svelte/src/lib/hooks/useSvelteFlow.ts +++ b/packages/svelte/src/lib/hooks/useSvelteFlow.ts @@ -30,9 +30,9 @@ export function useSvelteFlow(): { zoomIn: ZoomInOut; zoomOut: ZoomInOut; getNode: (id: string) => Node | undefined; - getNodes: (ids: string[]) => (Node | undefined)[]; + getNodes: (ids?: string[]) => (Node | undefined)[]; getEdge: (id: string) => Edge | undefined; - getEdges: (ids: string[]) => (Edge | undefined)[]; + getEdges: (ids?: string[]) => (Edge | undefined)[]; setZoom: (zoomLevel: number, options?: ViewportHelperFunctionOptions) => void; getZoom: () => number; setCenter: (x: number, y: number, options?: SetCenterOptions) => void; @@ -128,9 +128,19 @@ export function useSvelteFlow(): { zoomIn, zoomOut, getNode: (id) => get(nodeLookup).get(id), - getNodes: (ids) => ids.map((id) => get(nodeLookup).get(id)), + getNodes: (ids) => { + if (!ids) { + return get(nodes); + } + return ids.map((id) => get(nodeLookup).get(id)); + }, getEdge: (id) => get(edgeLookup).get(id), - getEdges: (ids) => ids.map((id) => get(edgeLookup).get(id)), + getEdges: (ids) => { + if (!ids) { + return get(edges); + } + return ids.map((id) => get(edgeLookup).get(id)); + }, setZoom: (zoomLevel, options) => { get(panZoom)?.scaleTo(zoomLevel, { duration: options?.duration }); }, From 34c77caab393301cc918f1522c939e2f7b58625c Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 22 Jan 2024 17:05:26 +0100 Subject: [PATCH 12/32] refactor(types): OnNodeDrag --- examples/react/src/examples/Basic/index.tsx | 3 +- .../examples/CustomNode/ColorSelectorNode.tsx | 6 ++-- .../react/src/examples/CustomNode/index.tsx | 33 ++++++++++++++----- .../handle-connect/MultiHandleNode.svelte | 9 +++-- packages/react/src/index.ts | 1 - packages/react/src/types/component-props.ts | 8 ++--- packages/react/src/types/general.ts | 2 +- packages/react/src/types/nodes.ts | 12 ++++--- packages/react/src/types/store.ts | 2 +- packages/svelte/src/lib/index.ts | 1 - packages/svelte/src/lib/store/index.ts | 3 +- packages/system/src/types/nodes.ts | 8 ++--- packages/system/src/xydrag/XYDrag.ts | 12 +++---- 13 files changed, 59 insertions(+), 41 deletions(-) diff --git a/examples/react/src/examples/Basic/index.tsx b/examples/react/src/examples/Basic/index.tsx index 8d9c7dfd..f454b313 100644 --- a/examples/react/src/examples/Basic/index.tsx +++ b/examples/react/src/examples/Basic/index.tsx @@ -10,9 +10,10 @@ import { Edge, useReactFlow, Panel, + OnNodeDrag, } from '@xyflow/react'; -const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node); +const onNodeDrag: OnNodeDrag = (_, node) => console.log('drag', node); const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); diff --git a/examples/react/src/examples/CustomNode/ColorSelectorNode.tsx b/examples/react/src/examples/CustomNode/ColorSelectorNode.tsx index 6857e1ac..72255e27 100644 --- a/examples/react/src/examples/CustomNode/ColorSelectorNode.tsx +++ b/examples/react/src/examples/CustomNode/ColorSelectorNode.tsx @@ -1,6 +1,8 @@ -import React, { memo, FC, CSSProperties, useCallback, useEffect } from 'react'; +import React, { memo, FC, CSSProperties, useCallback } from 'react'; import { Handle, Position, NodeProps, Connection, Edge, useOnViewportChange, Viewport } from '@xyflow/react'; +import type { ColorSelectorNode } from '.'; + const targetHandleStyle: CSSProperties = { background: '#555' }; const sourceHandleStyleA: CSSProperties = { ...targetHandleStyle, top: 10 }; const sourceHandleStyleB: CSSProperties = { @@ -11,7 +13,7 @@ const sourceHandleStyleB: CSSProperties = { const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params); -const ColorSelectorNode: FC = ({ data, isConnectable }) => { +const ColorSelectorNode: FC> = ({ data, isConnectable }) => { const onStart = useCallback((viewport: Viewport) => console.log('onStart', viewport), []); const onChange = useCallback((viewport: Viewport) => console.log('onChange', viewport), []); const onEnd = useCallback((viewport: Viewport) => console.log('onEnd', viewport), []); diff --git a/examples/react/src/examples/CustomNode/index.tsx b/examples/react/src/examples/CustomNode/index.tsx index ce7d0f62..986638a0 100644 --- a/examples/react/src/examples/CustomNode/index.tsx +++ b/examples/react/src/examples/CustomNode/index.tsx @@ -5,24 +5,32 @@ import { Controls, addEdge, Node, - ReactFlowInstance, Position, SnapGrid, Connection, - useNodesState, useEdgesState, Background, Edge, + OnNodeDrag, + OnInit, + applyNodeChanges, + OnNodesChange, } from '@xyflow/react'; import ColorSelectorNode from './ColorSelectorNode'; -const onInit = (reactFlowInstance: ReactFlowInstance) => { +export type ColorSelectorNode = Node< + { color: string; onChange: (event: ChangeEvent) => void }, + 'selectorNode' +>; +export type MyNode = Node | ColorSelectorNode; + +const onInit: OnInit = (reactFlowInstance) => { console.log('flow loaded:', reactFlowInstance); }; -const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); -const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); +const onNodeDragStop: OnNodeDrag = (_, node) => console.log('drag stop', node); +const onNodeClick = (_: MouseEvent, node: MyNode) => console.log('click', node); const initBgColor = '#1A192B'; @@ -34,7 +42,16 @@ const nodeTypes = { }; const CustomNodeFlow = () => { - const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [nodes, setNodes] = useState([]); + const onNodesChange: OnNodesChange = useCallback( + (changes) => + setNodes((nds) => { + const nextNodes = applyNodeChanges(changes, nds); + return nextNodes; + }), + [setNodes] + ); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [bgColor, setBgColor] = useState(initBgColor); @@ -145,14 +162,14 @@ const CustomNodeFlow = () => { maxZoom={2} > { + nodeStrokeColor={(n: MyNode): string => { if (n.type === 'input') return '#0041d0'; if (n.type === 'selectorNode') return bgColor; if (n.type === 'output') return '#ff0072'; return '#eee'; }} - nodeColor={(n: Node): string => { + nodeColor={(n: MyNode): string => { if (n.type === 'selectorNode') return bgColor; return '#fff'; diff --git a/examples/svelte/src/routes/examples/handle-connect/MultiHandleNode.svelte b/examples/svelte/src/routes/examples/handle-connect/MultiHandleNode.svelte index ebc85c74..f411fc44 100644 --- a/examples/svelte/src/routes/examples/handle-connect/MultiHandleNode.svelte +++ b/examples/svelte/src/routes/examples/handle-connect/MultiHandleNode.svelte @@ -43,10 +43,8 @@ export let zIndex: $$Props['zIndex'] = undefined; export let dragging: $$Props['dragging'] = false; export let dragHandle: $$Props['dragHandle'] = undefined; - export let positionAbsolute: $$Props['positionAbsolute'] = { - x: 0, - y: 0 - }; + export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0; + export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0; export let isConnectable: $$Props['isConnectable'] = undefined; data; @@ -59,7 +57,8 @@ zIndex; dragging; dragHandle; - positionAbsolute; + positionAbsoluteX; + positionAbsoluteY; isConnectable; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 3cabb813..8ad67000 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -67,7 +67,6 @@ export { type OnError, type NodeProps, type NodeOrigin, - type OnNodeDrag, type OnSelectionDrag, Position, type XYPosition, diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index 50f07303..f8bc8dfd 100644 --- a/packages/react/src/types/component-props.ts +++ b/packages/react/src/types/component-props.ts @@ -40,10 +40,10 @@ import type { OnDelete, OnNodesChange, OnEdgesChange, - NodeDragHandler, NodeMouseHandler, SelectionDragHandler, EdgeMouseHandler, + OnNodeDrag, } from '.'; /** @@ -111,11 +111,11 @@ export interface ReactFlowProps extends Omit, 'on /** This event handler is called when a user right clicks on a node */ onNodeContextMenu?: NodeMouseHandler; /** This event handler is called when a user starts to drag a node */ - onNodeDragStart?: NodeDragHandler; + onNodeDragStart?: OnNodeDrag; /** This event handler is called when a user drags a node */ - onNodeDrag?: NodeDragHandler; + onNodeDrag?: OnNodeDrag; /** This event handler is called when a user stops dragging a node */ - onNodeDragStop?: NodeDragHandler; + onNodeDragStop?: OnNodeDrag; /** This event handler is called when a user clicks on an edge */ onEdgeClick?: (event: ReactMouseEvent, edge: Edge) => void; /** This event handler is called when a user right clicks on an edge */ diff --git a/packages/react/src/types/general.ts b/packages/react/src/types/general.ts index 9a36e41f..1a7462ad 100644 --- a/packages/react/src/types/general.ts +++ b/packages/react/src/types/general.ts @@ -19,7 +19,7 @@ import { ComponentType } from 'react'; export type OnNodesChange = (changes: NodeChange[]) => void; export type OnEdgesChange = (changes: EdgeChange[]) => void; -export type OnNodesDelete = (nodes: Node[]) => void; +export type OnNodesDelete = (nodes: NodeType[]) => void; export type OnEdgesDelete = (edges: Edge[]) => void; export type OnDelete = (params: { nodes: Node[]; edges: Edge[] }) => void; diff --git a/packages/react/src/types/nodes.ts b/packages/react/src/types/nodes.ts index 71bfb685..612c1d50 100644 --- a/packages/react/src/types/nodes.ts +++ b/packages/react/src/types/nodes.ts @@ -3,11 +3,11 @@ import type { CoordinateExtent, NodeBase, NodeOrigin, OnError } from '@xyflow/sy import { NodeTypes } from './general'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any /** * The node data structure that gets used for the nodes prop. * @public */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export type Node = NodeBase< NodeData, NodeType @@ -18,9 +18,13 @@ export type Node void; -export type NodeDragHandler = (event: ReactMouseEvent, node: Node, nodes: Node[]) => void; -export type SelectionDragHandler = (event: ReactMouseEvent, nodes: Node[]) => void; +export type NodeMouseHandler = (event: ReactMouseEvent, node: NodeType) => void; +export type SelectionDragHandler = (event: ReactMouseEvent, nodes: NodeType[]) => void; +export type OnNodeDrag = ( + event: ReactMouseEvent, + node: NodeType, + nodes: NodeType[] +) => void; export type NodeWrapperProps = { id: string; diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index b138fc9d..a0d5f8cc 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -17,7 +17,6 @@ import { type PanBy, type OnConnectStart, type OnConnectEnd, - type OnNodeDrag, type OnSelectionDrag, type OnMoveStart, type OnMove, @@ -43,6 +42,7 @@ import type { OnSelectionChangeFunc, UnselectNodesAndEdgesParams, OnDelete, + OnNodeDrag, } from '.'; export type ReactFlowStore = { diff --git a/packages/svelte/src/lib/index.ts b/packages/svelte/src/lib/index.ts index 4c3737ea..edaed9ee 100644 --- a/packages/svelte/src/lib/index.ts +++ b/packages/svelte/src/lib/index.ts @@ -79,7 +79,6 @@ export { type OnError, type NodeProps, type NodeOrigin, - type OnNodeDrag, type OnSelectionDrag, Position, type XYPosition, diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index 18941bc6..91160860 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -15,7 +15,6 @@ import { type XYPosition, type CoordinateExtent, type UpdateConnection, - type NodeBase, type NodeDragItem, errorMessages } from '@xyflow/system'; @@ -67,7 +66,7 @@ export function createStore({ const updateNodePositions: UpdateNodePositions = (nodeDragItems, dragging = false) => { store.nodes.update((nds) => { return nds.map((node) => { - const nodeDragItem = (nodeDragItems as Array).find( + const nodeDragItem = (nodeDragItems as Array).find( (ndi) => ndi.id === node.id ); diff --git a/packages/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts index 40aa1c16..b75e6d1b 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -94,9 +94,9 @@ export type NodeProps = { positionAbsoluteY: number; width?: number; height?: number; - dragging: boolean; - targetPosition?: Position; - sourcePosition?: Position; + dragging: NodeBase['dragging']; + sourcePosition?: NodeBase['sourcePosition']; + targetPosition?: NodeBase['targetPosition']; }; export type NodeHandleBounds = { @@ -134,8 +134,6 @@ export type NodeDragItem = { export type NodeOrigin = [number, number]; -export type OnNodeDrag = (event: MouseEvent, node: NodeBase, nodes: NodeBase[]) => void; - export type OnSelectionDrag = (event: MouseEvent, nodes: NodeBase[]) => void; export type NodeHandle = Optional; diff --git a/packages/system/src/xydrag/XYDrag.ts b/packages/system/src/xydrag/XYDrag.ts index 79a0823a..40d1aa4b 100644 --- a/packages/system/src/xydrag/XYDrag.ts +++ b/packages/system/src/xydrag/XYDrag.ts @@ -23,7 +23,6 @@ import type { SnapGrid, Transform, PanBy, - OnNodeDrag, OnSelectionDrag, UpdateNodePositions, Box, @@ -31,7 +30,7 @@ import type { export type OnDrag = (event: MouseEvent, dragItems: NodeDragItem[], node: NodeBase, nodes: NodeBase[]) => void; -type StoreItems = { +type StoreItems = { nodes: NodeBase[]; nodeLookup: Map; edges: EdgeBase[]; @@ -58,9 +57,9 @@ type StoreItems = { updateNodePositions: UpdateNodePositions; }; -export type XYDragParams = { +export type XYDragParams = { domNode: Element; - getStoreItems: () => StoreItems; + getStoreItems: () => StoreItems; onDragStart?: OnDrag; onDrag?: OnDrag; onDragStop?: OnDrag; @@ -80,14 +79,15 @@ export type DragUpdateParams = { domNode: Element; }; -export function XYDrag({ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function XYDrag void | undefined>({ domNode, onNodeMouseDown, getStoreItems, onDragStart, onDrag, onDragStop, -}: XYDragParams): XYDragInstance { +}: XYDragParams): XYDragInstance { let lastPos: { x: number | null; y: number | null } = { x: null, y: null }; let autoPanId = 0; let dragItems: NodeDragItem[] = []; From 1f454b42f6c51b4d19ee0a98b2cd8e39f891b6fe Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 22 Jan 2024 17:19:39 +0100 Subject: [PATCH 13/32] chore(svelte): cleanup imports --- .../svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte | 5 +++-- .../svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte | 2 +- packages/svelte/src/lib/hooks/useHandleEdgeSelect.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte b/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte index eb0c794f..81039d0d 100644 --- a/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte +++ b/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte @@ -1,8 +1,9 @@ diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index 52665614..6dbf84e2 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -359,28 +359,24 @@ export function createStore({ ([edges, defaultColor, id]) => createMarkerIds(edges, { defaultColor, id }) ), initialized: (() => { - console.log('This closure gets called'); let initialized = false; const initialNodesLength = get(store.nodes).length; const initialEdgesLength = get(store.edges).length; return derived( [store.nodesInitialized, store.edgesInitialized, store.viewportInitialized], ([nodesInitialized, edgesInitialized, viewportInitialized]) => { - console.log('Get the derived store even called?'); - // If it was already initialized once return true from then on + // If it was already initialized, return true from then on if (initialized) return initialized; - // if it hasn't been initialised check if is now + // if it hasn't been initialised check if it's now if (initialNodesLength === 0) { initialized = viewportInitialized; - return initialized; - } - if (initialEdgesLength === 0) { + } else if (initialEdgesLength === 0) { initialized = viewportInitialized && nodesInitialized; - return initialized; + } else { + initialized = viewportInitialized && nodesInitialized && edgesInitialized; } - initialized = viewportInitialized && nodesInitialized && edgesInitialized; return initialized; } ); From b2614db1037b7ff8d8b3dc01c9b69334cbfdde24 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 23 Jan 2024 15:29:02 +0100 Subject: [PATCH 23/32] chore(svelte): remove useEdgesInit hook --- packages/svelte/src/lib/hooks/useInitialized.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/svelte/src/lib/hooks/useInitialized.ts b/packages/svelte/src/lib/hooks/useInitialized.ts index 09b0307c..0f486abf 100644 --- a/packages/svelte/src/lib/hooks/useInitialized.ts +++ b/packages/svelte/src/lib/hooks/useInitialized.ts @@ -12,15 +12,6 @@ export function useNodesInitialized() { } as Readable; } -/** - * Hook for seeing if edges are initialized - * @returns - edgesInitialized Writable - */ -export function useEdgesInitialized() { - const { edgesInitialized } = useStore(); - return edgesInitialized; -} - /** * Hook for seeing if the flow is initialized * @returns - initialized Writable From befcb3433030a75f919d17d5f57d761638236546 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 23 Jan 2024 15:51:30 +0100 Subject: [PATCH 24/32] chore(svelte): update changelog --- packages/svelte/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 23b550ea..124a1bba 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -5,6 +5,7 @@ ## Minor changes - add `getNode`, `getNodes`, `getEdge` and `getEdges` to `useSvelteFlow` +- add `useInitialized` / `useNNodesInitialized` hooks and `oninit` handler ## Patch changes From 009fb42017c6c483979dc39bcc8181e46cb2ebb1 Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 24 Jan 2024 13:04:44 +0100 Subject: [PATCH 25/32] refactor(system): revise calcNextPosition --- examples/react/src/examples/Subflow/index.tsx | 2 + .../react/src/hooks/useUpdateNodePositions.ts | 33 +++-- packages/react/src/store/initialState.ts | 3 +- packages/system/src/utils/general.ts | 4 + packages/system/src/utils/graph.ts | 114 ++++++++++-------- packages/system/src/xydrag/XYDrag.ts | 18 ++- 6 files changed, 106 insertions(+), 68 deletions(-) diff --git a/examples/react/src/examples/Subflow/index.tsx b/examples/react/src/examples/Subflow/index.tsx index e2e9af89..19dc119a 100644 --- a/examples/react/src/examples/Subflow/index.tsx +++ b/examples/react/src/examples/Subflow/index.tsx @@ -13,6 +13,7 @@ import { MiniMap, Background, Panel, + NodeOrigin, } from '@xyflow/react'; import DebugNode from './DebugNode'; @@ -119,6 +120,7 @@ const initialNodes: Node[] = [ data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light', + extent: 'parent', }, ]; diff --git a/packages/react/src/hooks/useUpdateNodePositions.ts b/packages/react/src/hooks/useUpdateNodePositions.ts index 1ca0d4ee..5f79df90 100644 --- a/packages/react/src/hooks/useUpdateNodePositions.ts +++ b/packages/react/src/hooks/useUpdateNodePositions.ts @@ -1,5 +1,5 @@ import { useCallback } from 'react'; -import { calcNextPosition, snapPosition } from '@xyflow/system'; +import { calculateNodePosition, snapPosition } from '@xyflow/system'; import { Node } from '../types'; import { useStoreApi } from '../hooks/useStore'; @@ -17,7 +17,17 @@ export function useUpdateNodePositions() { const store = useStoreApi(); const updatePositions = useCallback((params: { x: number; y: number; isShiftPressed: boolean }) => { - const { nodeExtent, nodes, snapToGrid, snapGrid, nodesDraggable, onError, updateNodePositions } = store.getState(); + const { + nodeExtent, + nodes, + snapToGrid, + snapGrid, + nodesDraggable, + onError, + updateNodePositions, + nodeLookup, + nodeOrigin, + } = store.getState(); const selectedNodes = nodes.filter(selectedAndDraggable(nodesDraggable)); // by default a node moves 5px on each key press, or 20px if shift is pressed // if snap grid is enabled, we use that for the velocity. @@ -31,27 +41,24 @@ export function useUpdateNodePositions() { const nodeUpdates = selectedNodes.map((node) => { if (node.computed?.positionAbsolute) { let nextPosition = { - x: node.computed?.positionAbsolute.x + xDiff, - y: node.computed?.positionAbsolute.y + yDiff, + x: node.computed.positionAbsolute.x + xDiff, + y: node.computed.positionAbsolute.y + yDiff, }; if (snapToGrid) { nextPosition = snapPosition(nextPosition, snapGrid); } - const { positionAbsolute, position } = calcNextPosition( - node, + const { position, positionAbsolute } = calculateNodePosition({ + nodeId: node.id, nextPosition, - nodes, + nodeLookup, nodeExtent, - undefined, - onError - ); + nodeOrigin, + onError, + }); node.position = position; - if (!node.computed) { - node.computed = {}; - } node.computed.positionAbsolute = positionAbsolute; } diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index c8f92903..109899d6 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -6,6 +6,7 @@ import { getViewportForBounds, Transform, updateConnectionLookup, + devWarn, } from '@xyflow/system'; import type { Edge, Node, ReactFlowStore } from '../types'; @@ -100,7 +101,7 @@ const getInitialState = ({ autoPanOnConnect: true, autoPanOnNodeDrag: true, connectionRadius: 20, - onError: () => null, + onError: devWarn, isValidConnection: undefined, onSelectionChangeHandlers: [], diff --git a/packages/system/src/utils/general.ts b/packages/system/src/utils/general.ts index bc8f4dfe..d6f95a1f 100644 --- a/packages/system/src/utils/general.ts +++ b/packages/system/src/utils/general.ts @@ -198,3 +198,7 @@ export const getViewportForBounds = ( }; export const isMacOs = () => typeof navigator !== 'undefined' && navigator?.userAgent?.indexOf('Mac') >= 0; + +export function isCoordinateExtent(extent?: CoordinateExtent | 'parent'): extent is CoordinateExtent { + return extent !== undefined && extent !== 'parent'; +} diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index f7e177b9..9dd23a1c 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -4,11 +4,11 @@ import { clampPosition, getBoundsOfBoxes, getOverlappingArea, - isNumeric, rectToBox, nodeToRect, pointToRendererPoint, getViewportForBounds, + isCoordinateExtent, } from './general'; import { type Transform, @@ -19,10 +19,10 @@ import { type EdgeBase, type FitViewParamsBase, type FitViewOptionsBase, - NodeDragItem, CoordinateExtent, OnError, OnBeforeDeleteBase, + NodeLookup, } from '../types'; import { errorMessages } from '../constants'; @@ -258,69 +258,87 @@ export function fitView, Options exte return false; } -function clampNodeExtent(node: NodeDragItem | NodeBase, extent?: CoordinateExtent | 'parent') { +/** + * This function clamps the passed extend by the node's width and height. + * This is needed to prevent the node from being dragged outside of its extent. + * + * @param node + * @param extent + * @returns + */ +function clampNodeExtent( + node: NodeType, + extent?: CoordinateExtent | 'parent' +): CoordinateExtent | 'parent' | undefined { if (!extent || extent === 'parent') { return extent; } return [extent[0], [extent[1][0] - (node.computed?.width ?? 0), extent[1][1] - (node.computed?.height ?? 0)]]; } -export function calcNextPosition( - node: NodeDragItem | NodeType, - nextPosition: XYPosition, - nodes: NodeType[], - nodeExtent?: CoordinateExtent, - nodeOrigin: NodeOrigin = [0, 0], - onError?: OnError -): { position: XYPosition; positionAbsolute: XYPosition } { - const clampedNodeExtent = clampNodeExtent(node, node.extent || nodeExtent); - let currentExtent = clampedNodeExtent; - let parentNode: NodeType | null = null; - let parentPos = { x: 0, y: 0 }; - - if (node.parentNode) { - parentNode = nodes.find((n) => n.id === node.parentNode) || null; - parentPos = parentNode - ? getNodePositionWithOrigin(parentNode, parentNode.origin || nodeOrigin).positionAbsolute - : parentPos; - } +/** + * This function calculates the next position of a node, taking into account the node's extent, parent node, and origin. + * + * @internal + * @returns position, positionAbsolute + */ +export function calculateNodePosition({ + nodeId, + nextPosition, + nodeLookup, + nodeOrigin = [0, 0], + nodeExtent, + onError, +}: { + nodeId: string; + nextPosition: XYPosition; + nodeLookup: NodeLookup; + nodeOrigin?: NodeOrigin; + nodeExtent?: CoordinateExtent; + onError?: OnError; +}): { position: XYPosition; positionAbsolute: XYPosition } { + const node = nodeLookup.get(nodeId)!; + const parentNode = node.parentNode ? nodeLookup.get(node.parentNode) : undefined; + const { x: parentX, y: parentY } = parentNode + ? getNodePositionWithOrigin(parentNode, parentNode.origin || nodeOrigin).positionAbsolute + : { x: 0, y: 0 }; + let currentExtent = clampNodeExtent(node, node.extent || nodeExtent); if (node.extent === 'parent' && !node.expandParent) { - const nodeWidth = node.computed?.width; - const nodeHeight = node.computed?.height; - if (node.parentNode && nodeWidth && nodeHeight) { - const currNodeOrigin = node.origin || nodeOrigin; - - currentExtent = - parentNode && isNumeric(parentNode.computed?.width) && isNumeric(parentNode.computed?.height) - ? [ - [parentPos.x + nodeWidth * currNodeOrigin[0], parentPos.y + nodeHeight * currNodeOrigin[1]], - [ - parentPos.x + (parentNode.computed?.width ?? 0) - nodeWidth + nodeWidth * currNodeOrigin[0], - parentPos.y + (parentNode.computed?.height ?? 0) - nodeHeight + nodeHeight * currNodeOrigin[1], - ], - ] - : currentExtent; - } else { + if (!parentNode) { onError?.('005', errorMessages['error005']()); - currentExtent = clampedNodeExtent; + } else { + const nodeWidth = node.computed?.width; + const nodeHeight = node.computed?.height; + const parentWidth = parentNode?.computed?.width; + const parentHeight = parentNode?.computed?.height; + + if (nodeWidth && nodeHeight && parentWidth && parentHeight) { + const currNodeOrigin = node.origin || nodeOrigin; + const extentX = parentX + nodeWidth * currNodeOrigin[0]; + const extentY = parentY + nodeHeight * currNodeOrigin[1]; + + currentExtent = [ + [extentX, extentY], + [extentX + parentWidth - nodeWidth, extentY + parentHeight - nodeHeight], + ]; + } } - } else if (node.extent && node.parentNode && node.extent !== 'parent') { + } else if (parentNode && isCoordinateExtent(node.extent)) { currentExtent = [ - [node.extent[0][0] + parentPos.x, node.extent[0][1] + parentPos.y], - [node.extent[1][0] + parentPos.x, node.extent[1][1] + parentPos.y], + [node.extent[0][0] + parentX, node.extent[0][1] + parentY], + [node.extent[1][0] + parentX, node.extent[1][1] + parentY], ]; } - const positionAbsolute = - currentExtent && currentExtent !== 'parent' - ? clampPosition(nextPosition, currentExtent as CoordinateExtent) - : nextPosition; + const positionAbsolute = isCoordinateExtent(currentExtent) + ? clampPosition(nextPosition, currentExtent) + : nextPosition; return { position: { - x: positionAbsolute.x - parentPos.x, - y: positionAbsolute.y - parentPos.y, + x: positionAbsolute.x - parentX, + y: positionAbsolute.y - parentY, }, positionAbsolute, }; diff --git a/packages/system/src/xydrag/XYDrag.ts b/packages/system/src/xydrag/XYDrag.ts index 40d1aa4b..b2e8af9f 100644 --- a/packages/system/src/xydrag/XYDrag.ts +++ b/packages/system/src/xydrag/XYDrag.ts @@ -5,7 +5,7 @@ import { calcAutoPan, getEventPosition, getPointerPosition, - calcNextPosition, + calculateNodePosition, snapPosition, getNodesBounds, rectToBox, @@ -103,7 +103,6 @@ export function XYDrag voi function update({ noDragClassName, handleSelector, domNode, isSelectable, nodeId }: DragUpdateParams) { function updateNodes({ x, y }: XYPosition) { const { - nodes, nodeLookup, nodeExtent, snapGrid, @@ -149,13 +148,20 @@ export function XYDrag voi n.computed.positionAbsolute.y + (n.computed?.height ?? 0) - nodesBox.y2 + nodeExtent[1][1]; } - const updatedPos = calcNextPosition(n, nextPosition, nodes, adjustedNodeExtent, nodeOrigin, onError); + const { position, positionAbsolute } = calculateNodePosition({ + nodeId: n.id, + nextPosition, + nodeLookup, + nodeExtent: adjustedNodeExtent, + nodeOrigin, + onError, + }); // we want to make sure that we only fire a change event when there is a change - hasChange = hasChange || n.position.x !== updatedPos.position.x || n.position.y !== updatedPos.position.y; + hasChange = hasChange || n.position.x !== position.x || n.position.y !== position.y; - n.position = updatedPos.position; - n.computed.positionAbsolute = updatedPos.positionAbsolute; + n.position = position; + n.computed.positionAbsolute = positionAbsolute; return n; }); From 8983d64397a36809a9cb20d71d3e7a0e4b3f4f20 Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 24 Jan 2024 15:45:49 +0100 Subject: [PATCH 26/32] chore(react): use correct type for nodeRef --- packages/react/src/hooks/useDrag.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/src/hooks/useDrag.ts b/packages/react/src/hooks/useDrag.ts index f632d732..8363d393 100644 --- a/packages/react/src/hooks/useDrag.ts +++ b/packages/react/src/hooks/useDrag.ts @@ -5,7 +5,7 @@ import { handleNodeClick } from '../components/Nodes/utils'; import { useStoreApi } from './useStore'; type UseDragParams = { - nodeRef: RefObject; + nodeRef: RefObject; disabled?: boolean; noDragClassName?: string; handleSelector?: string; @@ -39,7 +39,7 @@ export function useDrag({ handleNodeClick({ id, store, - nodeRef: nodeRef as RefObject, + nodeRef, }); }, onDragStart: () => { From 80a1759fd20631adcf6fcf05119391f1f0a6d234 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 25 Jan 2024 00:25:31 +0100 Subject: [PATCH 27/32] chore(examples): cleanup usenodesdata --- examples/react/src/examples/UseNodesData/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/react/src/examples/UseNodesData/index.tsx b/examples/react/src/examples/UseNodesData/index.tsx index d6b697a9..d99ddb43 100644 --- a/examples/react/src/examples/UseNodesData/index.tsx +++ b/examples/react/src/examples/UseNodesData/index.tsx @@ -18,7 +18,7 @@ import UppercaseNode from './UppercaseNode'; export type TextNode = Node<{ text: string }, 'text'>; export type ResultNode = Node<{}, 'result'>; export type UppercaseNode = Node<{}, 'uppercase'>; -export type MyNode = Node<{ text: string }, 'text'> | Node<{}, 'result'> | Node<{}, 'uppercase'>; +export type MyNode = Node | TextNode | ResultNode | UppercaseNode; const nodeTypes = { text: TextNode, From b8fc637091bd4eea4f48a15a34cce61976be6b13 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 25 Jan 2024 11:42:03 +0100 Subject: [PATCH 28/32] refactor(svelte): use correct id for handle data-id attr --- packages/svelte/src/lib/components/Handle/Handle.svelte | 2 +- packages/system/src/xyhandle/XYHandle.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/lib/components/Handle/Handle.svelte b/packages/svelte/src/lib/components/Handle/Handle.svelte index 87b6c9d2..f00ccc44 100644 --- a/packages/svelte/src/lib/components/Handle/Handle.svelte +++ b/packages/svelte/src/lib/components/Handle/Handle.svelte @@ -134,7 +134,7 @@ The Handle component is the part of a node that can be used to connect nodes. data-handleid={handleId} data-nodeid={nodeId} data-handlepos={position} - data-id="{flowId}-{nodeId}-{id || null}-{type}" + data-id="{$flowId}-{nodeId}-{id || null}-{type}" class={cc([ 'svelte-flow__handle', `svelte-flow__handle-${position}`, diff --git a/packages/system/src/xyhandle/XYHandle.ts b/packages/system/src/xyhandle/XYHandle.ts index 239e6356..4ddb5313 100644 --- a/packages/system/src/xyhandle/XYHandle.ts +++ b/packages/system/src/xyhandle/XYHandle.ts @@ -260,9 +260,10 @@ function isValidHandle( }: IsValidParams ) { const isTarget = fromType === 'target'; - const handleDomNode = doc.querySelector( - `.${lib}-flow__handle[data-id="${flowId}-${handle?.nodeId}-${handle?.id}-${handle?.type}"]` - ); + const handleDomNode = handle + ? doc.querySelector(`.${lib}-flow__handle[data-id="${flowId}-${handle?.nodeId}-${handle?.id}-${handle?.type}"]`) + : null; + const { x, y } = getEventPosition(event); const handleBelow = doc.elementFromPoint(x, y); // we always want to prioritize the handle below the mouse cursor over the closest distance handle, From 7a1617d71d85586ca417862826cfd936ce924c73 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 25 Jan 2024 12:24:00 +0100 Subject: [PATCH 29/32] fix(react): apply defaultEdgeOptions marker closes #3826 --- examples/react/src/examples/Edges/index.tsx | 10 +++++++ packages/react/CHANGELOG.md | 3 +- .../EdgeRenderer/MarkerDefinitions.tsx | 29 +++++++++---------- packages/svelte/CHANGELOG.md | 3 +- packages/system/src/utils/marker.ts | 21 ++++++++++---- 5 files changed, 44 insertions(+), 22 deletions(-) diff --git a/examples/react/src/examples/Edges/index.tsx b/examples/react/src/examples/Edges/index.tsx index d6870621..bbba30fb 100644 --- a/examples/react/src/examples/Edges/index.tsx +++ b/examples/react/src/examples/Edges/index.tsx @@ -175,6 +175,15 @@ const edgeTypes: EdgeTypes = { custom2: CustomEdge2, }; +const defaultEdgeOptions = { + markerEnd: { + type: MarkerType.ArrowClosed, + color: 'red', + width: 20, + height: 20, + }, +}; + const EdgesFlow = () => { const [nodes, , onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); @@ -197,6 +206,7 @@ const EdgesFlow = () => { onEdgeMouseMove={onEdgeMouseMove} onEdgeMouseLeave={onEdgeMouseLeave} onDelete={console.log} + defaultEdgeOptions={defaultEdgeOptions} > diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index f47f7ef4..9b83293c 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -6,7 +6,8 @@ - selection box is not interrupted by selectionKey being let go - fix `OnNodeDrag` type -- refactor(handles): do not use fallback handle if an id is being used #3409 +- do not use fallback handle if a specific id is being used +- fix `defaultEdgeOptions` markers not being applied ## 12.0.0-next.7 diff --git a/packages/react/src/container/EdgeRenderer/MarkerDefinitions.tsx b/packages/react/src/container/EdgeRenderer/MarkerDefinitions.tsx index a4696e5b..5b6194c5 100644 --- a/packages/react/src/container/EdgeRenderer/MarkerDefinitions.tsx +++ b/packages/react/src/container/EdgeRenderer/MarkerDefinitions.tsx @@ -1,9 +1,8 @@ -import { memo, useCallback } from 'react'; +import { memo, useMemo } from 'react'; import { type MarkerProps, createMarkerIds } from '@xyflow/system'; import { useStore } from '../../hooks/useStore'; import { useMarkerSymbol } from './MarkerSymbols'; -import type { ReactFlowState } from '../../types'; type MarkerDefinitionsProps = { defaultColor: string; @@ -43,23 +42,23 @@ const Marker = ({ ); }; -const markerSelector = - ({ defaultColor, rfId }: { defaultColor: string; rfId?: string }) => - (s: ReactFlowState) => { - const markers = createMarkerIds(s.edges, { id: rfId, defaultColor }); - - return markers; - }; - -const markersEqual = (a: MarkerProps[], b: MarkerProps[]) => - // the id includes all marker options, so we just need to look at that part of the marker - !(a.length !== b.length || a.some((m, i) => m.id !== b[i].id)); - // when you have multiple flows on a page and you hide the first one, the other ones have no markers anymore // when they do have markers with the same ids. To prevent this the user can pass a unique id to the react flow wrapper // that we can then use for creating our unique marker ids const MarkerDefinitions = ({ defaultColor, rfId }: MarkerDefinitionsProps) => { - const markers = useStore(useCallback(markerSelector({ defaultColor, rfId }), [defaultColor, rfId]), markersEqual); + const edges = useStore((s) => s.edges); + const defaultEdgeOptions = useStore((s) => s.defaultEdgeOptions); + + const markers = useMemo(() => { + const markers = createMarkerIds(edges, { + id: rfId, + defaultColor, + defaultMarkerStart: defaultEdgeOptions?.markerStart, + defaultMarkerEnd: defaultEdgeOptions?.markerEnd, + }); + + return markers; + }, [edges, defaultEdgeOptions, rfId, defaultColor]); if (!markers.length) { return null; diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 6ca5483e..4bdc8461 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -11,7 +11,8 @@ - selection box is not interrupted by selectionKey being let go - Edge label has a default background and is clickable -- refactor(handles): do not use fallback handle if an id is being used #3409 +- do not use fallback handle if a specific id is being used +- use correct id for `` data-id attribute ## 0.0.34 diff --git a/packages/system/src/utils/marker.ts b/packages/system/src/utils/marker.ts index 0a191276..834f17ef 100644 --- a/packages/system/src/utils/marker.ts +++ b/packages/system/src/utils/marker.ts @@ -19,21 +19,32 @@ export function getMarkerId(marker: EdgeMarkerType | undefined, id?: string | nu export function createMarkerIds( edges: EdgeBase[], - { id, defaultColor }: { id?: string | null; defaultColor?: string } + { + id, + defaultColor, + defaultMarkerStart, + defaultMarkerEnd, + }: { + id?: string | null; + defaultColor?: string; + defaultMarkerStart?: EdgeMarkerType; + defaultMarkerEnd?: EdgeMarkerType; + } ) { - const ids: string[] = []; + const ids = new Set(); return edges .reduce((markers, edge) => { - [edge.markerStart, edge.markerEnd].forEach((marker) => { + [edge.markerStart || defaultMarkerStart, edge.markerEnd || defaultMarkerEnd].forEach((marker) => { if (marker && typeof marker === 'object') { const markerId = getMarkerId(marker, id); - if (!ids.includes(markerId)) { + if (!ids.has(markerId)) { markers.push({ id: markerId, color: marker.color || defaultColor, ...marker }); - ids.push(markerId); + ids.add(markerId); } } }); + return markers; }, []) .sort((a, b) => a.id.localeCompare(b.id)); From e352f3cdc493d4b991cfd8ce151bf749640b133f Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 25 Jan 2024 17:10:44 +0100 Subject: [PATCH 30/32] chore(ts): add annotations for useReactFlow/ useSvelteFlow --- .../examples/add-node-on-drop/Flow.svelte | 2 +- packages/react/src/hooks/useConnection.ts | 20 ++- .../react/src/hooks/useHandleConnections.ts | 2 +- .../react/src/hooks/useOnSelectionChange.ts | 2 +- packages/react/src/hooks/useViewportHelper.ts | 14 +- packages/react/src/types/general.ts | 78 ++++++++- packages/react/src/types/instance.ts | 93 ++++++++++ .../svelte/src/lib/hooks/useSvelteFlow.ts | 161 +++++++++++++++++- 8 files changed, 351 insertions(+), 21 deletions(-) diff --git a/examples/svelte/src/routes/examples/add-node-on-drop/Flow.svelte b/examples/svelte/src/routes/examples/add-node-on-drop/Flow.svelte index 809ae82c..e767e4ac 100644 --- a/examples/svelte/src/routes/examples/add-node-on-drop/Flow.svelte +++ b/examples/svelte/src/routes/examples/add-node-on-drop/Flow.svelte @@ -34,7 +34,7 @@ // See of connection landed inside the flow pane const targetIsPane = (event.target as HTMLDivElement)?.classList.contains('svelte-flow__pane'); - if (targetIsPane) { + if (targetIsPane && 'clientX' in event && 'clientY' in event) { const id = getId(); const position = { x: event.clientX, diff --git a/packages/react/src/hooks/useConnection.ts b/packages/react/src/hooks/useConnection.ts index 3544f31f..9b458e87 100644 --- a/packages/react/src/hooks/useConnection.ts +++ b/packages/react/src/hooks/useConnection.ts @@ -10,18 +10,24 @@ const selector = (s: ReactFlowStore) => ({ position: s.connectionStartHandle ? s.connectionPosition : null, }); +type UseConnectionResult = { + /** The start handle where the user interaction started or null */ + startHandle: ReactFlowStore['connectionStartHandle']; + /** The target handle that's inside the connection radius or null */ + endHandle: ReactFlowStore['connectionEndHandle']; + /** The current connection status 'valid', 'invalid' or null*/ + status: ReactFlowStore['connectionStatus']; + /** The current connection position or null */ + position: ReactFlowStore['connectionPosition'] | null; +}; + /** * Hook for accessing the ongoing connection. * * @public - * @returns ongoing connection: startHandle, endHandle, status, position + * @returns ongoing connection */ -export function useConnection(): { - startHandle: ReactFlowStore['connectionStartHandle']; - endHandle: ReactFlowStore['connectionEndHandle']; - status: ReactFlowStore['connectionStatus']; - position: ReactFlowStore['connectionPosition'] | null; -} { +export function useConnection(): UseConnectionResult { const ongoingConnection = useStore(selector, shallow); return ongoingConnection; diff --git a/packages/react/src/hooks/useHandleConnections.ts b/packages/react/src/hooks/useHandleConnections.ts index 7232cce4..4f62615e 100644 --- a/packages/react/src/hooks/useHandleConnections.ts +++ b/packages/react/src/hooks/useHandleConnections.ts @@ -13,7 +13,7 @@ type useHandleConnectionsParams = { }; /** - * Hook to check if a is connected to another and get the connections. + * Hook to check if a is connected to another and get the connections. * * @public * @param param.type - handle type 'source' or 'target' diff --git a/packages/react/src/hooks/useOnSelectionChange.ts b/packages/react/src/hooks/useOnSelectionChange.ts index 53c48891..8e08191b 100644 --- a/packages/react/src/hooks/useOnSelectionChange.ts +++ b/packages/react/src/hooks/useOnSelectionChange.ts @@ -11,7 +11,7 @@ export type UseOnSelectionChangeOptions = { * Hook for registering an onSelectionChange handler. * * @public - * @params params.onChange - The handler to register + * @param params.onChange - The handler to register */ export function useOnSelectionChange({ onChange }: UseOnSelectionChangeOptions) { const store = useStoreApi(); diff --git a/packages/react/src/hooks/useViewportHelper.ts b/packages/react/src/hooks/useViewportHelper.ts index 39320459..0e27d933 100644 --- a/packages/react/src/hooks/useViewportHelper.ts +++ b/packages/react/src/hooks/useViewportHelper.ts @@ -86,31 +86,31 @@ const useViewportHelper = (): ViewportHelperFunctions => { panZoom?.setViewport(viewport, { duration: options?.duration }); }, - screenToFlowPosition: (position: XYPosition, options: { snapToGrid: boolean } = { snapToGrid: true }) => { + screenToFlowPosition: (clientPosition: XYPosition, options: { snapToGrid: boolean } = { snapToGrid: true }) => { const { transform, snapGrid, domNode } = store.getState(); if (!domNode) { - return position; + return clientPosition; } const { x: domX, y: domY } = domNode.getBoundingClientRect(); const correctedPosition = { - x: position.x - domX, - y: position.y - domY, + x: clientPosition.x - domX, + y: clientPosition.y - domY, }; return pointToRendererPoint(correctedPosition, transform, options.snapToGrid, snapGrid); }, - flowToScreenPosition: (position: XYPosition) => { + flowToScreenPosition: (flowPosition: XYPosition) => { const { transform, domNode } = store.getState(); if (!domNode) { - return position; + return flowPosition; } const { x: domX, y: domY } = domNode.getBoundingClientRect(); - const rendererPosition = rendererPointToPoint(position, transform); + const rendererPosition = rendererPointToPoint(flowPosition, transform); return { x: rendererPosition.x + domX, diff --git a/packages/react/src/types/general.ts b/packages/react/src/types/general.ts index 53d95d68..4f3f127a 100644 --- a/packages/react/src/types/general.ts +++ b/packages/react/src/types/general.ts @@ -47,17 +47,91 @@ export type OnInit = ) => void; export type ViewportHelperFunctions = { + /** + * Zooms viewport in by 1.2. + * + * @param options.duration - optional duration. If set, a transition will be applied + */ zoomIn: ZoomInOut; + /** + * Zooms viewport out by 1 / 1.2. + * + * @param options.duration - optional duration. If set, a transition will be applied + */ zoomOut: ZoomInOut; + /** + * Sets the current zoom level. + * + * @param zoomLevel - the zoom level to set + * @param options.duration - optional duration. If set, a transition will be applied + */ zoomTo: ZoomTo; + /** + * Returns the current zoom level. + * + * @returns current zoom as a number + */ getZoom: GetZoom; + /** + * Sets the current viewport. + * + * @param viewport - the viewport to set + * @param options.duration - optional duration. If set, a transition will be applied + */ setViewport: SetViewport; + /** + * Returns the current viewport. + * + * @returns Viewport + */ getViewport: GetViewport; + /** + * Fits the view. + * + * @param options.padding - optional padding + * @param options.includeHiddenNodes - optional includeHiddenNodes + * @param options.minZoom - optional minZoom + * @param options.maxZoom - optional maxZoom + * @param options.duration - optional duration. If set, a transition will be applied + * @param options.nodes - optional nodes to fit the view to + */ fitView: FitView; + /** + * Sets the center of the view to the given position. + * + * @param x - x position + * @param y - y position + * @param options.zoom - optional zoom + */ setCenter: SetCenter; + /** + * Fits the view to the given bounds . + * + * @param bounds - the bounds ({ x: number, y: number, width: number, height: number }) to fit the view to + * @param options.padding - optional padding + */ fitBounds: FitBounds; - screenToFlowPosition: (position: XYPosition, options?: { snapToGrid: boolean }) => XYPosition; - flowToScreenPosition: (position: XYPosition) => XYPosition; + /** + * Converts a screen / client position to a flow position. + * + * @param clientPosition - the screen / client position. When you are working with events you can use event.clientX and event.clientY + * @param options.snapToGrid - if true, the converted position will be snapped to the grid + * @returns position as { x: number, y: number } + * + * @example + * const flowPosition = screenToFlowPosition({ x: event.clientX, y: event.clientY }) + */ + screenToFlowPosition: (clientPosition: XYPosition, options?: { snapToGrid: boolean }) => XYPosition; + /** + * Converts a flow position to a screen / client position. + * + * @param flowPosition - the screen / client position. When you are working with events you can use event.clientX and event.clientY + * @returns position as { x: number, y: number } + * + * @example + * const clientPosition = flowToScreenPosition({ x: node.position.x, y: node.position.y }) + */ + flowToScreenPosition: (flowPosition: XYPosition) => XYPosition; viewportInitialized: boolean; }; diff --git a/packages/react/src/types/instance.ts b/packages/react/src/types/instance.ts index 6b37fd14..d34c23dc 100644 --- a/packages/react/src/types/instance.ts +++ b/packages/react/src/types/instance.ts @@ -59,19 +59,112 @@ export namespace Instance { } export type ReactFlowInstance = { + /** + * Returns nodes. + * + * @returns nodes array + */ getNodes: Instance.GetNodes; + /** + * Sets nodes. + * + * @param payload - the nodes to set or a function that receives the current nodes and returns the new nodes + */ setNodes: Instance.SetNodes; + /** + * Adds nodes. + * + * @param payload - the nodes to add + */ addNodes: Instance.AddNodes; + /** + * Returns a node by id. + * + * @param id - the node id + * @returns the node or undefined if no node was found + */ getNode: Instance.GetNode; + /** + * Returns edges. + * + * @returns edges array + */ getEdges: Instance.GetEdges; + /** + * Sets edges. + * + * @param payload - the edges to set or a function that receives the current edges and returns the new edges + */ setEdges: Instance.SetEdges; + /** + * Adds edges. + * + * @param payload - the edges to add + */ addEdges: Instance.AddEdges; + /** + * Returns an edge by id. + * + * @param id - the edge id + * @returns the edge or undefined if no edge was found + */ getEdge: Instance.GetEdge; + /** + * Returns the nodes, edges and the viewport as a JSON object. + * + * @returns the nodes, edges and the viewport as a JSON object + */ toObject: Instance.ToObject; + /** + * Deletes nodes and edges. + * + * @param params.nodes - optional nodes array to delete + * @param params.edges - optional edges array to delete + * + * @returns a promise that resolves with the deleted nodes and edges + */ deleteElements: Instance.DeleteElements; + /** + * Returns all nodes that intersect with the given node or rect. + * + * @param node - the node or rect to check for intersections + * @param partially - if true, the node is considered to be intersecting if it partially overlaps with the passed node or rect + * @param nodes - optional nodes array to check for intersections + * + * @returns an array of intersecting nodes + */ getIntersectingNodes: Instance.GetIntersectingNodes; + /** + * Checks if the given node or rect intersects with the passed rect. + * + * @param node - the node or rect to check for intersections + * @param area - the rect to check for intersections + * @param partially - if true, the node is considered to be intersecting if it partially overlaps with the passed react + * + * @returns true if the node or rect intersects with the given area + */ isNodeIntersecting: Instance.IsNodeIntersecting; + /** + * Updates a node. + * + * @param id - id of the node to update + * @param nodeUpdate - the node update as an object or a function that receives the current node and returns the node update + * @param options.replace - if true, the node is replaced with the node update, otherwise the changes get merged + * + * @example + * updateNode('node-1', (node) => ({ position: { x: node.position.x + 10, y: node.position.y } })); + */ updateNode: Instance.UpdateNode; + /** + * Updates the data attribute of a node. + * + * @param id - id of the node to update + * @param dataUpdate - the data update as an object or a function that receives the current data and returns the data update + * @param options.replace - if true, the data is replaced with the data update, otherwise the changes get merged + * + * @example + * updateNodeData('node-1', { label: 'A new label' }); + */ updateNodeData: Instance.UpdateNodeData; viewportInitialized: boolean; } & Omit; diff --git a/packages/svelte/src/lib/hooks/useSvelteFlow.ts b/packages/svelte/src/lib/hooks/useSvelteFlow.ts index b6bb941e..c035a52a 100644 --- a/packages/svelte/src/lib/hooks/useSvelteFlow.ts +++ b/packages/svelte/src/lib/hooks/useSvelteFlow.ts @@ -24,32 +24,136 @@ import { isNode } from '$lib/utils'; * Hook for accessing the ReactFlow instance. * * @public + * * @returns helper functions */ export function useSvelteFlow(): { + /** + * Zooms viewport in by 1.2. + * + * @param options.duration - optional duration. If set, a transition will be applied + */ zoomIn: ZoomInOut; + /** + * Zooms viewport out by 1 / 1.2. + * + * @param options.duration - optional duration. If set, a transition will be applied + */ zoomOut: ZoomInOut; + /** + * Returns a node by id. + * + * @param id - the node id + * @returns the node or undefined if no node was found + */ getNode: (id: string) => Node | undefined; + /** + * Returns nodes. + * + * @returns nodes array + */ getNodes: (ids?: string[]) => Node[]; + /** + * Returns an edge by id. + * + * @param id - the edge id + * @returns the edge or undefined if no edge was found + */ getEdge: (id: string) => Edge | undefined; + /** + * Returns edges. + * + * @returns edges array + */ getEdges: (ids?: string[]) => Edge[]; + /** + * Sets the current zoom level. + * + * @param zoomLevel - the zoom level to set + * @param options.duration - optional duration. If set, a transition will be applied + */ setZoom: (zoomLevel: number, options?: ViewportHelperFunctionOptions) => void; + /** + * Returns the current zoom level. + * + * @returns current zoom as a number + */ getZoom: () => number; + /** + * Sets the center of the view to the given position. + * + * @param x - x position + * @param y - y position + * @param options.zoom - optional zoom + */ setCenter: (x: number, y: number, options?: SetCenterOptions) => void; + /** + * Sets the current viewport. + * + * @param viewport - the viewport to set + * @param options.duration - optional duration. If set, a transition will be applied + */ setViewport: (viewport: Viewport, options?: ViewportHelperFunctionOptions) => void; + /** + * Returns the current viewport. + * + * @returns Viewport + */ getViewport: () => Viewport; + /** + * Fits the view. + * + * @param options.padding - optional padding + * @param options.includeHiddenNodes - optional includeHiddenNodes + * @param options.minZoom - optional minZoom + * @param options.maxZoom - optional maxZoom + * @param options.duration - optional duration. If set, a transition will be applied + * @param options.nodes - optional nodes to fit the view to + */ fitView: (options?: FitViewOptions) => void; + /** + * Returns all nodes that intersect with the given node or rect. + * + * @param node - the node or rect to check for intersections + * @param partially - if true, the node is considered to be intersecting if it partially overlaps with the passed node or rect + * @param nodes - optional nodes array to check for intersections + * + * @returns an array of intersecting nodes + */ getIntersectingNodes: ( nodeOrRect: Node | { id: Node['id'] } | Rect, partially?: boolean, nodesToIntersect?: Node[] ) => Node[]; + /** + * Checks if the given node or rect intersects with the passed rect. + * + * @param node - the node or rect to check for intersections + * @param area - the rect to check for intersections + * @param partially - if true, the node is considered to be intersecting if it partially overlaps with the passed react + * + * @returns true if the node or rect intersects with the given area + */ isNodeIntersecting: ( nodeOrRect: Node | { id: Node['id'] } | Rect, area: Rect, partially?: boolean ) => boolean; + /** + * Fits the view to the given bounds . + * + * @param bounds - the bounds ({ x: number, y: number, width: number, height: number }) to fit the view to + * @param options.padding - optional padding + */ fitBounds: (bounds: Rect, options?: FitBoundsOptions) => void; + /** + * Deletes nodes and edges. + * + * @param params.nodes - optional nodes array to delete + * @param params.edges - optional edges array to delete + * + * @returns a promise that resolves with the deleted nodes and edges + */ deleteElements: ({ nodes, edges @@ -57,19 +161,66 @@ export function useSvelteFlow(): { nodes?: (Node | { id: Node['id'] })[]; edges?: (Edge | { id: Edge['id'] })[]; }) => Promise<{ deletedNodes: Node[]; deletedEdges: Edge[] }>; - screenToFlowPosition: (position: XYPosition, options?: { snapToGrid: boolean }) => XYPosition; - flowToScreenPosition: (position: XYPosition) => XYPosition; + /** + * Converts a screen / client position to a flow position. + * + * @param clientPosition - the screen / client position. When you are working with events you can use event.clientX and event.clientY + * @param options.snapToGrid - if true, the converted position will be snapped to the grid + * @returns position as { x: number, y: number } + * + * @example + * const flowPosition = screenToFlowPosition({ x: event.clientX, y: event.clientY }) + */ + screenToFlowPosition: ( + clientPosition: XYPosition, + options?: { snapToGrid: boolean } + ) => XYPosition; + /** + * Converts a flow position to a screen / client position. + * + * @param flowPosition - the screen / client position. When you are working with events you can use event.clientX and event.clientY + * @returns position as { x: number, y: number } + * + * @example + * const clientPosition = flowToScreenPosition({ x: node.position.x, y: node.position.y }) + */ + flowToScreenPosition: (flowPosition: XYPosition) => XYPosition; viewport: Writable; + /** + * Updates a node. + * + * @param id - id of the node to update + * @param nodeUpdate - the node update as an object or a function that receives the current node and returns the node update + * @param options.replace - if true, the node is replaced with the node update, otherwise the changes get merged + * + * @example + * updateNode('node-1', (node) => ({ position: { x: node.position.x + 10, y: node.position.y } })); + */ updateNode: ( id: string, nodeUpdate: Partial | ((node: Node) => Partial), options?: { replace: boolean } ) => void; + /** + * Updates the data attribute of a node. + * + * @param id - id of the node to update + * @param dataUpdate - the data update as an object or a function that receives the current data and returns the data update + * @param options.replace - if true, the data is replaced with the data update, otherwise the changes get merged + * + * @example + * updateNodeData('node-1', { label: 'A new label' }); + */ updateNodeData: ( id: string, dataUpdate: object | ((node: Node) => object), options?: { replace: boolean } ) => void; + /** + * Returns the nodes, edges and the viewport as a JSON object. + * + * @returns the nodes, edges and the viewport as a JSON object + */ toObject: () => { nodes: Node[]; edges: Edge[]; viewport: Viewport }; } { const { @@ -263,6 +414,11 @@ export function useSvelteFlow(): { _snapGrid || [1, 1] ); }, + /** + * + * @param position + * @returns + */ flowToScreenPosition: (position: XYPosition) => { const _domNode = get(domNode); @@ -279,6 +435,7 @@ export function useSvelteFlow(): { y: rendererPosition.y + domY }; }, + toObject: () => { return { nodes: get(nodes).map((node) => ({ From 19bcfe28a23a7e761c05b804eba0ba45cefb6f93 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 25 Jan 2024 17:46:46 +0100 Subject: [PATCH 31/32] fix(getNodesBounds): use absolute position by default, add options param --- packages/react/CHANGELOG.md | 1 + .../additional-components/MiniMap/MiniMap.tsx | 3 +- .../NodeToolbar/NodeToolbar.tsx | 2 +- .../src/components/NodesSelection/index.tsx | 2 +- packages/react/src/store/initialState.ts | 3 +- packages/svelte/CHANGELOG.md | 1 + .../plugins/NodeToolbar/NodeToolbar.svelte | 2 +- .../svelte/src/lib/store/initial-store.ts | 3 +- packages/system/src/utils/graph.ts | 30 ++++++++++++------- packages/system/src/utils/store.ts | 2 +- packages/system/src/xydrag/XYDrag.ts | 2 +- 11 files changed, 33 insertions(+), 18 deletions(-) diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 9b83293c..8b366ce1 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -8,6 +8,7 @@ - fix `OnNodeDrag` type - do not use fallback handle if a specific id is being used - fix `defaultEdgeOptions` markers not being applied +- fix `getNodesBounds` and add second param for passing options ## 12.0.0-next.7 diff --git a/packages/react/src/additional-components/MiniMap/MiniMap.tsx b/packages/react/src/additional-components/MiniMap/MiniMap.tsx index 34a66fa1..cd3f64ae 100644 --- a/packages/react/src/additional-components/MiniMap/MiniMap.tsx +++ b/packages/react/src/additional-components/MiniMap/MiniMap.tsx @@ -25,7 +25,8 @@ const selector = (s: ReactFlowState) => { return { viewBB, - boundingRect: s.nodes.length > 0 ? getBoundsOfRects(getNodesBounds(s.nodes, s.nodeOrigin), viewBB) : viewBB, + boundingRect: + s.nodes.length > 0 ? getBoundsOfRects(getNodesBounds(s.nodes, { nodeOrigin: s.nodeOrigin }), viewBB) : viewBB, rfId: s.rfId, nodeOrigin: s.nodeOrigin, panZoom: s.panZoom, diff --git a/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx b/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx index a681d6f3..0ecc8185 100644 --- a/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx +++ b/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx @@ -73,7 +73,7 @@ export function NodeToolbar({ return null; } - const nodeRect: Rect = getNodesBounds(nodes, nodeOrigin); + const nodeRect: Rect = getNodesBounds(nodes, { nodeOrigin }); const zIndex: number = Math.max(...nodes.map((node) => (node[internalsSymbol]?.z || 1) + 1)); const wrapperStyle: CSSProperties = { diff --git a/packages/react/src/components/NodesSelection/index.tsx b/packages/react/src/components/NodesSelection/index.tsx index f8aeb77c..d3ed7a06 100644 --- a/packages/react/src/components/NodesSelection/index.tsx +++ b/packages/react/src/components/NodesSelection/index.tsx @@ -22,7 +22,7 @@ export type NodesSelectionProps = { const selector = (s: ReactFlowState) => { const selectedNodes = s.nodes.filter((n) => n.selected); - const { width, height, x, y } = getNodesBounds(selectedNodes, s.nodeOrigin); + const { width, height, x, y } = getNodesBounds(selectedNodes, { nodeOrigin: s.nodeOrigin }); return { width, diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index 109899d6..aace0bb4 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -38,7 +38,8 @@ const getInitialState = ({ if (fitView && width && height) { const nodesWithDimensions = nextNodes.filter((node) => node.width && node.height); - const bounds = getNodesBounds(nodesWithDimensions, [0, 0]); + // @todo users nodeOrigin should be used here + const bounds = getNodesBounds(nodesWithDimensions, { nodeOrigin: [0, 0] }); const { x, y, zoom } = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1); transform = [x, y, zoom]; } diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 4bdc8461..b534df65 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -13,6 +13,7 @@ - Edge label has a default background and is clickable - do not use fallback handle if a specific id is being used - use correct id for `` data-id attribute +- fix `getNodesBounds` and add second param for passing options ## 0.0.34 diff --git a/packages/svelte/src/lib/plugins/NodeToolbar/NodeToolbar.svelte b/packages/svelte/src/lib/plugins/NodeToolbar/NodeToolbar.svelte index fab6ce2d..ca6dba1f 100644 --- a/packages/svelte/src/lib/plugins/NodeToolbar/NodeToolbar.svelte +++ b/packages/svelte/src/lib/plugins/NodeToolbar/NodeToolbar.svelte @@ -58,7 +58,7 @@ height: toolbarNode.computed?.height ?? toolbarNode.height ?? 0 }; } else if (toolbarNodes.length > 1) { - nodeRect = getNodesBounds(toolbarNodes, $nodeOrigin); + nodeRect = getNodesBounds(toolbarNodes, { nodeOrigin: $nodeOrigin }); } if (nodeRect) { diff --git a/packages/svelte/src/lib/store/initial-store.ts b/packages/svelte/src/lib/store/initial-store.ts index fedaa364..6c18c877 100644 --- a/packages/svelte/src/lib/store/initial-store.ts +++ b/packages/svelte/src/lib/store/initial-store.ts @@ -92,7 +92,8 @@ export const getInitialStore = ({ if (fitView && width && height) { const nodesWithDimensions = nextNodes.filter((node) => node.width && node.height); - const bounds = getNodesBounds(nodesWithDimensions, [0, 0]); + // @todo users nodeOrigin should be used here + const bounds = getNodesBounds(nodesWithDimensions, { nodeOrigin: [0, 0] }); viewport = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1); } diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index 9dd23a1c..6407d1a1 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -102,11 +102,13 @@ export const getIncomers = { +): { position: XYPosition; positionAbsolute: XYPosition } => { if (!node) { return { - x: 0, - y: 0, + position: { + x: 0, + y: 0, + }, positionAbsolute: { x: 0, y: 0, @@ -123,7 +125,7 @@ export const getNodePositionWithOrigin = ( }; return { - ...position, + position, positionAbsolute: node.computed?.positionAbsolute ? { x: node.computed.positionAbsolute.x - offsetX, @@ -133,27 +135,35 @@ export const getNodePositionWithOrigin = ( }; }; +export type GetNodesBoundsParams = { + nodeOrigin?: NodeOrigin; + useRelativePosition?: boolean; +}; + /** * Determines a bounding box that contains all given nodes in an array * @public * @remarks Useful when combined with {@link getViewportForBounds} to calculate the correct transform to fit the given nodes in a viewport. * @param nodes - Nodes to calculate the bounds for - * @param nodeOrigin - Origin of the nodes: [0, 0] - top left, [0.5, 0.5] - center + * @param params.nodeOrigin - Origin of the nodes: [0, 0] - top left, [0.5, 0.5] - center + * @param params.useRelativePosition - Whether to use the relative or absolute node positions * @returns Bounding box enclosing all nodes */ -export const getNodesBounds = (nodes: NodeBase[], nodeOrigin: NodeOrigin = [0, 0]): Rect => { +export const getNodesBounds = ( + nodes: NodeBase[], + params: GetNodesBoundsParams = { nodeOrigin: [0, 0], useRelativePosition: false } +): Rect => { if (nodes.length === 0) { return { x: 0, y: 0, width: 0, height: 0 }; } const box = nodes.reduce( (currBox, node) => { - const { x, y } = getNodePositionWithOrigin(node, node.origin || nodeOrigin); + const nodePos = getNodePositionWithOrigin(node, node.origin || params.nodeOrigin); return getBoundsOfBoxes( currBox, rectToBox({ - x, - y, + ...nodePos[params.useRelativePosition ? 'position' : 'positionAbsolute'], width: node.computed?.width ?? node.width ?? 0, height: node.computed?.height ?? node.height ?? 0, }) @@ -239,7 +249,7 @@ export function fitView, Options exte }); if (filteredNodes.length > 0) { - const bounds = getNodesBounds(filteredNodes, nodeOrigin); + const bounds = getNodesBounds(filteredNodes, { nodeOrigin }); const viewport = getViewportForBounds( bounds, diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index 8efb84c2..51cb1329 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -136,7 +136,7 @@ function calculateXYZPosition( } const parentNode = nodeLookup.get(node.parentNode)!; - const parentNodePosition = getNodePositionWithOrigin(parentNode, parentNode?.origin || nodeOrigin); + const { position: parentNodePosition } = getNodePositionWithOrigin(parentNode, parentNode?.origin || nodeOrigin); return calculateXYZPosition( parentNode, diff --git a/packages/system/src/xydrag/XYDrag.ts b/packages/system/src/xydrag/XYDrag.ts index b2e8af9f..ffd19f37 100644 --- a/packages/system/src/xydrag/XYDrag.ts +++ b/packages/system/src/xydrag/XYDrag.ts @@ -120,7 +120,7 @@ export function XYDrag voi let nodesBox: Box = { x: 0, y: 0, x2: 0, y2: 0 }; if (dragItems.length > 1 && nodeExtent) { - const rect = getNodesBounds(dragItems as unknown as NodeBase[], nodeOrigin); + const rect = getNodesBounds(dragItems as unknown as NodeBase[], { nodeOrigin }); nodesBox = rectToBox(rect); } From a027b26f1140728cdaa6a0430d6ed5d2b2525aab Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 25 Jan 2024 21:13:06 +0100 Subject: [PATCH 32/32] fix(react): expandParent option for child nodes --- packages/react/CHANGELOG.md | 1 + packages/react/src/utils/changes.ts | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 8b366ce1..6931ea5b 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -9,6 +9,7 @@ - do not use fallback handle if a specific id is being used - fix `defaultEdgeOptions` markers not being applied - fix `getNodesBounds` and add second param for passing options +- fix `expandParent` for child nodes ## 12.0.0-next.7 diff --git a/packages/react/src/utils/changes.ts b/packages/react/src/utils/changes.ts index 6bcddabd..310b784c 100644 --- a/packages/react/src/utils/changes.ts +++ b/packages/react/src/utils/changes.ts @@ -6,10 +6,7 @@ export function handleParentExpand(updatedElements: any[], updateItem: any) { for (const [index, item] of updatedElements.entries()) { if (item.id === updateItem.parentNode) { const parent = { ...item }; - - if (!parent.computed) { - 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; @@ -106,7 +103,7 @@ function applyChanges(changes: any[], elements: any[]): any[] { const updatedElement = { ...element }; for (const change of changes) { - applyChange(change, updatedElement, elements); + applyChange(change, updatedElement, updatedElements); } updatedElements.push(updatedElement);