Merge pull request #4222 from xyflow/next
React Flow 12.0.0-next.17 | Svelte Flow 0.1.0
This commit is contained in:
@@ -54,26 +54,24 @@ const DefaultNodes = () => {
|
||||
|
||||
const updateNodePositions = () => {
|
||||
instance.setNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
node.position = {
|
||||
nodes.map((node) => ({
|
||||
...node,
|
||||
position: {
|
||||
x: Math.random() * 400,
|
||||
y: Math.random() * 400,
|
||||
};
|
||||
|
||||
return node;
|
||||
})
|
||||
},
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
const updateEdgeColors = () => {
|
||||
instance.setEdges((edges) =>
|
||||
edges.map((edge) => {
|
||||
edge.style = {
|
||||
edges.map((edge) => ({
|
||||
...edge,
|
||||
style: {
|
||||
stroke: '#ff5050',
|
||||
};
|
||||
|
||||
return edge;
|
||||
})
|
||||
},
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -44,10 +44,10 @@ export default function NodeInspector() {
|
||||
<ViewportPortal>
|
||||
<div className="react-flow__devtools-nodeinspector">
|
||||
{nodes.map((node) => {
|
||||
const x = node.computed?.positionAbsolute?.x || 0;
|
||||
const y = node.computed?.positionAbsolute?.y || 0;
|
||||
const width = node.computed?.width || 0;
|
||||
const height = node.computed?.height || 0;
|
||||
const x = node?.position?.x || 0;
|
||||
const y = node?.position?.y || 0;
|
||||
const width = node.measured?.width || 0;
|
||||
const height = node.measured?.height || 0;
|
||||
|
||||
return (
|
||||
<NodeInfo
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
useEdgesState,
|
||||
OnConnect,
|
||||
useNodesInitialized,
|
||||
Panel,
|
||||
useReactFlow,
|
||||
} from '@xyflow/react';
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
@@ -49,6 +51,7 @@ const initialEdges: Edge[] = [
|
||||
];
|
||||
|
||||
const UseZoomPanHelperFlow = () => {
|
||||
const { addNodes } = useReactFlow();
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
@@ -59,6 +62,13 @@ const UseZoomPanHelperFlow = () => {
|
||||
console.log('initialized', initialized);
|
||||
}, [initialized]);
|
||||
|
||||
const addNode = () =>
|
||||
addNodes({
|
||||
id: `${Math.random()}`,
|
||||
data: { label: 'new node' },
|
||||
position: { x: Math.random() * 400, y: Math.random() * 400 },
|
||||
});
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
@@ -71,6 +81,9 @@ const UseZoomPanHelperFlow = () => {
|
||||
>
|
||||
<Background />
|
||||
<MiniMap />
|
||||
<Panel>
|
||||
<button onClick={addNode}>add node</button>
|
||||
</Panel>
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import DragHandleNode from './components/DragHandleNode.svelte';
|
||||
export default {
|
||||
flowProps: {
|
||||
fitView: true,
|
||||
nodeDragThreshold: 0,
|
||||
nodeTypes: {
|
||||
DragHandleNode
|
||||
},
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
|
||||
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
|
||||
|
||||
type $$Props = NodeProps;
|
||||
type $$Props = NodeProps;
|
||||
|
||||
export let isConnectable: $$Props['isConnectable'];
|
||||
export let isConnectable: $$Props['isConnectable'];
|
||||
$$restProps;
|
||||
|
||||
const DEFAULT_HANDLE_STYLE = 'width: 10px; height: 10px; bottom: -5px;';
|
||||
const handleStyle = 'width: 10px; height: 10px; bottom: -5px;';
|
||||
</script>
|
||||
|
||||
<div style="background: #DDD; padding: 25px">
|
||||
<div>Node</div>
|
||||
<Handle
|
||||
type="source"
|
||||
id="red"
|
||||
position={Position.Bottom}
|
||||
style={DEFAULT_HANDLE_STYLE + 'left: 15%; background: red;'}
|
||||
{isConnectable}
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="blue"
|
||||
style={DEFAULT_HANDLE_STYLE + 'left: 50%; background: blue;'}
|
||||
{isConnectable}
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="orange"
|
||||
style={DEFAULT_HANDLE_STYLE + 'left: 85%; background: orange;'}
|
||||
{isConnectable}
|
||||
/>
|
||||
<div>Node</div>
|
||||
<Handle
|
||||
type="source"
|
||||
id="red"
|
||||
position={Position.Bottom}
|
||||
style={handleStyle + 'left: 15%; background: red;'}
|
||||
{isConnectable}
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="blue"
|
||||
style={handleStyle + 'left: 50%; background: blue;'}
|
||||
{isConnectable}
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="orange"
|
||||
style={handleStyle + 'left: 85%; background: orange;'}
|
||||
{isConnectable}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
export let data: $$Props['data'];
|
||||
|
||||
$$restProps;
|
||||
|
||||
const { colorStore } = data;
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,27 +4,18 @@
|
||||
type $$Props = EdgeProps;
|
||||
|
||||
export let id: $$Props['id'] = '';
|
||||
export let source: $$Props['source'] = '';
|
||||
export let target: $$Props['target'] = '';
|
||||
export let animated: $$Props['animated'] = undefined;
|
||||
export let selected: $$Props['selected'] = undefined;
|
||||
export let label: $$Props['label'] = undefined;
|
||||
export let labelStyle: $$Props['labelStyle'] = undefined;
|
||||
export let data: $$Props['data'] = undefined;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let markerStart: $$Props['markerStart'] = undefined;
|
||||
export let markerEnd: $$Props['markerEnd'] = undefined;
|
||||
export let interactionWidth: $$Props['interactionWidth'] = undefined;
|
||||
|
||||
export let sourceX: $$Props['sourceX'];
|
||||
export let sourceY: $$Props['sourceY'];
|
||||
export let sourcePosition: $$Props['sourcePosition'];
|
||||
export let sourceHandleId: $$Props['sourceHandleId'] = undefined;
|
||||
|
||||
export let targetX: $$Props['targetX'];
|
||||
export let targetY: $$Props['targetY'];
|
||||
export let targetPosition: $$Props['targetPosition'];
|
||||
export let targetHandleId: $$Props['targetHandleId'] = undefined;
|
||||
|
||||
$$restProps;
|
||||
|
||||
$: [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX,
|
||||
@@ -34,18 +25,6 @@
|
||||
targetY,
|
||||
targetPosition
|
||||
});
|
||||
|
||||
source;
|
||||
target;
|
||||
animated;
|
||||
selected;
|
||||
data;
|
||||
label;
|
||||
labelStyle;
|
||||
markerStart;
|
||||
interactionWidth;
|
||||
sourceHandleId;
|
||||
targetHandleId;
|
||||
</script>
|
||||
|
||||
<BaseEdge path={edgePath} {markerEnd} {style} />
|
||||
|
||||
@@ -3,14 +3,8 @@
|
||||
|
||||
type $$Props = EdgeProps;
|
||||
|
||||
export let id: $$Props['id'] = '';
|
||||
export let source: $$Props['source'] = '';
|
||||
export let target: $$Props['target'] = '';
|
||||
export let animated: $$Props['animated'] = undefined;
|
||||
export let selected: $$Props['selected'] = undefined;
|
||||
export let label: $$Props['label'] = undefined;
|
||||
export let labelStyle: $$Props['labelStyle'] = undefined;
|
||||
export let data: $$Props['data'] = undefined;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let markerStart: $$Props['markerStart'] = undefined;
|
||||
export let markerEnd: $$Props['markerEnd'] = undefined;
|
||||
@@ -19,23 +13,12 @@
|
||||
export let sourceX: $$Props['sourceX'];
|
||||
export let sourceY: $$Props['sourceY'];
|
||||
export let sourcePosition: $$Props['sourcePosition'];
|
||||
export let sourceHandleId: $$Props['sourceHandleId'] = undefined;
|
||||
|
||||
export let targetX: $$Props['targetX'];
|
||||
export let targetY: $$Props['targetY'];
|
||||
export let targetPosition: $$Props['targetPosition'];
|
||||
export let targetHandleId: $$Props['targetHandleId'] = undefined;
|
||||
|
||||
id;
|
||||
source;
|
||||
target;
|
||||
animated;
|
||||
selected;
|
||||
data;
|
||||
sourcePosition;
|
||||
targetPosition;
|
||||
sourceHandleId;
|
||||
targetHandleId;
|
||||
$$restProps;
|
||||
</script>
|
||||
|
||||
<BezierEdge
|
||||
|
||||
@@ -33,33 +33,7 @@
|
||||
console.log('connections', id, $connections);
|
||||
}
|
||||
|
||||
export let data: $$Props['data'];
|
||||
export let targetPosition: $$Props['targetPosition'] = Position.Top;
|
||||
export let sourcePosition: $$Props['sourcePosition'] = Position.Bottom;
|
||||
export let width: $$Props['width'] = undefined;
|
||||
export let height: $$Props['height'] = undefined;
|
||||
export let selected: $$Props['selected'] = undefined;
|
||||
export let type: $$Props['type'] = undefined;
|
||||
export let zIndex: $$Props['zIndex'] = undefined;
|
||||
export let dragging: $$Props['dragging'] = false;
|
||||
export let dragHandle: $$Props['dragHandle'] = undefined;
|
||||
export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
|
||||
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
|
||||
export let isConnectable: $$Props['isConnectable'] = undefined;
|
||||
|
||||
data;
|
||||
targetPosition;
|
||||
sourcePosition;
|
||||
width;
|
||||
height;
|
||||
selected;
|
||||
type;
|
||||
zIndex;
|
||||
dragging;
|
||||
dragHandle;
|
||||
positionAbsoluteX;
|
||||
positionAbsoluteY;
|
||||
isConnectable;
|
||||
$$restProps;
|
||||
</script>
|
||||
|
||||
<div class="custom">
|
||||
|
||||
@@ -21,34 +21,7 @@
|
||||
console.log('disconnect source', connection);
|
||||
}
|
||||
|
||||
export let data: $$Props['data'];
|
||||
export let targetPosition: $$Props['targetPosition'] = Position.Top;
|
||||
export let sourcePosition: $$Props['sourcePosition'] = Position.Bottom;
|
||||
export let width: $$Props['width'] = undefined;
|
||||
export let height: $$Props['height'] = undefined;
|
||||
export let selected: $$Props['selected'] = undefined;
|
||||
export let type: $$Props['type'] = undefined;
|
||||
export let zIndex: $$Props['zIndex'] = undefined;
|
||||
export let dragging: $$Props['dragging'] = false;
|
||||
export let dragHandle: $$Props['dragHandle'] = undefined;
|
||||
export let positionAbsolute: $$Props['positionAbsolute'] = {
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
export let isConnectable: $$Props['isConnectable'] = undefined;
|
||||
|
||||
data;
|
||||
targetPosition;
|
||||
sourcePosition;
|
||||
width;
|
||||
height;
|
||||
selected;
|
||||
type;
|
||||
zIndex;
|
||||
dragging;
|
||||
dragHandle;
|
||||
positionAbsolute;
|
||||
isConnectable;
|
||||
$$restProps;
|
||||
</script>
|
||||
|
||||
<div class="custom">
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { SvelteFlow, Controls, Panel, type Node, type Edge } from '@xyflow/svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { SvelteFlow, Controls, Panel, type Edge } from '@xyflow/svelte';
|
||||
|
||||
import DefaultResizer from './DefaultResizer.svelte';
|
||||
import CustomResizer from './CustomResizer.svelte';
|
||||
import VerticalResizer from './VerticalResizer.svelte';
|
||||
import HorizontalResizer from './HorizontalResizer.svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
import '@xyflow/svelte/dist/style.css';
|
||||
import type { ResizeNode } from './types';
|
||||
|
||||
const nodeTypes = {
|
||||
defaultResizer: DefaultResizer,
|
||||
@@ -20,7 +21,7 @@
|
||||
|
||||
const edges = writable<Edge[]>([]);
|
||||
|
||||
const nodes = writable<Node[]>([
|
||||
const nodes = writable<ResizeNode[]>([
|
||||
{
|
||||
id: '1',
|
||||
type: 'defaultResizer',
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { Handle, Position, type NodeProps, NodeResizeControl } from '@xyflow/svelte';
|
||||
import type { ResizeNode } from './types';
|
||||
|
||||
type $$Props = NodeProps;
|
||||
type $$Props = NodeProps<ResizeNode>;
|
||||
|
||||
export let data: $$Props['data'] = undefined;
|
||||
export let data: $$Props['data'];
|
||||
</script>
|
||||
|
||||
<NodeResizeControl
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { Handle, NodeResizer, Position, type NodeProps } from '@xyflow/svelte';
|
||||
import type { ResizeNode } from './types';
|
||||
|
||||
type $$Props = NodeProps;
|
||||
type $$Props = NodeProps<ResizeNode>;
|
||||
|
||||
export let data: $$Props['data'] = undefined;
|
||||
export let data: $$Props['data'];
|
||||
export let selected: $$Props['selected'] = undefined;
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { Handle, NodeResizeControl, Position, type NodeProps } from '@xyflow/svelte';
|
||||
import type { ResizeNode } from './types';
|
||||
|
||||
type $$Props = NodeProps;
|
||||
type $$Props = NodeProps<ResizeNode>;
|
||||
|
||||
export let data: $$Props['data'] = undefined;
|
||||
export let data: $$Props['data'];
|
||||
</script>
|
||||
|
||||
<NodeResizeControl
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
function ResizeIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="8"
|
||||
height="8"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
style={{ position: 'absolute', right: 2, bottom: 2 }}
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<polyline points="16 20 20 20 20 16" />
|
||||
<line x1="14" y1="14" x2="20" y2="20" />
|
||||
<polyline points="8 4 4 4 4 8" />
|
||||
<line x1="4" y1="4" x2="10" y2="10" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResizeIcon;
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { Handle, NodeResizeControl, Position, type NodeProps } from '@xyflow/svelte';
|
||||
import type { ResizeNode } from './types';
|
||||
|
||||
type $$Props = NodeProps;
|
||||
type $$Props = NodeProps<ResizeNode>;
|
||||
|
||||
export let data: $$Props['data'] = undefined;
|
||||
export let data: $$Props['data'];
|
||||
</script>
|
||||
|
||||
<NodeResizeControl
|
||||
|
||||
21
examples/svelte/src/routes/examples/node-resizer/types.ts
Normal file
21
examples/svelte/src/routes/examples/node-resizer/types.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
type Node,
|
||||
type ShouldResize,
|
||||
type OnResizeStart,
|
||||
type OnResize,
|
||||
type OnResizeEnd
|
||||
} from '@xyflow/svelte';
|
||||
|
||||
export type ResizeNode = Node<{
|
||||
minWidth?: number;
|
||||
maxWidth?: number;
|
||||
minHeight?: number;
|
||||
maxHeight?: number;
|
||||
shouldResize?: ShouldResize;
|
||||
onResizeStart?: OnResizeStart;
|
||||
onResize?: OnResize;
|
||||
onResizeEnd?: OnResizeEnd;
|
||||
keepAspectRatio?: boolean;
|
||||
label?: string;
|
||||
isVisible?: boolean;
|
||||
}>;
|
||||
@@ -2,6 +2,7 @@
|
||||
import { NodeToolbar, type NodeProps, Handle, Position } from '@xyflow/svelte';
|
||||
|
||||
type $$Props = NodeProps;
|
||||
$$restProps;
|
||||
|
||||
export let data: $$Props['data'];
|
||||
</script>
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
type Node,
|
||||
type Edge,
|
||||
ConnectionMode,
|
||||
useSvelteFlow,
|
||||
ControlButton
|
||||
ControlButton,
|
||||
type FitViewOptions
|
||||
} from '@xyflow/svelte';
|
||||
|
||||
import CustomNode from './CustomNode.svelte';
|
||||
@@ -33,6 +33,11 @@
|
||||
custom: CustomEdge
|
||||
};
|
||||
|
||||
const fitViewOptions: FitViewOptions = {
|
||||
padding: 0.2,
|
||||
nodes: [{ id: '1' }, { id: '2' }]
|
||||
};
|
||||
|
||||
const nodes = writable<Node[]>([
|
||||
{
|
||||
id: '1',
|
||||
@@ -191,7 +196,7 @@
|
||||
attributionPosition={'top-center'}
|
||||
deleteKey={['Backspace', 'd']}
|
||||
>
|
||||
<Controls orientation="horizontal">
|
||||
<Controls orientation="horizontal" {fitViewOptions}>
|
||||
<ControlButton slot="before">xy</ControlButton>
|
||||
<ControlButton aria-label="log" on:click={() => console.log('control button')}
|
||||
>log</ControlButton
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
|
||||
import { Handle, Position, type BuiltInNode, type NodeProps } from '@xyflow/svelte';
|
||||
|
||||
type $$Props = NodeProps;
|
||||
type $$Props = NodeProps<BuiltInNode>;
|
||||
$$restProps;
|
||||
|
||||
export let data: { label: string } = { label: 'Node' };
|
||||
export let positionAbsoluteX: number = 0;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type { NodeProps } from '@xyflow/svelte';
|
||||
import type { BuiltInNode, NodeProps } from '@xyflow/svelte';
|
||||
|
||||
type $$Props = NodeProps;
|
||||
type $$Props = NodeProps<BuiltInNode>;
|
||||
$$restProps;
|
||||
|
||||
export let data: { label: string } = { label: 'Node' };
|
||||
</script>
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Handle, Position, type NodeProps, type XYPosition } from '@xyflow/svelte';
|
||||
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
|
||||
|
||||
type $$Props = NodeProps;
|
||||
|
||||
export let id: string;
|
||||
export let positionAbsolute: XYPosition = { x: 0, y: 0 };
|
||||
export let positionAbsoluteX: number = 0;
|
||||
export let positionAbsoluteY: number = 0;
|
||||
export let zIndex: number = 0;
|
||||
|
||||
$$restProps;
|
||||
</script>
|
||||
|
||||
<Handle type="target" position={Position.Top} />
|
||||
<div>{id}</div>
|
||||
<div>
|
||||
x:{Math.round(positionAbsolute.x)} y:{Math.round(positionAbsolute.y)} z:{zIndex}
|
||||
x:{Math.round(positionAbsoluteX)} y:{Math.round(positionAbsoluteY)} z:{zIndex}
|
||||
</div>
|
||||
<Handle type="source" position={Position.Bottom} />
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
type $$Props = NodeProps;
|
||||
|
||||
export let id: $$Props['id'];
|
||||
$$restProps;
|
||||
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# @xyflow/react
|
||||
|
||||
## 12.0.0-next.17
|
||||
|
||||
- fix broken `defaultNodes`
|
||||
- add string array to `UpdateNodeInternals` thanks @DenizUgur
|
||||
- pinch zoom on windows
|
||||
- drag for touch devices
|
||||
- return user node in node event handlers
|
||||
- cleanup `useReactFlow`
|
||||
- export `KeyCode` and `Align` type
|
||||
|
||||
## 12.0.0-next.16
|
||||
|
||||
## Patch changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xyflow/react",
|
||||
"version": "12.0.0-next.16",
|
||||
"version": "12.0.0-next.17",
|
||||
"description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.",
|
||||
"keywords": [
|
||||
"react",
|
||||
|
||||
@@ -98,11 +98,21 @@ export function NodeWrapper<NodeType extends Node>({
|
||||
});
|
||||
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
|
||||
|
||||
const onMouseEnterHandler = onMouseEnter ? (event: MouseEvent) => onMouseEnter(event, { ...node }) : undefined;
|
||||
const onMouseMoveHandler = onMouseMove ? (event: MouseEvent) => onMouseMove(event, { ...node }) : undefined;
|
||||
const onMouseLeaveHandler = onMouseLeave ? (event: MouseEvent) => onMouseLeave(event, { ...node }) : undefined;
|
||||
const onContextMenuHandler = onContextMenu ? (event: MouseEvent) => onContextMenu(event, { ...node }) : undefined;
|
||||
const onDoubleClickHandler = onDoubleClick ? (event: MouseEvent) => onDoubleClick(event, { ...node }) : undefined;
|
||||
const onMouseEnterHandler = onMouseEnter
|
||||
? (event: MouseEvent) => onMouseEnter(event, { ...internals.userNode })
|
||||
: undefined;
|
||||
const onMouseMoveHandler = onMouseMove
|
||||
? (event: MouseEvent) => onMouseMove(event, { ...internals.userNode })
|
||||
: undefined;
|
||||
const onMouseLeaveHandler = onMouseLeave
|
||||
? (event: MouseEvent) => onMouseLeave(event, { ...internals.userNode })
|
||||
: undefined;
|
||||
const onContextMenuHandler = onContextMenu
|
||||
? (event: MouseEvent) => onContextMenu(event, { ...internals.userNode })
|
||||
: undefined;
|
||||
const onDoubleClickHandler = onDoubleClick
|
||||
? (event: MouseEvent) => onDoubleClick(event, { ...internals.userNode })
|
||||
: undefined;
|
||||
|
||||
const onSelectNodeHandler = (event: MouseEvent) => {
|
||||
const { selectNodesOnDrag, nodeDragThreshold } = store.getState();
|
||||
@@ -118,7 +128,7 @@ export function NodeWrapper<NodeType extends Node>({
|
||||
}
|
||||
|
||||
if (onClick) {
|
||||
onClick(event, { ...node });
|
||||
onClick(event, { ...internals.userNode });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { useRef, useEffect, type MouseEvent, type KeyboardEvent } from 'react';
|
||||
import cc from 'classcat';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { getInternalNodesBounds } from '@xyflow/system';
|
||||
import { getInternalNodesBounds, isNumeric } from '@xyflow/system';
|
||||
|
||||
import { useStore, useStoreApi } from '../../hooks/useStore';
|
||||
import { useDrag } from '../../hooks/useDrag';
|
||||
@@ -26,8 +26,8 @@ const selector = (s: ReactFlowState) => {
|
||||
});
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
width: isNumeric(width) ? width : null,
|
||||
height: isNumeric(height) ? height : null,
|
||||
userSelectionActive: s.userSelectionActive,
|
||||
transformString: `translate(${s.transform[0]}px,${s.transform[1]}px) scale(${s.transform[2]}) translate(${x}px,${y}px)`,
|
||||
};
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
EdgeRemoveChange,
|
||||
evaluateAbsolutePosition,
|
||||
getElementsToRemove,
|
||||
getOverlappingArea,
|
||||
isRectObject,
|
||||
NodeRemoveChange,
|
||||
nodeToRect,
|
||||
type Rect,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import useViewportHelper from './useViewportHelper';
|
||||
import { useStoreApi } from './useStore';
|
||||
import { useStore, useStoreApi } from './useStore';
|
||||
import { useBatchContext } from '../components/BatchProvider';
|
||||
import { isNode } from '../utils';
|
||||
import type { ReactFlowInstance, Instance, Node, Edge, InternalNode } from '../types';
|
||||
import { elementToRemoveChange, isNode } from '../utils';
|
||||
import type { ReactFlowInstance, Node, Edge, InternalNode, ReactFlowState, GeneralHelpers } from '../types';
|
||||
|
||||
const selector = (s: ReactFlowState) => !!s.panZoom;
|
||||
|
||||
/**
|
||||
* Hook for accessing the ReactFlow instance.
|
||||
@@ -27,185 +31,40 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
||||
const viewportHelper = useViewportHelper();
|
||||
const store = useStoreApi();
|
||||
const batchContext = useBatchContext();
|
||||
const viewportInitialized = useStore(selector);
|
||||
|
||||
const getNodes = useCallback<Instance.GetNodes<NodeType>>(
|
||||
() => store.getState().nodes.map((n) => ({ ...n })) as NodeType[],
|
||||
[]
|
||||
);
|
||||
const generalHelper = useMemo<GeneralHelpers<NodeType, EdgeType>>(() => {
|
||||
const getInternalNode: GeneralHelpers<NodeType, EdgeType>['getInternalNode'] = (id) =>
|
||||
store.getState().nodeLookup.get(id) as InternalNode<NodeType>;
|
||||
|
||||
const getInternalNode = useCallback<Instance.GetInternalNode<NodeType>>(
|
||||
(id) => store.getState().nodeLookup.get(id) as InternalNode<NodeType>,
|
||||
[]
|
||||
);
|
||||
|
||||
const getNode = useCallback<Instance.GetNode<NodeType>>(
|
||||
(id) => getInternalNode(id)?.internals.userNode as NodeType,
|
||||
[getInternalNode]
|
||||
);
|
||||
|
||||
const getEdges = useCallback<Instance.GetEdges<EdgeType>>(() => {
|
||||
const { edges = [] } = store.getState();
|
||||
return edges.map((e) => ({ ...e })) as EdgeType[];
|
||||
}, []);
|
||||
|
||||
const getEdge = useCallback<Instance.GetEdge<EdgeType>>((id) => store.getState().edgeLookup.get(id) as EdgeType, []);
|
||||
|
||||
const setNodes = useCallback<Instance.SetNodes<NodeType>>((payload) => {
|
||||
batchContext.nodeQueue.push(payload as NodeType[]);
|
||||
}, []);
|
||||
|
||||
const setEdges = useCallback<Instance.SetEdges<EdgeType>>((payload) => {
|
||||
batchContext.edgeQueue.push(payload as EdgeType[]);
|
||||
}, []);
|
||||
|
||||
const addNodes = useCallback<Instance.AddNodes<NodeType>>((payload) => {
|
||||
const newNodes = Array.isArray(payload) ? payload : [payload];
|
||||
batchContext.nodeQueue.push((nodes) => [...nodes, ...newNodes]);
|
||||
}, []);
|
||||
|
||||
const addEdges = useCallback<Instance.AddEdges<EdgeType>>((payload) => {
|
||||
const newEdges = Array.isArray(payload) ? payload : [payload];
|
||||
batchContext.edgeQueue.push((edges) => [...edges, ...newEdges]);
|
||||
}, []);
|
||||
|
||||
const toObject = useCallback<Instance.ToObject<NodeType, EdgeType>>(() => {
|
||||
const { nodes = [], edges = [], transform } = store.getState();
|
||||
const [x, y, zoom] = transform;
|
||||
return {
|
||||
nodes: nodes.map((n) => ({ ...n })) as NodeType[],
|
||||
edges: edges.map((e) => ({ ...e })) as EdgeType[],
|
||||
viewport: {
|
||||
x,
|
||||
y,
|
||||
zoom,
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
|
||||
const deleteElements = useCallback<Instance.DeleteElements>(
|
||||
async ({ nodes: nodesToRemove = [], edges: edgesToRemove = [] }) => {
|
||||
const {
|
||||
nodes,
|
||||
edges,
|
||||
hasDefaultNodes,
|
||||
hasDefaultEdges,
|
||||
onNodesDelete,
|
||||
onEdgesDelete,
|
||||
onNodesChange,
|
||||
onEdgesChange,
|
||||
onDelete,
|
||||
onBeforeDelete,
|
||||
} = store.getState();
|
||||
const { nodes: matchingNodes, edges: matchingEdges } = await getElementsToRemove({
|
||||
nodesToRemove,
|
||||
edgesToRemove,
|
||||
nodes,
|
||||
edges,
|
||||
onBeforeDelete,
|
||||
});
|
||||
|
||||
const hasMatchingEdges = matchingEdges.length > 0;
|
||||
const hasMatchingNodes = matchingNodes.length > 0;
|
||||
|
||||
if (hasMatchingEdges) {
|
||||
if (hasDefaultEdges) {
|
||||
const nextEdges = edges.filter((e) => !matchingEdges.some((mE) => mE.id === e.id));
|
||||
store.getState().setEdges(nextEdges);
|
||||
}
|
||||
|
||||
onEdgesDelete?.(matchingEdges);
|
||||
onEdgesChange?.(
|
||||
matchingEdges.map((edge) => ({
|
||||
id: edge.id,
|
||||
type: 'remove',
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
if (hasMatchingNodes) {
|
||||
if (hasDefaultNodes) {
|
||||
const nextNodes = nodes.filter((n) => !matchingNodes.some((mN) => mN.id === n.id));
|
||||
store.getState().setNodes(nextNodes);
|
||||
}
|
||||
|
||||
onNodesDelete?.(matchingNodes);
|
||||
onNodesChange?.(matchingNodes.map((node) => ({ id: node.id, type: 'remove' })));
|
||||
}
|
||||
|
||||
if (hasMatchingNodes || hasMatchingEdges) {
|
||||
onDelete?.({ nodes: matchingNodes, edges: matchingEdges });
|
||||
}
|
||||
|
||||
return { deletedNodes: matchingNodes, deletedEdges: matchingEdges };
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const getNodeRect = useCallback((node: NodeType | { id: string }): Rect | null => {
|
||||
const { nodeLookup, nodeOrigin } = store.getState();
|
||||
|
||||
const nodeToUse = isNode<NodeType>(node) ? node : nodeLookup.get(node.id)!;
|
||||
const position = nodeToUse.parentId
|
||||
? evaluateAbsolutePosition(nodeToUse.position, nodeToUse.parentId, nodeLookup, nodeOrigin)
|
||||
: nodeToUse.position;
|
||||
|
||||
const nodeWithPosition = {
|
||||
id: nodeToUse.id,
|
||||
position,
|
||||
width: nodeToUse.measured?.width ?? nodeToUse.width,
|
||||
height: nodeToUse.measured?.height ?? nodeToUse.height,
|
||||
data: nodeToUse.data,
|
||||
const setNodes: GeneralHelpers<NodeType, EdgeType>['setNodes'] = (payload) => {
|
||||
batchContext.nodeQueue.push(payload as NodeType[]);
|
||||
};
|
||||
|
||||
return nodeToRect(nodeWithPosition);
|
||||
}, []);
|
||||
const getNodeRect = (node: NodeType | { id: string }): Rect | null => {
|
||||
const { nodeLookup, nodeOrigin } = store.getState();
|
||||
|
||||
const getIntersectingNodes = useCallback<Instance.GetIntersectingNodes<NodeType>>(
|
||||
(nodeOrRect, partially = true, nodes) => {
|
||||
const isRect = isRectObject(nodeOrRect);
|
||||
const nodeRect = isRect ? nodeOrRect : getNodeRect(nodeOrRect);
|
||||
const hasNodesOption = nodes !== undefined;
|
||||
const nodeToUse = isNode<NodeType>(node) ? node : nodeLookup.get(node.id)!;
|
||||
const position = nodeToUse.parentId
|
||||
? evaluateAbsolutePosition(nodeToUse.position, nodeToUse.parentId, nodeLookup, nodeOrigin)
|
||||
: nodeToUse.position;
|
||||
|
||||
if (!nodeRect) {
|
||||
return [];
|
||||
}
|
||||
const nodeWithPosition = {
|
||||
id: nodeToUse.id,
|
||||
position,
|
||||
width: nodeToUse.measured?.width ?? nodeToUse.width,
|
||||
height: nodeToUse.measured?.height ?? nodeToUse.height,
|
||||
data: nodeToUse.data,
|
||||
};
|
||||
|
||||
return (nodes || store.getState().nodes).filter((n) => {
|
||||
const internalNode = store.getState().nodeLookup.get(n.id);
|
||||
return nodeToRect(nodeWithPosition);
|
||||
};
|
||||
|
||||
if (internalNode && !isRect && (n.id === nodeOrRect!.id || !internalNode.internals.positionAbsolute)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currNodeRect = nodeToRect(hasNodesOption ? n : internalNode!);
|
||||
const overlappingArea = getOverlappingArea(currNodeRect, nodeRect);
|
||||
const partiallyVisible = partially && overlappingArea > 0;
|
||||
|
||||
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
|
||||
}) as NodeType[];
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const isNodeIntersecting = useCallback<Instance.IsNodeIntersecting<NodeType>>(
|
||||
(nodeOrRect, area, partially = true) => {
|
||||
const isRect = isRectObject(nodeOrRect);
|
||||
const nodeRect = isRect ? nodeOrRect : getNodeRect(nodeOrRect);
|
||||
|
||||
if (!nodeRect) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const overlappingArea = getOverlappingArea(nodeRect, area);
|
||||
const partiallyVisible = partially && overlappingArea > 0;
|
||||
|
||||
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const updateNode = useCallback<Instance.UpdateNode<NodeType>>(
|
||||
(id, nodeUpdate, options = { replace: false }) => {
|
||||
const updateNode: GeneralHelpers<NodeType, EdgeType>['updateNode'] = (
|
||||
id,
|
||||
nodeUpdate,
|
||||
options = { replace: false }
|
||||
) => {
|
||||
setNodes((prevNodes) =>
|
||||
prevNodes.map((node) => {
|
||||
if (node.id === id) {
|
||||
@@ -216,59 +75,139 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
||||
return node;
|
||||
})
|
||||
);
|
||||
},
|
||||
[setNodes]
|
||||
);
|
||||
};
|
||||
|
||||
const updateNodeData = useCallback<Instance.UpdateNodeData<NodeType>>(
|
||||
(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 {
|
||||
getNodes: () => store.getState().nodes.map((n) => ({ ...n })) as NodeType[],
|
||||
getNode: (id) => getInternalNode(id)?.internals.userNode as NodeType,
|
||||
getInternalNode,
|
||||
getEdges: () => {
|
||||
const { edges = [] } = store.getState();
|
||||
return edges.map((e) => ({ ...e })) as EdgeType[];
|
||||
},
|
||||
getEdge: (id) => store.getState().edgeLookup.get(id) as EdgeType,
|
||||
setNodes,
|
||||
setEdges: (payload) => {
|
||||
batchContext.edgeQueue.push(payload as EdgeType[]);
|
||||
},
|
||||
addNodes: (payload) => {
|
||||
const newNodes = Array.isArray(payload) ? payload : [payload];
|
||||
batchContext.nodeQueue.push((nodes) => [...nodes, ...newNodes]);
|
||||
},
|
||||
addEdges: (payload) => {
|
||||
const newEdges = Array.isArray(payload) ? payload : [payload];
|
||||
batchContext.edgeQueue.push((edges) => [...edges, ...newEdges]);
|
||||
},
|
||||
toObject: () => {
|
||||
const { nodes = [], edges = [], transform } = store.getState();
|
||||
const [x, y, zoom] = transform;
|
||||
return {
|
||||
nodes: nodes.map((n) => ({ ...n })) as NodeType[],
|
||||
edges: edges.map((e) => ({ ...e })) as EdgeType[],
|
||||
viewport: {
|
||||
x,
|
||||
y,
|
||||
zoom,
|
||||
},
|
||||
};
|
||||
},
|
||||
deleteElements: async ({ nodes: nodesToRemove = [], edges: edgesToRemove = [] }) => {
|
||||
const {
|
||||
nodes,
|
||||
edges,
|
||||
onNodesDelete,
|
||||
onEdgesDelete,
|
||||
triggerNodeChanges,
|
||||
triggerEdgeChanges,
|
||||
onDelete,
|
||||
onBeforeDelete,
|
||||
} = store.getState();
|
||||
const { nodes: matchingNodes, edges: matchingEdges } = await getElementsToRemove({
|
||||
nodesToRemove,
|
||||
edgesToRemove,
|
||||
nodes,
|
||||
edges,
|
||||
onBeforeDelete,
|
||||
});
|
||||
|
||||
const hasMatchingEdges = matchingEdges.length > 0;
|
||||
const hasMatchingNodes = matchingNodes.length > 0;
|
||||
|
||||
if (hasMatchingEdges) {
|
||||
const edgeChanges: EdgeRemoveChange[] = matchingEdges.map(elementToRemoveChange);
|
||||
|
||||
onEdgesDelete?.(matchingEdges);
|
||||
triggerEdgeChanges(edgeChanges);
|
||||
}
|
||||
|
||||
if (hasMatchingNodes) {
|
||||
const nodeChanges: NodeRemoveChange[] = matchingNodes.map(elementToRemoveChange);
|
||||
|
||||
onNodesDelete?.(matchingNodes);
|
||||
triggerNodeChanges(nodeChanges);
|
||||
}
|
||||
|
||||
if (hasMatchingNodes || hasMatchingEdges) {
|
||||
onDelete?.({ nodes: matchingNodes, edges: matchingEdges });
|
||||
}
|
||||
|
||||
return { deletedNodes: matchingNodes, deletedEdges: matchingEdges };
|
||||
},
|
||||
getIntersectingNodes: (nodeOrRect, partially = true, nodes) => {
|
||||
const isRect = isRectObject(nodeOrRect);
|
||||
const nodeRect = isRect ? nodeOrRect : getNodeRect(nodeOrRect);
|
||||
const hasNodesOption = nodes !== undefined;
|
||||
|
||||
if (!nodeRect) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (nodes || store.getState().nodes).filter((n) => {
|
||||
const internalNode = store.getState().nodeLookup.get(n.id);
|
||||
|
||||
if (internalNode && !isRect && (n.id === nodeOrRect!.id || !internalNode.internals.positionAbsolute)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currNodeRect = nodeToRect(hasNodesOption ? n : internalNode!);
|
||||
const overlappingArea = getOverlappingArea(currNodeRect, nodeRect);
|
||||
const partiallyVisible = partially && overlappingArea > 0;
|
||||
|
||||
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
|
||||
}) as NodeType[];
|
||||
},
|
||||
isNodeIntersecting: (nodeOrRect, area, partially = true) => {
|
||||
const isRect = isRectObject(nodeOrRect);
|
||||
const nodeRect = isRect ? nodeOrRect : getNodeRect(nodeOrRect);
|
||||
|
||||
if (!nodeRect) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const overlappingArea = getOverlappingArea(nodeRect, area);
|
||||
const partiallyVisible = partially && overlappingArea > 0;
|
||||
|
||||
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
|
||||
},
|
||||
updateNode,
|
||||
updateNodeData: (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
|
||||
);
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
...generalHelper,
|
||||
...viewportHelper,
|
||||
getNodes,
|
||||
getNode,
|
||||
getInternalNode,
|
||||
getEdges,
|
||||
getEdge,
|
||||
setNodes,
|
||||
setEdges,
|
||||
addNodes,
|
||||
addEdges,
|
||||
toObject,
|
||||
deleteElements,
|
||||
getIntersectingNodes,
|
||||
isNodeIntersecting,
|
||||
updateNode,
|
||||
updateNodeData,
|
||||
viewportInitialized,
|
||||
};
|
||||
}, [
|
||||
viewportHelper,
|
||||
getNodes,
|
||||
getNode,
|
||||
getInternalNode,
|
||||
getEdges,
|
||||
getEdge,
|
||||
setNodes,
|
||||
setEdges,
|
||||
addNodes,
|
||||
addEdges,
|
||||
toObject,
|
||||
deleteElements,
|
||||
getIntersectingNodes,
|
||||
isNodeIntersecting,
|
||||
updateNode,
|
||||
updateNodeData,
|
||||
]);
|
||||
}, [viewportInitialized]);
|
||||
}
|
||||
|
||||
@@ -7,10 +7,8 @@ import {
|
||||
rendererPointToPoint,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { useStoreApi, useStore } from '../hooks/useStore';
|
||||
import type { ViewportHelperFunctions, ReactFlowState } from '../types';
|
||||
|
||||
const selector = (s: ReactFlowState) => !!s.panZoom;
|
||||
import { useStoreApi } from '../hooks/useStore';
|
||||
import type { ViewportHelperFunctions } from '../types';
|
||||
|
||||
/**
|
||||
* Hook for getting viewport helper functions.
|
||||
@@ -20,9 +18,8 @@ const selector = (s: ReactFlowState) => !!s.panZoom;
|
||||
*/
|
||||
const useViewportHelper = (): ViewportHelperFunctions => {
|
||||
const store = useStoreApi();
|
||||
const panZoomInitialized = useStore(selector);
|
||||
|
||||
const viewportHelperFunctions = useMemo<ViewportHelperFunctions>(() => {
|
||||
return useMemo<ViewportHelperFunctions>(() => {
|
||||
return {
|
||||
zoomIn: (options) => store.getState().panZoom?.scaleBy(1.2, { duration: options?.duration }),
|
||||
zoomOut: (options) => store.getState().panZoom?.scaleBy(1 / 1.2, { duration: options?.duration }),
|
||||
@@ -117,11 +114,8 @@ const useViewportHelper = (): ViewportHelperFunctions => {
|
||||
y: rendererPosition.y + domY,
|
||||
};
|
||||
},
|
||||
viewportInitialized: panZoomInitialized,
|
||||
};
|
||||
}, [panZoomInitialized]);
|
||||
|
||||
return viewportHelperFunctions;
|
||||
}, []);
|
||||
};
|
||||
|
||||
export default useViewportHelper;
|
||||
|
||||
@@ -38,6 +38,7 @@ export * from './types';
|
||||
|
||||
// system types
|
||||
export {
|
||||
type Align,
|
||||
type SmoothStepPathOptions,
|
||||
type BezierPathOptions,
|
||||
ConnectionLineType,
|
||||
@@ -97,6 +98,7 @@ export {
|
||||
type EdgeRemoveChange,
|
||||
type EdgeAddChange,
|
||||
type EdgeReplaceChange,
|
||||
type KeyCode,
|
||||
} from '@xyflow/system';
|
||||
|
||||
// system utils
|
||||
|
||||
@@ -74,7 +74,7 @@ const createStore = ({
|
||||
// new dimensions and update the nodes.
|
||||
updateNodeInternals: (updates) => {
|
||||
const {
|
||||
onNodesChange,
|
||||
triggerNodeChanges,
|
||||
fitView,
|
||||
nodeLookup,
|
||||
parentLookup,
|
||||
@@ -120,7 +120,7 @@ const createStore = ({
|
||||
if (debug) {
|
||||
console.log('React Flow: trigger node changes', changes);
|
||||
}
|
||||
onNodesChange?.(changes);
|
||||
triggerNodeChanges?.(changes);
|
||||
}
|
||||
},
|
||||
updateNodePositions: (nodeDragItems, dragging = false) => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* 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';
|
||||
@import '../../../system/src/styles/node-resizer.css';
|
||||
|
||||
@@ -63,8 +63,8 @@ export type OnSelectionChangeParams = {
|
||||
|
||||
export type OnSelectionChangeFunc = (params: OnSelectionChangeParams) => void;
|
||||
|
||||
export type FitViewParams = FitViewParamsBase<Node>;
|
||||
export type FitViewOptions = FitViewOptionsBase<Node>;
|
||||
export type FitViewParams<NodeType extends Node = Node> = FitViewParamsBase<NodeType>;
|
||||
export type FitViewOptions<NodeType extends Node = Node> = FitViewOptionsBase<NodeType>;
|
||||
export type FitView = (fitViewOptions?: FitViewOptions) => boolean;
|
||||
export type OnInit<NodeType extends Node = Node, EdgeType extends Edge = Edge> = (
|
||||
reactFlowInstance: ReactFlowInstance<NodeType, EdgeType>
|
||||
@@ -156,7 +156,6 @@ export type ViewportHelperFunctions = {
|
||||
* const clientPosition = flowToScreenPosition({ x: node.position.x, y: node.position.y })
|
||||
*/
|
||||
flowToScreenPosition: (flowPosition: XYPosition) => XYPosition;
|
||||
viewportInitialized: boolean;
|
||||
};
|
||||
|
||||
export type OnBeforeDelete<NodeType extends Node = Node, EdgeType extends Edge = Edge> = OnBeforeDeleteBase<
|
||||
|
||||
@@ -13,115 +13,70 @@ export type DeleteElementsOptions = {
|
||||
edges?: (Edge | { id: Edge['id'] })[];
|
||||
};
|
||||
|
||||
export namespace Instance {
|
||||
export type GetNodes<NodeType extends Node = Node> = () => NodeType[];
|
||||
export type SetNodes<NodeType extends Node = Node> = (
|
||||
payload: NodeType[] | ((nodes: NodeType[]) => NodeType[])
|
||||
) => void;
|
||||
export type AddNodes<NodeType extends Node = Node> = (payload: NodeType[] | NodeType) => void;
|
||||
export type GetNode<NodeType extends Node = Node> = (id: string) => NodeType | undefined;
|
||||
export type GetInternalNode<NodeType extends Node = Node> = (id: string) => InternalNode<NodeType> | undefined;
|
||||
export type GetEdges<EdgeType extends Edge = Edge> = () => EdgeType[];
|
||||
export type SetEdges<EdgeType extends Edge = Edge> = (
|
||||
payload: EdgeType[] | ((edges: EdgeType[]) => EdgeType[])
|
||||
) => void;
|
||||
export type GetEdge<EdgeType extends Edge = Edge> = (id: string) => EdgeType | undefined;
|
||||
export type AddEdges<EdgeType extends Edge = Edge> = (payload: EdgeType[] | EdgeType) => void;
|
||||
export type ToObject<NodeType extends Node = Node, EdgeType extends Edge = Edge> = () => ReactFlowJsonObject<
|
||||
NodeType,
|
||||
EdgeType
|
||||
>;
|
||||
export type DeleteElements = (params: DeleteElementsOptions) => Promise<{
|
||||
deletedNodes: Node[];
|
||||
deletedEdges: Edge[];
|
||||
}>;
|
||||
export type GetIntersectingNodes<NodeType extends Node = Node> = (
|
||||
node: NodeType | { id: Node['id'] } | Rect,
|
||||
partially?: boolean,
|
||||
nodes?: NodeType[]
|
||||
) => NodeType[];
|
||||
export type IsNodeIntersecting<NodeType extends Node = Node> = (
|
||||
node: NodeType | { id: Node['id'] } | Rect,
|
||||
area: Rect,
|
||||
partially?: boolean
|
||||
) => boolean;
|
||||
|
||||
export type UpdateNode<NodeType extends Node = Node> = (
|
||||
id: string,
|
||||
nodeUpdate: Partial<NodeType> | ((node: NodeType) => Partial<NodeType>),
|
||||
options?: { replace: boolean }
|
||||
) => void;
|
||||
export type UpdateNodeData<NodeType extends Node = Node> = (
|
||||
id: string,
|
||||
dataUpdate: object | ((node: NodeType) => object),
|
||||
options?: { replace: boolean }
|
||||
) => void;
|
||||
}
|
||||
|
||||
export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
|
||||
export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
|
||||
/**
|
||||
* Returns nodes.
|
||||
*
|
||||
* @returns nodes array
|
||||
*/
|
||||
getNodes: Instance.GetNodes<NodeType>;
|
||||
getNodes: () => NodeType[];
|
||||
/**
|
||||
* Sets nodes.
|
||||
*
|
||||
* @param payload - the nodes to set or a function that receives the current nodes and returns the new nodes
|
||||
*/
|
||||
setNodes: Instance.SetNodes<NodeType>;
|
||||
setNodes: (payload: NodeType[] | ((nodes: NodeType[]) => NodeType[])) => void;
|
||||
/**
|
||||
* Adds nodes.
|
||||
*
|
||||
* @param payload - the nodes to add
|
||||
*/
|
||||
addNodes: Instance.AddNodes<NodeType>;
|
||||
addNodes: (payload: NodeType[] | NodeType) => void;
|
||||
/**
|
||||
* Returns a node by id.
|
||||
*
|
||||
* @param id - the node id
|
||||
* @returns the node or undefined if no node was found
|
||||
*/
|
||||
getNode: Instance.GetNode<NodeType>;
|
||||
getNode: (id: string) => NodeType | undefined;
|
||||
/**
|
||||
* Returns an internal node by id.
|
||||
*
|
||||
* @param id - the node id
|
||||
* @returns the internal node or undefined if no node was found
|
||||
*/
|
||||
getInternalNode: Instance.GetInternalNode<NodeType>;
|
||||
getInternalNode: (id: string) => InternalNode<NodeType> | undefined;
|
||||
/**
|
||||
* Returns edges.
|
||||
*
|
||||
* @returns edges array
|
||||
*/
|
||||
getEdges: Instance.GetEdges<EdgeType>;
|
||||
getEdges: () => EdgeType[];
|
||||
/**
|
||||
* Sets edges.
|
||||
*
|
||||
* @param payload - the edges to set or a function that receives the current edges and returns the new edges
|
||||
*/
|
||||
setEdges: Instance.SetEdges<EdgeType>;
|
||||
setEdges: (payload: EdgeType[] | ((edges: EdgeType[]) => EdgeType[])) => void;
|
||||
/**
|
||||
* Adds edges.
|
||||
*
|
||||
* @param payload - the edges to add
|
||||
*/
|
||||
addEdges: Instance.AddEdges<EdgeType>;
|
||||
addEdges: (payload: EdgeType[] | EdgeType) => void;
|
||||
/**
|
||||
* Returns an edge by id.
|
||||
*
|
||||
* @param id - the edge id
|
||||
* @returns the edge or undefined if no edge was found
|
||||
*/
|
||||
getEdge: Instance.GetEdge<EdgeType>;
|
||||
getEdge: (id: string) => EdgeType | undefined;
|
||||
/**
|
||||
* 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<NodeType, EdgeType>;
|
||||
toObject: () => ReactFlowJsonObject<NodeType, EdgeType>;
|
||||
/**
|
||||
* Deletes nodes and edges.
|
||||
*
|
||||
@@ -130,7 +85,10 @@ export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edg
|
||||
*
|
||||
* @returns a promise that resolves with the deleted nodes and edges
|
||||
*/
|
||||
deleteElements: Instance.DeleteElements;
|
||||
deleteElements: (params: DeleteElementsOptions) => Promise<{
|
||||
deletedNodes: Node[];
|
||||
deletedEdges: Edge[];
|
||||
}>;
|
||||
/**
|
||||
* Returns all nodes that intersect with the given node or rect.
|
||||
*
|
||||
@@ -140,7 +98,11 @@ export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edg
|
||||
*
|
||||
* @returns an array of intersecting nodes
|
||||
*/
|
||||
getIntersectingNodes: Instance.GetIntersectingNodes<NodeType>;
|
||||
getIntersectingNodes: (
|
||||
node: NodeType | { id: Node['id'] } | Rect,
|
||||
partially?: boolean,
|
||||
nodes?: NodeType[]
|
||||
) => NodeType[];
|
||||
/**
|
||||
* Checks if the given node or rect intersects with the passed rect.
|
||||
*
|
||||
@@ -150,7 +112,7 @@ export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edg
|
||||
*
|
||||
* @returns true if the node or rect intersects with the given area
|
||||
*/
|
||||
isNodeIntersecting: Instance.IsNodeIntersecting<NodeType>;
|
||||
isNodeIntersecting: (node: NodeType | { id: Node['id'] } | Rect, area: Rect, partially?: boolean) => boolean;
|
||||
/**
|
||||
* Updates a node.
|
||||
*
|
||||
@@ -161,7 +123,11 @@ export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edg
|
||||
* @example
|
||||
* updateNode('node-1', (node) => ({ position: { x: node.position.x + 10, y: node.position.y } }));
|
||||
*/
|
||||
updateNode: Instance.UpdateNode<NodeType>;
|
||||
updateNode: (
|
||||
id: string,
|
||||
nodeUpdate: Partial<NodeType> | ((node: NodeType) => Partial<NodeType>),
|
||||
options?: { replace: boolean }
|
||||
) => void;
|
||||
/**
|
||||
* Updates the data attribute of a node.
|
||||
*
|
||||
@@ -172,6 +138,17 @@ export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edg
|
||||
* @example
|
||||
* updateNodeData('node-1', { label: 'A new label' });
|
||||
*/
|
||||
updateNodeData: Instance.UpdateNodeData<NodeType>;
|
||||
viewportInitialized: boolean;
|
||||
} & Omit<ViewportHelperFunctions, 'initialized'>;
|
||||
updateNodeData: (
|
||||
id: string,
|
||||
dataUpdate: object | ((node: NodeType) => object),
|
||||
options?: { replace: boolean }
|
||||
) => void;
|
||||
};
|
||||
|
||||
export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edge = Edge> = GeneralHelpers<
|
||||
NodeType,
|
||||
EdgeType
|
||||
> &
|
||||
Omit<ViewportHelperFunctions, 'initialized'> & {
|
||||
viewportInitialized: boolean;
|
||||
};
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
NodeChange,
|
||||
NodeSelectionChange,
|
||||
EdgeSelectionChange,
|
||||
NodeRemoveChange,
|
||||
EdgeRemoveChange,
|
||||
} from '@xyflow/system';
|
||||
import type { Node, Edge, InternalNode } from '../types';
|
||||
|
||||
@@ -258,3 +260,10 @@ export function getElementsDiffChanges({
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
export function elementToRemoveChange<T extends Node | Edge>(item: T): NodeRemoveChange | EdgeRemoveChange {
|
||||
return {
|
||||
id: item.id,
|
||||
type: 'remove',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# @xyflow/svelte
|
||||
|
||||
## 0.1.0
|
||||
|
||||
This is a bigger update for Svelte Flow to keep up with the latest changes we made for React Flow and the Svelte5 rewrite. The biggest change is the separation of user nodes (type `Node`) and internal nodes (type `InternalNode`), which includes a renaming of the `node.computed` attribute to `node.measured`. In the previous versions, we stored internals in `node[internalsSymbol]`. This doesn't exist anymore, but we only add it to our internal nodes in `node.internals.`.
|
||||
|
||||
## ⚠️ Breaking
|
||||
|
||||
- rename `node.computed` to `node.measured` - this attribute only includes `width` and `height` and no `positionAbsolute` anymore. For this we added the helpers `getInternalNode` and `useInternalNode`
|
||||
- rename `node.parentNode` to `node.parentId`
|
||||
|
||||
### More updates:
|
||||
|
||||
- add `isValidConnection` for `<Handle />` component
|
||||
- add `fitViewOptions` for `<Controls />` component
|
||||
- add `getInternalNode` to `useSvelteFlow`
|
||||
- add `useInternalNode` hook
|
||||
- don't reset nodes and edges when svelte flow unmounts - thanks @darabos
|
||||
- fix node event types - thanks @RedPhoenixQ
|
||||
- make handleId and isTarget reactive - thanks @darabos
|
||||
- fix MiniMap interaction for touch devices
|
||||
- fix pane: pinch zoom on windows
|
||||
- fix nodes: return user node in node event handlers
|
||||
|
||||
## 0.0.41
|
||||
|
||||
- fix: re-observe nodes when not initialized
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xyflow/svelte",
|
||||
"version": "0.0.41",
|
||||
"version": "0.1.0",
|
||||
"description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
|
||||
"keywords": [
|
||||
"svelte",
|
||||
|
||||
@@ -53,8 +53,8 @@
|
||||
|
||||
$: edgeType = type || 'default';
|
||||
$: edgeComponent = $edgeTypes[edgeType] || BezierEdgeInternal;
|
||||
$: markerStartUrl = markerStart ? `url(#${getMarkerId(markerStart, $flowId)})` : undefined;
|
||||
$: markerEndUrl = markerEnd ? `url(#${getMarkerId(markerEnd, $flowId)})` : undefined;
|
||||
$: markerStartUrl = markerStart ? `url('#${getMarkerId(markerStart, $flowId)}')` : undefined;
|
||||
$: markerEndUrl = markerEnd ? `url('#${getMarkerId(markerEnd, $flowId)}')` : undefined;
|
||||
$: isSelectable = selectable || ($elementsSelectable && typeof selectable === 'undefined');
|
||||
|
||||
const handleEdgeSelect = useHandleEdgeSelect();
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
export let position: $$Props['position'] = Position.Top;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let isConnectable: $$Props['isConnectable'] = undefined;
|
||||
export let isValidConnection: $$Props['isValidConnection'] = undefined;
|
||||
export let onconnect: $$Props['onconnect'] = undefined;
|
||||
export let ondisconnect: $$Props['ondisconnect'] = undefined;
|
||||
// @todo implement connectablestart, connectableend
|
||||
@@ -31,12 +32,12 @@
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
|
||||
const isTarget = type === 'target';
|
||||
$: isTarget = type === 'target';
|
||||
const nodeId = getContext<string>('svelteflow__node_id');
|
||||
const connectable = getContext<Writable<boolean>>('svelteflow__node_connectable');
|
||||
$: isConnectable = isConnectable !== undefined ? isConnectable : $connectable;
|
||||
|
||||
const handleId = id || null;
|
||||
$: handleId = id || null;
|
||||
|
||||
const store = useStore();
|
||||
const {
|
||||
@@ -45,7 +46,7 @@
|
||||
nodeLookup,
|
||||
connectionRadius,
|
||||
viewport,
|
||||
isValidConnection,
|
||||
isValidConnection: isValidConnectionStore,
|
||||
lib,
|
||||
addEdge,
|
||||
onedgecreate,
|
||||
@@ -77,7 +78,7 @@
|
||||
lib: $lib,
|
||||
autoPanOnConnect: $autoPanOnConnect,
|
||||
flowId: $flowId,
|
||||
isValidConnection: $isValidConnection,
|
||||
isValidConnection: isValidConnection ?? $isValidConnectionStore,
|
||||
updateConnection,
|
||||
cancelConnection,
|
||||
panBy,
|
||||
|
||||
@@ -1,35 +1,41 @@
|
||||
<script lang="ts">
|
||||
import { getNodesBounds } from '@xyflow/system';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { getInternalNodesBounds, isNumeric, type Rect } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import { Selection } from '$lib/components/Selection';
|
||||
import drag from '$lib/actions/drag';
|
||||
import type { Node } from '$lib/types';
|
||||
import { createNodeEventDispatcher } from '$lib';
|
||||
import type { Node, NodeEventMap } from '$lib/types';
|
||||
|
||||
const store = useStore();
|
||||
const { selectionRectMode, nodes } = store;
|
||||
const { selectionRectMode, nodes, nodeLookup } = store;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
selectioncontextmenu: { nodes: Node[]; event: MouseEvent | TouchEvent };
|
||||
selectionclick: { nodes: Node[]; event: MouseEvent | TouchEvent };
|
||||
}>();
|
||||
const dispatchNodeEvent = createNodeEventDispatcher();
|
||||
const dispatch = createEventDispatcher<
|
||||
NodeEventMap & {
|
||||
selectioncontextmenu: { nodes: Node[]; event: MouseEvent | TouchEvent };
|
||||
selectionclick: { nodes: Node[]; event: MouseEvent | TouchEvent };
|
||||
}
|
||||
>();
|
||||
|
||||
$: selectedNodes = $nodes.filter((n) => n.selected);
|
||||
$: bounds = getNodesBounds(selectedNodes);
|
||||
let bounds: Rect | null = null;
|
||||
|
||||
$: if ($selectionRectMode === 'nodes') {
|
||||
bounds = getInternalNodesBounds($nodeLookup, { filter: (node) => !!node.selected });
|
||||
$nodes;
|
||||
}
|
||||
|
||||
function onContextMenu(event: MouseEvent | TouchEvent) {
|
||||
const selectedNodes = $nodes.filter((n) => n.selected);
|
||||
dispatch('selectioncontextmenu', { nodes: selectedNodes, event });
|
||||
}
|
||||
|
||||
function onClick(event: MouseEvent | TouchEvent) {
|
||||
const selectedNodes = $nodes.filter((n) => n.selected);
|
||||
dispatch('selectionclick', { nodes: selectedNodes, event });
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if selectedNodes && $selectionRectMode === 'nodes'}
|
||||
{#if $selectionRectMode === 'nodes' && bounds && isNumeric(bounds.x) && isNumeric(bounds.y)}
|
||||
<div
|
||||
class="selection-wrapper nopan"
|
||||
style="width: {bounds.width}px; height: {bounds.height}px; transform: translate({bounds.x}px, {bounds.y}px)"
|
||||
@@ -37,13 +43,13 @@
|
||||
disabled: false,
|
||||
store,
|
||||
onDrag: (event, _, __, nodes) => {
|
||||
dispatchNodeEvent('nodedrag', { event, targetNode: null, nodes });
|
||||
dispatch('nodedrag', { event, targetNode: null, nodes });
|
||||
},
|
||||
onDragStart: (event, _, __, nodes) => {
|
||||
dispatchNodeEvent('nodedragstart', { event, targetNode: null, nodes });
|
||||
dispatch('nodedragstart', { event, targetNode: null, nodes });
|
||||
},
|
||||
onDragStop: (event, _, __, nodes) => {
|
||||
dispatchNodeEvent('nodedragstop', { event, targetNode: null, nodes });
|
||||
dispatch('nodedragstop', { event, targetNode: null, nodes });
|
||||
}
|
||||
}}
|
||||
on:contextmenu={onContextMenu}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<svelte:options immutable />
|
||||
|
||||
<script lang="ts">
|
||||
import { setContext, onDestroy } from 'svelte';
|
||||
import { setContext, onDestroy, createEventDispatcher } from 'svelte';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import cc from 'classcat';
|
||||
import { errorMessages, Position } from '@xyflow/system';
|
||||
@@ -11,7 +11,7 @@
|
||||
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
|
||||
import type { NodeWrapperProps } from './types';
|
||||
import { getNodeInlineStyleDimensions } from './utils';
|
||||
import { createNodeEventDispatcher } from '$lib';
|
||||
import type { NodeEventMap } from '$lib/types';
|
||||
|
||||
interface $$Props extends NodeWrapperProps {}
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
let nodeRef: HTMLDivElement;
|
||||
let prevNodeRef: HTMLDivElement | null = null;
|
||||
|
||||
const dispatchNodeEvent = createNodeEventDispatcher();
|
||||
const dispatchNodeEvent = createEventDispatcher<NodeEventMap>();
|
||||
const connectableStore = writable(connectable);
|
||||
let prevType: string | undefined = undefined;
|
||||
let prevSourcePosition: Position | undefined = undefined;
|
||||
@@ -104,7 +104,7 @@
|
||||
{
|
||||
id,
|
||||
nodeElement: nodeRef,
|
||||
forceUpdate: true
|
||||
force: true
|
||||
}
|
||||
]
|
||||
])
|
||||
@@ -141,7 +141,7 @@
|
||||
handleNodeSelection(id);
|
||||
}
|
||||
|
||||
dispatchNodeEvent('nodeclick', { node, event });
|
||||
dispatchNodeEvent('nodeclick', { node: node.internals.userNode, event });
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Node } from '$lib/types';
|
||||
import type { InternalNode, Node } from '$lib/types';
|
||||
|
||||
export type NodeWrapperProps = Pick<
|
||||
Node,
|
||||
@@ -32,6 +32,6 @@ export type NodeWrapperProps = Pick<
|
||||
resizeObserver?: ResizeObserver | null;
|
||||
isParent?: boolean;
|
||||
zIndex: number;
|
||||
node: Node;
|
||||
node: InternalNode;
|
||||
initialized: boolean;
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
updates.set(id, {
|
||||
id,
|
||||
nodeElement: entry.target as HTMLDivElement,
|
||||
forceUpdate: true
|
||||
force: true
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import type { Node, Edge } from '$lib/types';
|
||||
import type { Node, Edge, InternalNode } from '$lib/types';
|
||||
import type { PaneProps } from './types';
|
||||
|
||||
type $$Props = PaneProps;
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let containerBounds: DOMRect | null = null;
|
||||
let selectedNodes: Node[] = [];
|
||||
let selectedNodes: InternalNode[] = [];
|
||||
|
||||
$: _panOnDrag = $panActivationKeyPressed || panOnDrag;
|
||||
$: isSelecting =
|
||||
|
||||
17
packages/svelte/src/lib/hooks/useInternalNode.ts
Normal file
17
packages/svelte/src/lib/hooks/useInternalNode.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { derived, type Readable } from 'svelte/store';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import type { InternalNode } from '$lib/types';
|
||||
|
||||
/**
|
||||
* Hook to get an internal node by id.
|
||||
*
|
||||
* @public
|
||||
* @param id - the node id
|
||||
* @returns a readable with an internal node or undefined
|
||||
*/
|
||||
export function useInternalNode(id: string): Readable<InternalNode | undefined> {
|
||||
const { nodeLookup, nodes } = useStore();
|
||||
|
||||
return derived([nodeLookup, nodes], ([nodeLookup]) => nodeLookup.get(id));
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import type { Edge, FitViewOptions, Node } from '$lib/types';
|
||||
import type { Edge, FitViewOptions, InternalNode, Node } from '$lib/types';
|
||||
import { isNode } from '$lib/utils';
|
||||
|
||||
/**
|
||||
@@ -41,6 +41,13 @@ export function useSvelteFlow(): {
|
||||
* @param options.duration - optional duration. If set, a transition will be applied
|
||||
*/
|
||||
zoomOut: ZoomInOut;
|
||||
/**
|
||||
* Returns an internal node by id.
|
||||
*
|
||||
* @param id - the node id
|
||||
* @returns the node or undefined if no node was found
|
||||
*/
|
||||
getInternalNode: (id: string) => InternalNode | undefined;
|
||||
/**
|
||||
* Returns a node by id.
|
||||
*
|
||||
@@ -280,10 +287,13 @@ export function useSvelteFlow(): {
|
||||
}
|
||||
};
|
||||
|
||||
const getInternalNode = (id: string) => get(nodeLookup).get(id);
|
||||
|
||||
return {
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
getNode: (id) => get(nodeLookup).get(id),
|
||||
getInternalNode,
|
||||
getNode: (id) => getInternalNode(id)?.internals.userNode,
|
||||
getNodes: (ids) => (ids === undefined ? get(nodes) : getElements(get(nodeLookup), ids)),
|
||||
getEdge: (id) => get(edgeLookup).get(id),
|
||||
getEdges: (ids) => (ids === undefined ? get(edges) : getElements(get(edgeLookup), ids)),
|
||||
@@ -474,14 +484,17 @@ export function useSvelteFlow(): {
|
||||
viewport
|
||||
};
|
||||
}
|
||||
|
||||
function getElements<EdgeOrNode>(lookup: Map<string, EdgeOrNode>, ids: string[]) {
|
||||
function getElements(lookup: Map<string, InternalNode>, ids: string[]): Node[];
|
||||
function getElements(lookup: Map<string, Edge>, ids: string[]): Edge[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function getElements(lookup: Map<string, any>, ids: string[]): any[] {
|
||||
const result = [];
|
||||
|
||||
for (const id of ids) {
|
||||
const element = lookup.get(id);
|
||||
const item = lookup.get(id);
|
||||
|
||||
if (element) {
|
||||
if (item) {
|
||||
const element = 'internals' in item ? item.internals?.userNode : item;
|
||||
result.push(element);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export function useUpdateNodeInternals(): UpdateNodeInternals {
|
||||
) as HTMLDivElement;
|
||||
|
||||
if (nodeElement) {
|
||||
updates.set(updateId, { id: updateId, nodeElement, forceUpdate: true });
|
||||
updates.set(updateId, { id: updateId, nodeElement, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ export type { SvelteFlowStore } from '$lib/store/types';
|
||||
|
||||
// system types
|
||||
export {
|
||||
type Align,
|
||||
type SmoothStepPathOptions,
|
||||
type BezierPathOptions,
|
||||
ConnectionLineType,
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
export let ariaLabel: $$Props['aria-label'] = undefined;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let orientation: $$Props['orientation'] = 'vertical';
|
||||
export let fitViewOptions: $$Props['fitViewOptions'] = undefined;
|
||||
|
||||
let className: $$Props['class'] = '';
|
||||
export { className as class };
|
||||
@@ -63,7 +64,7 @@
|
||||
};
|
||||
|
||||
const onFitViewHandler = () => {
|
||||
fitView();
|
||||
fitView(fitViewOptions);
|
||||
};
|
||||
|
||||
const onToggleInteractivity = () => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { HTMLButtonAttributes } from 'svelte/elements';
|
||||
import type { PanelPosition } from '@xyflow/system';
|
||||
|
||||
import type { FitViewOptions } from '$lib/types';
|
||||
|
||||
export type ControlsProps = {
|
||||
/** Position of the controls on the pane
|
||||
* @example PanelPosition.TopLeft, PanelPosition.TopRight,
|
||||
@@ -21,6 +23,7 @@ export type ControlsProps = {
|
||||
style?: string;
|
||||
class?: string;
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
fitViewOptions?: FitViewOptions;
|
||||
};
|
||||
|
||||
export type ControlButtonProps = HTMLButtonAttributes & {
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
import cc from 'classcat';
|
||||
import {
|
||||
getBoundsOfRects,
|
||||
getInternalNodesBounds,
|
||||
getNodeDimensions,
|
||||
getNodePositionWithOrigin,
|
||||
getNodesBounds,
|
||||
nodeHasDimensions
|
||||
nodeHasDimensions,
|
||||
type Rect
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
@@ -49,6 +50,7 @@
|
||||
const defaultHeight = 150;
|
||||
const {
|
||||
nodes,
|
||||
nodeLookup,
|
||||
viewport,
|
||||
width: containerWidth,
|
||||
height: containerHeight,
|
||||
@@ -71,7 +73,14 @@
|
||||
width: $containerWidth / $viewport.zoom,
|
||||
height: $containerHeight / $viewport.zoom
|
||||
};
|
||||
$: boundingRect = $nodes.length > 0 ? getBoundsOfRects(getNodesBounds($nodes), viewBB) : viewBB;
|
||||
let boundingRect: Rect = viewBB;
|
||||
|
||||
$: {
|
||||
boundingRect =
|
||||
$nodeLookup.size > 0 ? getBoundsOfRects(getInternalNodesBounds($nodeLookup), viewBB) : viewBB;
|
||||
$nodes;
|
||||
}
|
||||
|
||||
$: elementWidth = width ?? defaultWidth;
|
||||
$: elementHeight = height ?? defaultHeight;
|
||||
$: scaledWidth = boundingRect.width / elementWidth;
|
||||
@@ -122,8 +131,9 @@
|
||||
>
|
||||
{#if ariaLabel}<title id={labelledBy}>{ariaLabel}</title>{/if}
|
||||
|
||||
{#each $nodes as node (node.id)}
|
||||
{#if nodeHasDimensions(node)}
|
||||
{#each $nodes as userNode (userNode.id)}
|
||||
{@const node = $nodeLookup.get(userNode.id)}
|
||||
{#if node && nodeHasDimensions(node)}
|
||||
{@const pos = getNodePositionWithOrigin(node).positionAbsolute}
|
||||
{@const nodeDimesions = getNodeDimensions(node)}
|
||||
<MinimapNode
|
||||
|
||||
@@ -2,7 +2,7 @@ import { getContext, setContext } from 'svelte';
|
||||
import { derived, get, writable } from 'svelte/store';
|
||||
import {
|
||||
createMarkerIds,
|
||||
fitView as fitViewUtil,
|
||||
fitView as fitViewSystem,
|
||||
getElementsToRemove,
|
||||
panBy as panBySystem,
|
||||
updateNodeInternals as updateNodeInternalsSystem,
|
||||
@@ -111,8 +111,12 @@ export function createStore({
|
||||
switch (change.type) {
|
||||
case 'dimensions': {
|
||||
const measured = { ...node.measured, ...change.dimensions };
|
||||
node.width = change.dimensions?.width ?? node.width;
|
||||
node.height = change.dimensions?.height ?? node.height;
|
||||
|
||||
if (change.setAttributes) {
|
||||
node.width = change.dimensions?.width ?? node.width;
|
||||
node.height = change.dimensions?.height ?? node.height;
|
||||
}
|
||||
|
||||
node.measured = measured;
|
||||
break;
|
||||
}
|
||||
@@ -136,7 +140,7 @@ export function createStore({
|
||||
return false;
|
||||
}
|
||||
|
||||
return fitViewUtil(
|
||||
return fitViewSystem(
|
||||
{
|
||||
nodeLookup: get(store.nodeLookup),
|
||||
width: get(store.width),
|
||||
@@ -350,8 +354,6 @@ export function createStore({
|
||||
store.selectionRectMode.set(null);
|
||||
store.snapGrid.set(null);
|
||||
store.isValidConnection.set(() => true);
|
||||
store.nodes.set([]);
|
||||
store.edges.set([]);
|
||||
|
||||
unselectNodesAndEdges();
|
||||
cancelConnection();
|
||||
|
||||
@@ -45,11 +45,15 @@ export type HandleComponentProps = {
|
||||
isConnectableStart?: boolean;
|
||||
/** Should you be able to connect to this handle */
|
||||
isConnectableEnd?: boolean;
|
||||
/** Function that is called when checking if connection is valid.
|
||||
* Overrides the isValidConnection on the Flow component.
|
||||
*/
|
||||
isValidConnection?: IsValidConnection;
|
||||
onconnect?: (connections: Connection[]) => void;
|
||||
ondisconnect?: (connections: Connection[]) => void;
|
||||
};
|
||||
|
||||
export type FitViewOptions = FitViewOptionsBase<Node>;
|
||||
export type FitViewOptions<NodeType extends Node = Node> = FitViewOptionsBase<NodeType>;
|
||||
|
||||
export type OnDelete = (params: { nodes: Node[]; edges: Edge[] }) => void;
|
||||
export type OnEdgeCreate = (connection: Connection) => Edge | Connection | void;
|
||||
|
||||
@@ -45,3 +45,14 @@ export type NodeTypes = Record<
|
||||
export type DefaultNodeOptions = Partial<Omit<Node, 'id'>>;
|
||||
|
||||
export type BuiltInNode = Node<{ label: string }, 'input' | 'output' | 'default'>;
|
||||
|
||||
export type NodeEventMap = {
|
||||
nodeclick: { node: Node; event: MouseEvent | TouchEvent };
|
||||
nodecontextmenu: { node: Node; event: MouseEvent | TouchEvent };
|
||||
nodedrag: { targetNode: Node | null; nodes: Node[]; event: MouseEvent | TouchEvent };
|
||||
nodedragstart: { targetNode: Node | null; nodes: Node[]; event: MouseEvent | TouchEvent };
|
||||
nodedragstop: { targetNode: Node | null; nodes: Node[]; event: MouseEvent | TouchEvent };
|
||||
nodemouseenter: { node: Node; event: MouseEvent | TouchEvent };
|
||||
nodemouseleave: { node: Node; event: MouseEvent | TouchEvent };
|
||||
nodemousemove: { node: Node; event: MouseEvent | TouchEvent };
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { isNodeBase, isEdgeBase } from '@xyflow/system';
|
||||
|
||||
import type { Edge, Node } from '$lib/types';
|
||||
@@ -22,15 +21,3 @@ export const isNode = <NodeType extends Node = Node>(element: unknown): element
|
||||
*/
|
||||
export const isEdge = <EdgeType extends Edge = Edge>(element: unknown): element is EdgeType =>
|
||||
isEdgeBase<EdgeType>(element);
|
||||
|
||||
export const createNodeEventDispatcher = () =>
|
||||
createEventDispatcher<{
|
||||
nodeclick: { node: Node; event: MouseEvent | TouchEvent };
|
||||
nodecontextmenu: { node: Node; event: MouseEvent | TouchEvent };
|
||||
nodedrag: { targetNode: Node | null; nodes: Node[]; event: MouseEvent | TouchEvent };
|
||||
nodedragstart: { targetNode: Node | null; nodes: Node[]; event: MouseEvent | TouchEvent };
|
||||
nodedragstop: { targetNode: Node | null; nodes: Node[]; event: MouseEvent | TouchEvent };
|
||||
nodemouseenter: { node: Node; event: MouseEvent | TouchEvent };
|
||||
nodemouseleave: { node: Node; event: MouseEvent | TouchEvent };
|
||||
nodemousemove: { node: Node; event: MouseEvent | TouchEvent };
|
||||
}>();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* this gets exported as style.css and can be used for the default theming */
|
||||
@import '../../../system/src/styles/init.css';
|
||||
@import '../../../system/src/styles/base.css';
|
||||
@import '../../../system/src/styles/node-resizer.css';
|
||||
|
||||
.svelte-flow__edge-label {
|
||||
text-align: center;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xyflow/system",
|
||||
"version": "0.0.24",
|
||||
"version": "0.0.25",
|
||||
"description": "xyflow core system that powers React Flow and Svelte Flow.",
|
||||
"keywords": [
|
||||
"node-based UI",
|
||||
|
||||
@@ -61,13 +61,13 @@ export type FitViewParamsBase<NodeType extends NodeBase> = {
|
||||
nodeOrigin?: NodeOrigin;
|
||||
};
|
||||
|
||||
export type FitViewOptionsBase<NodeType extends NodeBase> = {
|
||||
export type FitViewOptionsBase<NodeType extends NodeBase = NodeBase> = {
|
||||
padding?: number;
|
||||
includeHiddenNodes?: boolean;
|
||||
minZoom?: number;
|
||||
maxZoom?: number;
|
||||
duration?: number;
|
||||
nodes?: (NodeType | { id: NodeType['id'] })[];
|
||||
nodes?: (NodeType | { id: string })[];
|
||||
};
|
||||
|
||||
export type Viewport = {
|
||||
@@ -104,7 +104,7 @@ export type D3ZoomInstance = ZoomBehavior<Element, unknown>;
|
||||
export type D3SelectionInstance = D3Selection<Element, unknown, null, undefined>;
|
||||
export type D3ZoomHandler = (this: Element, event: any, d: unknown) => void;
|
||||
|
||||
export type UpdateNodeInternals = (nodeId: string) => void;
|
||||
export type UpdateNodeInternals = (nodeId: string | string[]) => void;
|
||||
|
||||
export type PanelPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
|
||||
|
||||
|
||||
@@ -395,7 +395,7 @@ export async function getElementsToRemove<NodeType extends NodeBase = NodeBase,
|
||||
nodes: NodeType[];
|
||||
edges: EdgeType[];
|
||||
}> {
|
||||
const nodeIds = nodesToRemove.map((node) => node.id);
|
||||
const nodeIds = new Set(nodesToRemove.map((node) => node.id));
|
||||
const matchingNodes: NodeType[] = [];
|
||||
|
||||
for (const node of nodes) {
|
||||
@@ -403,7 +403,7 @@ export async function getElementsToRemove<NodeType extends NodeBase = NodeBase,
|
||||
continue;
|
||||
}
|
||||
|
||||
const isIncluded = nodeIds.includes(node.id);
|
||||
const isIncluded = nodeIds.has(node.id);
|
||||
const parentHit = !isIncluded && node.parentId && matchingNodes.find((n) => n.id === node.parentId);
|
||||
|
||||
if (isIncluded || parentHit) {
|
||||
@@ -411,13 +411,13 @@ export async function getElementsToRemove<NodeType extends NodeBase = NodeBase,
|
||||
}
|
||||
}
|
||||
|
||||
const edgeIds = edgesToRemove.map((edge) => edge.id);
|
||||
const edgeIds = new Set(edgesToRemove.map((edge) => edge.id));
|
||||
const deletableEdges = edges.filter((edge) => edge.deletable !== false);
|
||||
const connectedEdges = getConnectedEdges(matchingNodes, deletableEdges);
|
||||
const matchingEdges: EdgeType[] = connectedEdges;
|
||||
|
||||
for (const edge of deletableEdges) {
|
||||
const isIncluded = edgeIds.includes(edge.id);
|
||||
const isIncluded = edgeIds.has(edge.id);
|
||||
|
||||
if (isIncluded && !matchingEdges.find((e) => e.id === edge.id)) {
|
||||
matchingEdges.push(edge);
|
||||
|
||||
@@ -77,7 +77,7 @@ export function getDragItems<NodeType extends NodeBase>(
|
||||
// returns two params:
|
||||
// 1. the dragged node (or the first of the list, if we are dragging a node selection)
|
||||
// 2. array of selected nodes (for multi selections)
|
||||
export function getEventHandlerParams<NodeType extends NodeBase>({
|
||||
export function getEventHandlerParams<NodeType extends InternalNodeBase>({
|
||||
nodeId,
|
||||
dragItems,
|
||||
nodeLookup,
|
||||
@@ -85,11 +85,11 @@ export function getEventHandlerParams<NodeType extends NodeBase>({
|
||||
nodeId?: string;
|
||||
dragItems: Map<string, NodeDragItem>;
|
||||
nodeLookup: Map<string, NodeType>;
|
||||
}): [NodeType, NodeType[]] {
|
||||
const nodesFromDragItems: NodeType[] = [];
|
||||
}): [NodeBase, NodeBase[]] {
|
||||
const nodesFromDragItems: NodeBase[] = [];
|
||||
|
||||
for (const [id, dragItem] of dragItems) {
|
||||
const node = nodeLookup.get(id);
|
||||
const node = nodeLookup.get(id)?.internals.userNode;
|
||||
|
||||
if (node) {
|
||||
nodesFromDragItems.push({
|
||||
@@ -103,7 +103,8 @@ export function getEventHandlerParams<NodeType extends NodeBase>({
|
||||
return [nodesFromDragItems[0], nodesFromDragItems];
|
||||
}
|
||||
|
||||
const node = nodeLookup.get(nodeId)!;
|
||||
const node = nodeLookup.get(nodeId)!.internals.userNode;
|
||||
|
||||
return [
|
||||
{
|
||||
...node,
|
||||
|
||||
@@ -214,6 +214,7 @@ export function XYPanZoom({
|
||||
currentTransform.x !== viewport.x ||
|
||||
currentTransform.y !== viewport.y
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
d3ZoomInstance?.transform(d3Selection, nextTransform, null, { sync: true });
|
||||
}
|
||||
|
||||
@@ -78,10 +78,9 @@ export function createPanOnScrollHandler({
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
const currentZoom = d3Selection.property('__zoom').k || 1;
|
||||
const _isMacOs = isMacOs();
|
||||
|
||||
// macos sets ctrlKey=true for pinch gesture on a trackpad
|
||||
if (event.ctrlKey && zoomOnPinch && _isMacOs) {
|
||||
if (event.ctrlKey && zoomOnPinch) {
|
||||
const point = pointer(event);
|
||||
const pinchDelta = wheelDelta(event);
|
||||
const zoom = currentZoom * Math.pow(2, pinchDelta);
|
||||
@@ -98,7 +97,7 @@ export function createPanOnScrollHandler({
|
||||
let deltaY = panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize;
|
||||
|
||||
// this enables vertical scrolling with shift + scroll on windows
|
||||
if (!_isMacOs && event.shiftKey && panOnScrollMode !== PanOnScrollMode.Vertical) {
|
||||
if (!isMacOs() && event.shiftKey && panOnScrollMode !== PanOnScrollMode.Vertical) {
|
||||
deltaX = event.deltaY * deltaNormalize;
|
||||
deltaY = 0;
|
||||
}
|
||||
@@ -208,6 +207,7 @@ export function createPanZoomEndHandler({
|
||||
if (event.sourceEvent?.internal) {
|
||||
return;
|
||||
}
|
||||
|
||||
zoomPanValues.isZoomingOrPanning = false;
|
||||
|
||||
if (
|
||||
|
||||
@@ -76,11 +76,7 @@ export function createFilter({
|
||||
}
|
||||
|
||||
// if the pane is only movable using allowed clicks
|
||||
if (
|
||||
Array.isArray(panOnDrag) &&
|
||||
!panOnDrag.includes(event.button) &&
|
||||
(event.type === 'mousedown' || event.type === 'touchstart')
|
||||
) {
|
||||
if (Array.isArray(panOnDrag) && !panOnDrag.includes(event.button) && event.type === 'mousedown') {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { type ZoomTransform, zoomIdentity } from 'd3-zoom';
|
||||
|
||||
import { type D3SelectionInstance, type Viewport } from '../types';
|
||||
|
||||
Reference in New Issue
Block a user