diff --git a/examples/react/src/examples/UseNodesData/TextNode.tsx b/examples/react/src/examples/UseNodesData/TextNode.tsx index e787d824..6ca424e6 100644 --- a/examples/react/src/examples/UseNodesData/TextNode.tsx +++ b/examples/react/src/examples/UseNodesData/TextNode.tsx @@ -1,10 +1,10 @@ import { memo, ChangeEventHandler } from 'react'; -import { Position, NodeProps, useSetNodeData, Handle } from '@xyflow/react'; +import { Position, NodeProps, Handle, useReactFlow } from '@xyflow/react'; function TextNode({ id, data }: NodeProps) { - const setNodeData = useSetNodeData(); + const { updateNodeData } = useReactFlow(); - const onChange: ChangeEventHandler = (evt) => setNodeData(id, { text: evt.target.value }); + const onChange: ChangeEventHandler = (evt) => updateNodeData(id, { text: evt.target.value }); return (
diff --git a/examples/react/src/examples/UseNodesData/UppercaseNode.tsx b/examples/react/src/examples/UseNodesData/UppercaseNode.tsx index 47ecd87c..699dc37e 100644 --- a/examples/react/src/examples/UseNodesData/UppercaseNode.tsx +++ b/examples/react/src/examples/UseNodesData/UppercaseNode.tsx @@ -1,15 +1,15 @@ import { memo, useEffect } from 'react'; -import { Position, NodeProps, useSetNodeData, Handle, useHandleConnections, useNodesData } from '@xyflow/react'; +import { Position, NodeProps, useReactFlow, Handle, useHandleConnections, useNodesData } from '@xyflow/react'; function UppercaseNode({ id }: NodeProps) { + const { updateNodeData } = useReactFlow(); const connections = useHandleConnections({ type: 'target', }); const nodeData = useNodesData(connections[0]?.source); - const setNodeData = useSetNodeData(); useEffect(() => { - setNodeData(id, { text: nodeData?.text.toUpperCase() }); + updateNodeData(id, { text: nodeData?.text.toUpperCase() }); }, [nodeData]); return ( diff --git a/examples/svelte/src/routes/examples/usenodesdata/TextNode.svelte b/examples/svelte/src/routes/examples/usenodesdata/TextNode.svelte index 63f2d83b..c1642da2 100644 --- a/examples/svelte/src/routes/examples/usenodesdata/TextNode.svelte +++ b/examples/svelte/src/routes/examples/usenodesdata/TextNode.svelte @@ -1,12 +1,12 @@
@@ -14,7 +14,7 @@
setNodeData(id, { text: evt.currentTarget.value })} + on:input={(evt) => updateNodeData(id, { text: evt.currentTarget.value })} />
diff --git a/examples/svelte/src/routes/examples/usenodesdata/UppercaseNode.svelte b/examples/svelte/src/routes/examples/usenodesdata/UppercaseNode.svelte index b23481d8..88c8a918 100644 --- a/examples/svelte/src/routes/examples/usenodesdata/UppercaseNode.svelte +++ b/examples/svelte/src/routes/examples/usenodesdata/UppercaseNode.svelte @@ -5,14 +5,14 @@ type NodeProps, useHandleConnections, useNodesData, - useSetNodeData + useSvelteFlow } from '@xyflow/svelte'; type $$Props = NodeProps; export let id: $$Props['id']; - const setNodeData = useSetNodeData(); + const { updateNodeData } = useSvelteFlow(); const connections = useHandleConnections({ nodeId: id, type: 'target' @@ -21,7 +21,7 @@ $: nodeData = useNodesData($connections[0]?.source); $: { - setNodeData(id, { text: $nodeData?.text?.toUpperCase() || '' }); + updateNodeData(id, { text: $nodeData?.text?.toUpperCase() || '' }); } diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index 44e29c14..9bb17eb9 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -10,7 +10,7 @@ import { } from '@xyflow/system'; import useViewportHelper from './useViewportHelper'; -import { useStoreApi } from '../hooks/useStore'; +import { useStoreApi } from './useStore'; import type { ReactFlowInstance, Instance, @@ -24,6 +24,7 @@ import type { Node, Edge, } from '../types'; +import { isNode } from '../utils'; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ export default function useReactFlow(): ReactFlowInstance { @@ -271,6 +272,36 @@ export default function useReactFlow(): ReactFlo return getOutgoersBase(node, nodes, edges); }, []); + const updateNode = useCallback( + (id, nodeUpdate, options = { replace: true }) => { + setNodes((prevNodes) => + prevNodes.map((node) => { + if (node.id === id) { + const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node as Node) : nodeUpdate; + return options.replace && isNode(nextNode) ? nextNode : { ...node, ...nextNode }; + } + + return node; + }) + ); + }, + [setNodes] + ); + + const updateNodeData = useCallback( + (id, dataUpdate, options = { replace: false }) => { + updateNode( + id, + (node) => { + const nextData = typeof dataUpdate === 'function' ? dataUpdate(node) : dataUpdate; + return options.replace ? { ...node, data: nextData } : { ...node, data: { ...node.data, ...nextData } }; + }, + options + ); + }, + [updateNode] + ); + return useMemo(() => { return { ...viewportHelper, @@ -289,6 +320,8 @@ export default function useReactFlow(): ReactFlo getConnectedEdges, getIncomers, getOutgoers, + updateNode, + updateNodeData, }; }, [ viewportHelper, diff --git a/packages/react/src/hooks/useSetNodeData.ts b/packages/react/src/hooks/useSetNodeData.ts deleted file mode 100644 index 16d33f66..00000000 --- a/packages/react/src/hooks/useSetNodeData.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useCallback } from 'react'; - -import useReactFlow from './useReactFlow'; -import { Node } from '../types'; - -export function useSetNodeData() { - const { setNodes } = useReactFlow(); - - const setNodeData = useCallback( - function setNodesData( - id: string, - dataUpdate: object | ((node: NodeType) => object), - options: { replace: boolean } = { replace: true } - ) { - setNodes((prevNodes) => - prevNodes.map((node) => { - if (node.id === id) { - const nextData = typeof dataUpdate === 'function' ? dataUpdate(node as NodeType) : dataUpdate; - return options.replace ? { ...node, data: nextData } : { ...node, data: { ...node.data, ...nextData } }; - } - - return node; - }) - ); - }, - [setNodes] - ); - - return setNodeData; -} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index ab450904..ae82e78b 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -23,7 +23,6 @@ export { default as useOnViewportChange, type UseOnViewportChangeOptions } from export { default as useOnSelectionChange, type UseOnSelectionChangeOptions } from './hooks/useOnSelectionChange'; export { default as useNodesInitialized, type UseNodesInitializedOptions } from './hooks/useNodesInitialized'; export { useHandleConnections } from './hooks/useHandleConnections'; -export { useSetNodeData } from './hooks/useSetNodeData'; export { useNodesData } from './hooks/useNodesData'; export { useNodeId } from './contexts/NodeIdContext'; diff --git a/packages/react/src/types/instance.ts b/packages/react/src/types/instance.ts index f229ec87..a1c822bb 100644 --- a/packages/react/src/types/instance.ts +++ b/packages/react/src/types/instance.ts @@ -45,6 +45,17 @@ export namespace Instance { export type getConnectedEdges = (id: string | (Node | { id: Node['id'] })[]) => Edge[]; export type getIncomers = (node: string | Node | { id: Node['id'] }) => Node[]; export type getOutgoers = (node: string | Node | { id: Node['id'] }) => Node[]; + + export type UpdateNode = ( + id: string, + dataUpdate: Partial | ((node: Node) => Partial), + options?: { replace: boolean } + ) => void; + export type UpdateNodeData = ( + id: string, + dataUpdate: object | ((node: Node) => object), + options?: { replace: boolean } + ) => void; } export type ReactFlowInstance = { @@ -60,5 +71,7 @@ export type ReactFlowInstance = { deleteElements: Instance.DeleteElements; getIntersectingNodes: Instance.GetIntersectingNodes; isNodeIntersecting: Instance.IsNodeIntersecting; + updateNode: Instance.UpdateNode; + updateNodeData: Instance.UpdateNodeData; viewportInitialized: boolean; } & Omit; diff --git a/packages/react/src/utils/general.ts b/packages/react/src/utils/general.ts index e13247c1..a106e182 100644 --- a/packages/react/src/utils/general.ts +++ b/packages/react/src/utils/general.ts @@ -10,8 +10,8 @@ import { import type { Edge, Node } from '../types'; -export const isNode = isNodeBase; -export const isEdge = isEdgeBase; +export const isNode = isNodeBase; +export const isEdge = isEdgeBase; export const getOutgoers = getOutgoersBase; export const getIncomers = getIncomersBase; export const addEdge = addEdgeBase; diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 5ed9f8e9..60f4b8ed 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -41,7 +41,7 @@ "access": "public" }, "dependencies": { - "@svelte-put/shortcut": "^3.0.0", + "@svelte-put/shortcut": "^3.1.0", "@xyflow/system": "workspace:*", "classcat": "^5.0.4" }, diff --git a/packages/svelte/src/lib/actions/shortcut/index.ts b/packages/svelte/src/lib/actions/shortcut/index.ts deleted file mode 100644 index 10c1e207..00000000 --- a/packages/svelte/src/lib/actions/shortcut/index.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Shortcut action is copied and slightly adjusted from https://github.com/vnphanquang/svelte-put/tree/main/packages/actions/shortcut -// If this discussion https://github.com/vnphanquang/svelte-put/discussions/256 gets resolved, we will use the original action again. - -export interface ShortcutEventDetail { - event: KeyboardEvent; - trigger: ShortcutTrigger; -} - -export type ShortcutModifier = 'alt' | 'ctrl' | 'meta' | 'shift'; - -export type ShortcutModifierDefinition = - | ShortcutModifier - | ShortcutModifier[] - | ShortcutModifier[][]; - -export type ShortcutTrigger = { - enabled?: boolean; - modifier?: ShortcutModifierDefinition; - id?: string; - key: string; - callback?: (detail: ShortcutEventDetail) => void; - preventDefault?: boolean; -}; - -export type ShortcutParameter = { - enabled?: boolean; - trigger: Array | ShortcutTrigger; - type?: 'keydown' | 'keyup'; -}; - -export function shortcut(node: Window, param: ShortcutParameter) { - let { enabled = true, trigger, type = 'keydown' } = param; - - function handler(event: KeyboardEvent) { - const normalizedTriggers = Array.isArray(trigger) ? trigger : [trigger]; - const modifiedMap = { - alt: event.altKey, - ctrl: event.ctrlKey, - shift: event.shiftKey, - meta: event.metaKey - }; - for (const trigger of normalizedTriggers) { - const mergedTrigger = { - modifier: [], - preventDefault: false, - enabled: true, - ...trigger - }; - const { modifier, key, callback, preventDefault, enabled: triggerEnabled } = mergedTrigger; - if (triggerEnabled) { - if (modifier.length) { - const modifierDefs = (Array.isArray(modifier) ? modifier : [modifier]).map((def) => - typeof def === 'string' ? [def] : def - ); - const modified = modifierDefs.some((def) => - def.every((modifier) => modifiedMap[modifier]) - ); - if (!modified) continue; - } - if (event.key === key) { - if (preventDefault) event.preventDefault(); - const detail = { event, trigger: mergedTrigger }; - callback?.(detail); - } - } - } - } - - if (enabled) node.addEventListener(type, handler); - - return { - update: (update: ShortcutParameter) => { - const { enabled: newEnabled = true, type: newType = 'keydown' } = update; - - if (enabled && (!newEnabled || type !== newType)) { - node.removeEventListener(type, handler); - } else if (!enabled && newEnabled) { - node.addEventListener(newType, handler); - } - - enabled = newEnabled; - type = newType; - trigger = update.trigger; - }, - destroy: () => { - node.removeEventListener(type, handler); - } - }; -} diff --git a/packages/svelte/src/lib/components/KeyHandler/KeyHandler.svelte b/packages/svelte/src/lib/components/KeyHandler/KeyHandler.svelte index 54f3a9c1..36206982 100644 --- a/packages/svelte/src/lib/components/KeyHandler/KeyHandler.svelte +++ b/packages/svelte/src/lib/components/KeyHandler/KeyHandler.svelte @@ -1,10 +1,10 @@