Merge pull request #3993 from xyflow/next

* fix(react): ReactFlow comp ref type

* chore(changelog): update

* Refactor(types): unify node and edge handling (#3978)

* refactor(types): unify node and edge type behaviour

* chore(changelogs): update

* chore(examples): cleanup

* chore(svelte): update deps

* refactor(useNodesData): improve selector equality function (#3976)

* improved selector equality function for useNodesData

* fixed selector & useNodesData example

* added shallow-node-data as system function

* fix(safari): prevent selection of viewport (#3973)

* prevent selection of viewport

* added user-select=none to viewport-portal and moved to be over nodes

* chore(changelogs): update

* fix(react): allow edges to start from 0,0 (#3980)

* test(playwright): fix edge selection and auto pan tests

* fix(react): select nodes with selection in uncontrolled flows

* text(react): cleanup tests, add onNodesChange tests

* react(pane): position fixed

* chore(test): cleanup

* chore(react): dont use position fixed

---------

Co-authored-by: peterkogo <7165378+peterkogo@users.noreply.github.com>
This commit is contained in:
Moritz Klack
2024-03-07 13:54:58 +01:00
committed by GitHub
co-authored by peterkogo
64 changed files with 1484 additions and 705 deletions
@@ -6,7 +6,7 @@ import * as simpleflow from '../../fixtures/simpleflow';
describe('<ReactFlow />: Basic Props', () => {
describe('uses defaultNodes and defaultEdges', () => {
beforeEach(() => {
cy.mount(<ReactFlow defaultNodes={simpleflow.nodes} defaultEdges={simpleflow.edges} />);
cy.mount(<ReactFlow defaultNodes={simpleflow.nodes} defaultEdges={simpleflow.edges} nodeDragThreshold={0} />);
});
it('mounts nodes and edges', () => {
@@ -0,0 +1,138 @@
import { useCallback, useState } from 'react';
import { OnNodesChange, Panel, ReactFlow, ReactFlowProps, applyNodeChanges, useReactFlow, Node } from '@xyflow/react';
const nodes = [
{
id: '1',
data: { label: 'Node 1' },
position: { x: 0, y: 0 },
style: { width: 100, height: 40 },
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 200, y: 200 },
style: { width: 80, height: 50 },
},
];
describe('<ReactFlow />: onNodesChange', () => {
beforeEach(() => {
const onNodesChange = cy.spy().as('onNodesChange');
cy.mount(<Comp nodes={nodes} onNodesChange={onNodesChange} />).wait(500);
});
it('receive initial dimension change', () => {
const expectedChanges = nodes.map((n) => ({
type: 'dimensions',
id: n.id,
dimensions: n.style,
}));
cy.get('@onNodesChange').should('have.callCount', 1);
cy.get('@onNodesChange').should('have.calledWith', expectedChanges);
});
it('receive replace and dimension change', () => {
const expectedDimensionChanges = [
{
type: 'dimensions',
id: '1',
dimensions: { width: 200, height: 100 },
},
];
const expectedReplaceChanges = [
{
type: 'replace',
id: '1',
item: {
...nodes[0],
computed: { positionAbsolute: nodes[0].position, width: 200, height: 100 },
style: { width: 200, height: 100 },
},
},
];
cy.get('[data-id=update-btn]').click();
cy.get('@onNodesChange').should('have.callCount', 3);
cy.get('@onNodesChange').then((s) => {
// dimension change (already checked above)
// const firstCall = s.getCall(0);
// replace change
// @ts-ignore
const secondChangeCallArgs = s.getCall(1).args[0];
// dimension change
// @ts-ignore
const thirdChangeCallArgs = s.getCall(2).args[0];
expect(secondChangeCallArgs).to.be.deep.eq(expectedReplaceChanges);
expect(thirdChangeCallArgs).to.be.deep.eq(expectedDimensionChanges);
});
});
it('receive select and unselect change', () => {
const expectedSelectChanges = [{ id: '1', type: 'select', selected: true }];
const expectedUnselectChanges = [{ id: '1', type: 'select', selected: false }];
cy.get('.react-flow__node').first().click();
cy.get('@onNodesChange').should('have.calledWith', expectedSelectChanges);
cy.get('.react-flow__pane').first().click({ force: true });
cy.get('@onNodesChange').should('have.calledWith', expectedUnselectChanges);
});
it('receive position change', () => {
const endPosition = { x: 200, y: 25 };
const expectedChanges = [
{ id: '1', type: 'position', dragging: false, position: endPosition, positionAbsolute: endPosition },
];
cy.drag('.react-flow__node:first', endPosition).then(() => {
cy.get('@onNodesChange').should('have.calledWith', expectedChanges);
});
});
it('receive remove change', () => {
const expectedChanges = [{ id: '1', type: 'remove' }];
cy.get('.react-flow__node').first().click();
cy.realPress('Backspace');
cy.get('@onNodesChange').should('have.calledWith', expectedChanges);
});
});
// test specific helpers
function UpdateButton() {
const { updateNode } = useReactFlow();
const updateNodeDimensions = () => {
updateNode('1', { style: { width: 200, height: 100 } });
};
return (
<Panel position="top-right">
<button data-id="update-btn" onClick={updateNodeDimensions} id="btn">
update
</button>
</Panel>
);
}
type CompProps = ReactFlowProps & { onNodesChange: (changes: any) => void };
function Comp(props: CompProps) {
const [nodes, setNodes] = useState(props.nodes || []);
const onNodesChange: OnNodesChange<Node> = useCallback((changes) => {
props.onNodesChange(changes);
setNodes((nds) => applyNodeChanges(changes, nds));
}, []);
return (
<ReactFlow nodes={nodes} onNodesChange={onNodesChange} nodeDragThreshold={0}>
<UpdateButton />
</ReactFlow>
);
}
+18 -24
View File
@@ -49,21 +49,16 @@ describe('Basic Flow Rendering', { testIsolation: false }, () => {
it('selects one node with a selection', () => {
cy.get('body')
.type('{shift}', { release: false })
.wait(50)
.type('{Shift}', { release: false })
.get('.react-flow__pane')
.trigger('mousedown', 1, 10, { button: 0, force: true })
.trigger('mousemove', 1000, 450, { button: 0 })
.wait(50)
.trigger('mouseup', 1000, 450, { button: 0, force: true });
.trigger('mousemove', 1000, 200, { button: 0 })
.trigger('mouseup', 1000, 200, { button: 0 });
cy.wait(200);
cy.get('.react-flow__node').eq(1).should('have.class', 'selected');
cy.get('.react-flow__node').eq(0).should('have.class', 'selected');
cy.get('.react-flow__node').eq(3).should('have.not.class', 'selected');
cy.get('.react-flow__nodesselection-rect');
cy.get('body').type('{shift}', { release: true, force: true });
});
@@ -97,19 +92,19 @@ describe('Basic Flow Rendering', { testIsolation: false }, () => {
it('drags a node', () => {
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
cy.drag('.react-flow__node:first', { x: 500, y: 25 }).then(($el: any) => {
cy.drag('.react-flow__node:first', { x: 10, y: 10 }).then(($el: any) => {
const styleAfterDrag = $el.css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
// @TODO: why does this fail since react18?
// it('removes a node', () => {
// cy.get('.react-flow__node').contains('Node 1').click().should('have.class', 'selected');
// cy.get('html').type('{backspace}').wait(100);
// cy.get('.react-flow__node').should('have.length', 3);
// cy.get('.react-flow__edge').should('have.length', 1);
// });
it('removes a node', () => {
cy.get('.react-flow__node').contains('Node 1').click().should('have.class', 'selected');
cy.get('html').realPress('Backspace');
cy.get('.react-flow__node').should('have.length', 3);
cy.get('.react-flow__edge').should('have.length', 0);
});
it('connects nodes', () => {
cy.get('.react-flow__node')
@@ -125,16 +120,15 @@ describe('Basic Flow Rendering', { testIsolation: false }, () => {
.trigger('mouseup', { force: true, button: 0 });
cy.get('.react-flow__edge').as('edge');
cy.get('@edge').should('have.length', 3);
cy.get('@edge').should('have.length', 1);
});
// @TODO: why does this fail since react18?
// it('removes an edge', () => {
// cy.get('.react-flow__edge:first').click();
// cy.get('body').type('{backspace}');
it('removes an edge', () => {
cy.get('.react-flow__edge:first').click();
cy.get('html').realPress('Backspace');
// cy.get('.react-flow__edge').should('have.length', 1);
// });
cy.get('.react-flow__edge').should('have.length', 0);
});
it('drags the pane', () => {
const styleBeforeDrag = Cypress.$('.react-flow__viewport').css('transform');
@@ -59,7 +59,7 @@ function ControlledFlow({
handlers.onConnect = onConnect;
}
return <ReactFlow nodes={nodes} edges={edges} {...handlers} {...rest} />;
return <ReactFlow nodes={nodes} edges={edges} {...handlers} {...rest} nodeDragThreshold={0} />;
}
export default ControlledFlow;
+1 -1
View File
@@ -9,7 +9,7 @@ Cypress.Commands.add('drag', (selector, { x, y }) =>
const nextY: number = centerY + y;
return elementToDrag
.trigger('mousedown', { view: window })
.trigger('mousedown', { view: window, force: true })
.trigger('mousemove', nextX, nextY, { force: true })
.wait(50)
.trigger('mouseup', { view: window, force: true });
+1
View File
@@ -15,6 +15,7 @@
// Import commands.js using ES2015 syntax:
import './commands';
import 'cypress-real-events/support';
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/;
+2 -2
View File
@@ -30,8 +30,8 @@
"@types/react-dom": "^18.2.14",
"@vitejs/plugin-react": "4.1.1",
"@vitejs/plugin-react-swc": "^3.4.1",
"cypress": "12.17.3",
"cypress-real-events": "1.11.0",
"cypress": "13.6.6",
"cypress-real-events": "1.12.0",
"start-server-and-test": "^2.0.2",
"typescript": "5.2.2",
"vite": "4.5.0"
+1 -1
View File
@@ -45,7 +45,6 @@ const initialNodes: Node[] = [
data: { label: 'Node 4' },
position: { x: 400, y: 200 },
className: 'light',
connectable: false,
},
];
@@ -129,6 +128,7 @@ const BasicFlow = () => {
selectNodesOnDrag={false}
elevateEdgesOnSelect
elevateNodesOnSelect={false}
nodeDragThreshold={0}
>
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
@@ -1,4 +1,4 @@
import React, { memo, FC, CSSProperties, useCallback } from 'react';
import React, { memo, CSSProperties, useCallback } from 'react';
import { Handle, Position, NodeProps, Connection, Edge, useOnViewportChange, Viewport } from '@xyflow/react';
import type { ColorSelectorNode } from '.';
@@ -13,7 +13,7 @@ const sourceHandleStyleB: CSSProperties = {
const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params);
const ColorSelectorNode: FC<NodeProps<ColorSelectorNode['data']>> = ({ data, isConnectable }) => {
function ColorSelectorNode({ data, isConnectable }: NodeProps<ColorSelectorNode>) {
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), []);
@@ -44,6 +44,6 @@ const ColorSelectorNode: FC<NodeProps<ColorSelectorNode['data']>> = ({ data, isC
<Handle type="source" position={Position.Right} id="b" style={sourceHandleStyleB} isConnectable={isConnectable} />
</>
);
};
}
export default memo(ColorSelectorNode);
@@ -1,4 +1,4 @@
import { useState, useEffect, MouseEvent, ChangeEvent, useCallback } from 'react';
import { useState, useEffect, MouseEvent, ChangeEvent, useCallback, useRef } from 'react';
import {
ReactFlow,
MiniMap,
@@ -17,6 +17,8 @@ import {
OnBeforeDelete,
BuiltInNode,
BuiltInEdge,
NodeTypes,
ReactFlowProvider,
} from '@xyflow/react';
import ColorSelectorNode from './ColorSelectorNode';
@@ -40,11 +42,12 @@ const initBgColor = '#1A192B';
const connectionLineStyle = { stroke: '#fff' };
const snapGrid: SnapGrid = [16, 16];
const nodeTypes = {
const nodeTypes: NodeTypes = {
selectorNode: ColorSelectorNode,
};
const CustomNodeFlow = () => {
const ref = useRef(null);
const [nodes, setNodes] = useState<MyNode[]>([]);
const onNodesChange: OnNodesChange<MyNode> = useCallback(
(changes) =>
@@ -165,6 +168,7 @@ const CustomNodeFlow = () => {
minZoom={0.3}
maxZoom={2}
onBeforeDelete={onBeforeDelete}
ref={ref}
>
<MiniMap<MyNode>
nodeStrokeColor={(n: MyNode): string => {
@@ -186,4 +190,8 @@ const CustomNodeFlow = () => {
);
};
export default CustomNodeFlow;
export default () => (
<ReactFlowProvider>
<CustomNodeFlow />
</ReactFlowProvider>
);
@@ -33,6 +33,7 @@ const DragHandleFlow = () => {
edges={edges}
nodeTypes={nodeTypes}
onNodeClick={onNodeClick}
nodeDragThreshold={0}
/>
);
};
@@ -36,7 +36,7 @@ const onNodeClick = (_: ReactMouseEvent, node: Node) => console.log('click', nod
const onEdgeClick = (_: ReactMouseEvent, edge: Edge) => console.log('click', edge);
const onPaneClick = (event: ReactMouseEvent) => console.log('onPaneClick', event);
const onPaneScroll = (event?: WheelEvent) => console.log('onPaneScroll', event);
const onPaneContextMenu = (event: ReactMouseEvent) => console.log('onPaneContextMenu', event);
const onPaneContextMenu = (event: ReactMouseEvent | MouseEvent) => console.log('onPaneContextMenu', event);
const onMoveEnd = (_: TouchEvent | MouseEvent | null, viewport: Viewport) => console.log('onMoveEnd', viewport);
const InteractionFlow = () => {
@@ -80,6 +80,7 @@ const InteractionFlow = () => {
onPaneClick={captureZoomClick ? onPaneClick : undefined}
onPaneScroll={captureZoomScroll ? onPaneScroll : undefined}
onPaneContextMenu={captureZoomClick ? onPaneContextMenu : undefined}
nodeDragThreshold={0}
onMoveEnd={onMoveEnd}
>
<MiniMap />
@@ -20,8 +20,8 @@ export type ResultNode = Node<{}, 'result'>;
export type UppercaseNode = Node<{ text: string }, 'uppercase'>;
export type MyNode = TextNode | ResultNode | UppercaseNode;
export function isTextNode(node: any): node is TextNode {
return node.type === 'text';
export function isTextNode(node: any): node is TextNode | UppercaseNode {
return node.type === 'text' || node.type === 'uppercase';
}
const nodeTypes = {
@@ -27,18 +27,21 @@ const initialNodes: Node[] = [
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
className: 'light',
type: 'default',
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
className: 'light',
type: 'default',
},
{
id: '4',
data: { label: 'Node 4' },
position: { x: 400, y: 200 },
className: 'light',
type: 'default',
},
];
@@ -85,6 +88,7 @@ const UseZoomPanHelperFlow = () => {
data: {
label: `${projectedPosition.x}-${projectedPosition.y}`,
},
type: 'default',
})
);
},
@@ -106,6 +110,7 @@ const UseZoomPanHelperFlow = () => {
data: {
label: 'New Node',
},
type: 'default',
};
addNodes(newNode);
@@ -139,8 +144,8 @@ const UseZoomPanHelperFlow = () => {
const onSetNodes = () => {
setNodes([
{ id: 'a', position: { x: 0, y: 0 }, data: { label: 'Node a' } },
{ id: 'b', position: { x: 0, y: 150 }, data: { label: 'Node b' } },
{ id: 'a', type: 'default', position: { x: 0, y: 0 }, data: { label: 'Node a' } },
{ id: 'b', type: 'default', position: { x: 0, y: 150 }, data: { label: 'Node b' } },
]);
setEdges([{ id: 'a-b', source: 'a', target: 'b' }]);
@@ -3,6 +3,7 @@ import { MarkerType } from '@xyflow/react';
export default {
flowProps: {
fitView: true,
multiSelectionKeyCode: 's',
nodes: [
{
id: '1',
+1 -1
View File
@@ -15,7 +15,7 @@
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"types": ["cypress", "cypress-real-events"]
"types": ["cypress", "node", "cypress-real-events"]
},
"include": ["**/*.ts", "**/*.tsx"],
"references": [{ "path": "./tsconfig.node.json" }]
+5 -4
View File
@@ -12,8 +12,8 @@
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.1.1",
"@sveltejs/kit": "^1.27.3",
"@sveltejs/adapter-auto": "^3.1.1",
"@sveltejs/kit": "^2.5.2",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"eslint": "^8.53.0",
@@ -21,8 +21,8 @@
"eslint-plugin-svelte": "^2.35.0",
"prettier": "^3.0.3",
"prettier-plugin-svelte": "^3.0.3",
"svelte": "^4.2.2",
"svelte-check": "^3.5.2",
"svelte": "^4.2.12",
"svelte-check": "^3.6.6",
"tslib": "^2.6.2",
"typescript": "^5.2.2",
"vite": "^4.5.0"
@@ -30,6 +30,7 @@
"type": "module",
"dependencies": {
"@dagrejs/dagre": "^1.0.4",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@xyflow/svelte": "workspace:^"
}
}
@@ -1,5 +1,4 @@
<script lang="ts">
import type { ChangeEventHandler } from 'svelte/elements';
import { writable } from 'svelte/store';
import {
SvelteFlow,
@@ -1,10 +1,10 @@
<script lang="ts">
import type { Writable } from 'svelte/store';
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
import { Handle, Position, type NodeProps, type Node } from '@xyflow/svelte';
type $$Props = NodeProps;
type $$Props = NodeProps<Node<{ colorStore: Writable<string> }>>;
export let data: { colorStore: Writable<string> };
export let data: $$Props['data'];
const { colorStore } = data;
</script>
@@ -5,8 +5,8 @@
type UppercaseNodeType = Node<{ text: string }, 'uppercase'>;
type ResultNodeType = Node<{}, 'result'>;
export function isTextNode(node: any): node is TextNodeType {
return node.type === 'text';
export function isTextNode(node: any): node is TextNodeType | UppercaseNode {
return node.type === 'text' || node.type === 'uppercase';
}
export type MyNode = TextNodeType | UppercaseNodeType | ResultNodeType;
+1 -1
View File
@@ -1,5 +1,5 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/kit/vite';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
+10
View File
@@ -1,5 +1,15 @@
# @xyflow/react
## 12.0.0-next.11
## Patch changes
- fix `ref` prop for `ReactFlow` and `Handle` component
- unify `Edge` and `Node` type handling
- fix safari: prevent selection of viewport
- fix `useNodesData` hook to prevent re-renderings
- fix edges: allow start at 0,0
## 12.0.0-next.10
## ⚠️ Breaking changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@xyflow/react",
"version": "12.0.0-next.10",
"version": "12.0.0-next.11",
"description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.",
"keywords": [
"react",
@@ -111,7 +111,7 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
[edge.markerEnd, rfId]
);
if (edge.hidden || !sourceX || !sourceY || !targetX || !targetY) {
if (edge.hidden || sourceX === null || sourceY === null || targetX === null || targetY === null) {
return null;
}
@@ -212,6 +212,7 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
id={id}
source={edge.source}
target={edge.target}
type={edge.type}
selected={edge.selected}
animated={edge.animated}
label={edge.label}
@@ -1,5 +1,4 @@
import type { ComponentType } from 'react';
import type { EdgeProps, EdgeTypes } from '../../types';
import type { EdgeTypes } from '../../types';
import {
BezierEdgeInternal,
StraightEdgeInternal,
@@ -9,11 +8,11 @@ import {
} from '../Edges';
export const builtinEdgeTypes: EdgeTypes = {
default: BezierEdgeInternal as ComponentType<EdgeProps>,
straight: StraightEdgeInternal as ComponentType<EdgeProps>,
step: StepEdgeInternal as ComponentType<EdgeProps>,
smoothstep: SmoothStepEdgeInternal as ComponentType<EdgeProps>,
simplebezier: SimpleBezierEdgeInternal as ComponentType<EdgeProps>,
default: BezierEdgeInternal,
straight: StraightEdgeInternal,
step: StepEdgeInternal,
smoothstep: SmoothStepEdgeInternal,
simplebezier: SimpleBezierEdgeInternal,
};
export const nullPosition = {
+175 -177
View File
@@ -1,9 +1,11 @@
/*
* The Handle component is used to connect nodes. When the user mousedowns a handle, we start the connection process.
* The user can then drag the connection to another handle or node. When the user releases the mouse, we check if the
* connection is valid and if so, we call the onConnect callback.
*/
import { memo, HTMLAttributes, forwardRef, MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from 'react';
import {
type HTMLAttributes,
type MouseEvent as ReactMouseEvent,
type TouchEvent as ReactTouchEvent,
type ForwardedRef,
memo,
forwardRef,
} from 'react';
import cc from 'classcat';
import { shallow } from 'zustand/shallow';
import {
@@ -58,190 +60,186 @@ const connectingSelector =
};
};
const HandleComponent = forwardRef<HTMLDivElement, HandleComponentProps>(
(
{
type = 'source',
position = Position.Top,
isValidConnection,
isConnectable = true,
isConnectableStart = true,
isConnectableEnd = true,
id,
onConnect,
children,
className,
onMouseDown,
onTouchStart,
...rest
},
ref
) => {
const handleId = id || null;
const isTarget = type === 'target';
const store = useStoreApi();
const nodeId = useNodeId();
const { connectOnClick, noPanClassName, rfId } = useStore(selector, shallow);
const { connectingFrom, connectingTo, clickConnecting, isPossibleEndHandle, connectionInProcess, valid } = useStore(
connectingSelector(nodeId, handleId, type),
shallow
);
function HandleComponent(
{
type = 'source',
position = Position.Top,
isValidConnection,
isConnectable = true,
isConnectableStart = true,
isConnectableEnd = true,
id,
onConnect,
children,
className,
onMouseDown,
onTouchStart,
...rest
}: HandleComponentProps,
ref: ForwardedRef<HTMLDivElement>
) {
const handleId = id || null;
const isTarget = type === 'target';
const store = useStoreApi();
const nodeId = useNodeId();
const { connectOnClick, noPanClassName, rfId } = useStore(selector, shallow);
const { connectingFrom, connectingTo, clickConnecting, isPossibleEndHandle, connectionInProcess, valid } = useStore(
connectingSelector(nodeId, handleId, type),
shallow
);
if (!nodeId) {
store.getState().onError?.('010', errorMessages['error010']());
if (!nodeId) {
store.getState().onError?.('010', errorMessages['error010']());
}
const onConnectExtended = (params: Connection) => {
const { defaultEdgeOptions, onConnect: onConnectAction, hasDefaultEdges } = store.getState();
const edgeParams = {
...defaultEdgeOptions,
...params,
};
if (hasDefaultEdges) {
const { edges, setEdges } = store.getState();
setEdges(addEdge(edgeParams, edges));
}
const onConnectExtended = (params: Connection) => {
const { defaultEdgeOptions, onConnect: onConnectAction, hasDefaultEdges } = store.getState();
onConnectAction?.(edgeParams);
onConnect?.(edgeParams);
};
const edgeParams = {
...defaultEdgeOptions,
...params,
};
if (hasDefaultEdges) {
const { edges, setEdges } = store.getState();
setEdges(addEdge(edgeParams, edges));
}
const onPointerDown = (event: ReactMouseEvent<HTMLDivElement> | ReactTouchEvent<HTMLDivElement>) => {
if (!nodeId) {
return;
}
onConnectAction?.(edgeParams);
onConnect?.(edgeParams);
};
const isMouseTriggered = isMouseEvent(event.nativeEvent);
const onPointerDown = (event: ReactMouseEvent<HTMLDivElement> | ReactTouchEvent<HTMLDivElement>) => {
if (!nodeId) {
return;
}
if (
isConnectableStart &&
((isMouseTriggered && (event as ReactMouseEvent<HTMLDivElement>).button === 0) || !isMouseTriggered)
) {
const currentStore = store.getState();
const isMouseTriggered = isMouseEvent(event.nativeEvent);
if (
isConnectableStart &&
((isMouseTriggered && (event as ReactMouseEvent<HTMLDivElement>).button === 0) || !isMouseTriggered)
) {
const currentStore = store.getState();
XYHandle.onPointerDown(event.nativeEvent, {
autoPanOnConnect: currentStore.autoPanOnConnect,
connectionMode: currentStore.connectionMode,
connectionRadius: currentStore.connectionRadius,
domNode: currentStore.domNode,
nodes: currentStore.nodes,
lib: currentStore.lib,
isTarget,
handleId,
nodeId,
flowId: currentStore.rfId,
panBy: currentStore.panBy,
cancelConnection: currentStore.cancelConnection,
onConnectStart: currentStore.onConnectStart,
onConnectEnd: currentStore.onConnectEnd,
updateConnection: currentStore.updateConnection,
onConnect: onConnectExtended,
isValidConnection: isValidConnection || currentStore.isValidConnection,
getTransform: () => store.getState().transform,
});
}
if (isMouseTriggered) {
onMouseDown?.(event as ReactMouseEvent<HTMLDivElement>);
} else {
onTouchStart?.(event as ReactTouchEvent<HTMLDivElement>);
}
};
const onClick = (event: ReactMouseEvent) => {
const {
onClickConnectStart,
onClickConnectEnd,
connectionClickStartHandle,
connectionMode,
isValidConnection: isValidConnectionStore,
lib,
rfId: flowId,
} = store.getState();
if (!nodeId || (!connectionClickStartHandle && !isConnectableStart)) {
return;
}
if (!connectionClickStartHandle) {
onClickConnectStart?.(event.nativeEvent, { nodeId, handleId, handleType: type });
store.setState({ connectionClickStartHandle: { nodeId, type, handleId } });
return;
}
const doc = getHostForElement(event.target as HTMLElement);
const isValidConnectionHandler = isValidConnection || isValidConnectionStore;
const { connection, isValid } = XYHandle.isValid(event.nativeEvent, {
handle: {
nodeId,
id: handleId,
type,
},
connectionMode,
fromNodeId: connectionClickStartHandle.nodeId,
fromHandleId: connectionClickStartHandle.handleId || null,
fromType: connectionClickStartHandle.type,
isValidConnection: isValidConnectionHandler,
flowId,
doc,
lib,
XYHandle.onPointerDown(event.nativeEvent, {
autoPanOnConnect: currentStore.autoPanOnConnect,
connectionMode: currentStore.connectionMode,
connectionRadius: currentStore.connectionRadius,
domNode: currentStore.domNode,
nodes: currentStore.nodes,
lib: currentStore.lib,
isTarget,
handleId,
nodeId,
flowId: currentStore.rfId,
panBy: currentStore.panBy,
cancelConnection: currentStore.cancelConnection,
onConnectStart: currentStore.onConnectStart,
onConnectEnd: currentStore.onConnectEnd,
updateConnection: currentStore.updateConnection,
onConnect: onConnectExtended,
isValidConnection: isValidConnection || currentStore.isValidConnection,
getTransform: () => store.getState().transform,
});
}
if (isValid && connection) {
onConnectExtended(connection);
}
if (isMouseTriggered) {
onMouseDown?.(event as ReactMouseEvent<HTMLDivElement>);
} else {
onTouchStart?.(event as ReactTouchEvent<HTMLDivElement>);
}
};
onClickConnectEnd?.(event as unknown as MouseEvent);
const onClick = (event: ReactMouseEvent) => {
const {
onClickConnectStart,
onClickConnectEnd,
connectionClickStartHandle,
connectionMode,
isValidConnection: isValidConnectionStore,
lib,
rfId: flowId,
} = store.getState();
store.setState({ connectionClickStartHandle: null });
};
if (!nodeId || (!connectionClickStartHandle && !isConnectableStart)) {
return;
}
return (
<div
data-handleid={handleId}
data-nodeid={nodeId}
data-handlepos={position}
data-id={`${rfId}-${nodeId}-${handleId}-${type}`}
className={cc([
'react-flow__handle',
`react-flow__handle-${position}`,
'nodrag',
noPanClassName,
className,
{
source: !isTarget,
target: isTarget,
connectable: isConnectable,
connectablestart: isConnectableStart,
connectableend: isConnectableEnd,
clickconnecting: clickConnecting,
connectingfrom: connectingFrom,
connectingto: connectingTo,
valid,
// shows where you can start a connection from
// and where you can end it while connecting
connectionindicator:
isConnectable &&
(!connectionInProcess || isPossibleEndHandle) &&
(connectionInProcess ? isConnectableEnd : isConnectableStart),
},
])}
onMouseDown={onPointerDown}
onTouchStart={onPointerDown}
onClick={connectOnClick ? onClick : undefined}
ref={ref}
{...rest}
>
{children}
</div>
);
}
);
if (!connectionClickStartHandle) {
onClickConnectStart?.(event.nativeEvent, { nodeId, handleId, handleType: type });
store.setState({ connectionClickStartHandle: { nodeId, type, handleId } });
return;
}
HandleComponent.displayName = 'Handle';
const doc = getHostForElement(event.target as HTMLElement);
const isValidConnectionHandler = isValidConnection || isValidConnectionStore;
const { connection, isValid } = XYHandle.isValid(event.nativeEvent, {
handle: {
nodeId,
id: handleId,
type,
},
connectionMode,
fromNodeId: connectionClickStartHandle.nodeId,
fromHandleId: connectionClickStartHandle.handleId || null,
fromType: connectionClickStartHandle.type,
isValidConnection: isValidConnectionHandler,
flowId,
doc,
lib,
});
if (isValid && connection) {
onConnectExtended(connection);
}
onClickConnectEnd?.(event as unknown as MouseEvent);
store.setState({ connectionClickStartHandle: null });
};
return (
<div
data-handleid={handleId}
data-nodeid={nodeId}
data-handlepos={position}
data-id={`${rfId}-${nodeId}-${handleId}-${type}`}
className={cc([
'react-flow__handle',
`react-flow__handle-${position}`,
'nodrag',
noPanClassName,
className,
{
source: !isTarget,
target: isTarget,
connectable: isConnectable,
connectablestart: isConnectableStart,
connectableend: isConnectableEnd,
clickconnecting: clickConnecting,
connectingfrom: connectingFrom,
connectingto: connectingTo,
valid,
// shows where you can start a connection from
// and where you can end it while connecting
connectionindicator:
isConnectable &&
(!connectionInProcess || isPossibleEndHandle) &&
(connectionInProcess ? isConnectableEnd : isConnectableStart),
},
])}
onMouseDown={onPointerDown}
onTouchStart={onPointerDown}
onClick={connectOnClick ? onClick : undefined}
ref={ref}
{...rest}
>
{children}
</div>
);
}
/**
* The Handle component is the part of a node that can be used to connect nodes.
* The Handle component is a UI element that is used to connect nodes.
*/
export const Handle = memo(HandleComponent);
export const Handle = memo(forwardRef(HandleComponent));
@@ -1,5 +1,4 @@
import type { ComponentType } from 'react';
import type { NodeProps, XYPosition } from '@xyflow/system';
import type { XYPosition } from '@xyflow/system';
import { InputNode } from '../Nodes/InputNode';
import { DefaultNode } from '../Nodes/DefaultNode';
@@ -15,10 +14,10 @@ export const arrowKeyDiffs: Record<string, XYPosition> = {
};
export const builtinNodeTypes: NodeTypes = {
input: InputNode as ComponentType<NodeProps>,
default: DefaultNode as ComponentType<NodeProps>,
output: OutputNode as ComponentType<NodeProps>,
group: GroupNode as ComponentType<NodeProps>,
input: InputNode,
default: DefaultNode,
output: OutputNode,
group: GroupNode,
};
export function getNodeInlineStyleDimensions<NodeType extends Node = Node>(
@@ -1,13 +1,14 @@
import { Position, type NodeProps } from '@xyflow/system';
import { Position } from '@xyflow/system';
import { Handle } from '../../components/Handle';
import type { BuiltInNode, NodeProps } from '../../types/nodes';
export function DefaultNode({
data,
isConnectable,
targetPosition = Position.Top,
sourcePosition = Position.Bottom,
}: NodeProps) {
}: NodeProps<BuiltInNode>) {
return (
<>
<Handle type="target" position={targetPosition} isConnectable={isConnectable} />
@@ -1,8 +1,9 @@
import { Position, type NodeProps } from '@xyflow/system';
import { Position } from '@xyflow/system';
import { Handle } from '../../components/Handle';
import type { BuiltInNode, NodeProps } from '../../types/nodes';
export function InputNode({ data, isConnectable, sourcePosition = Position.Bottom }: NodeProps) {
export function InputNode({ data, isConnectable, sourcePosition = Position.Bottom }: NodeProps<BuiltInNode>) {
return (
<>
{data?.label}
@@ -1,8 +1,9 @@
import { Position, type NodeProps } from '@xyflow/system';
import { Position } from '@xyflow/system';
import { Handle } from '../../components/Handle';
import type { BuiltInNode, NodeProps } from '../../types/nodes';
export function OutputNode({ data, isConnectable, targetPosition = Position.Top }: NodeProps) {
export function OutputNode({ data, isConnectable, targetPosition = Position.Top }: NodeProps<BuiltInNode>) {
return (
<>
<Handle type="target" position={targetPosition} isConnectable={isConnectable} />
@@ -172,7 +172,6 @@ function GraphViewComponent<NodeType extends Node = Node, EdgeType extends Edge
containerStyle={connectionLineContainerStyle}
/>
<div className="react-flow__edgelabel-renderer" />
<div className="react-flow__viewport-portal" />
<NodeRenderer<NodeType>
nodeTypes={nodeTypes}
onNodeClick={onNodeClick}
@@ -189,6 +188,7 @@ function GraphViewComponent<NodeType extends Node = Node, EdgeType extends Edge
nodeExtent={nodeExtent}
rfId={rfId}
/>
<div className="react-flow__viewport-portal" />
</Viewport>
</FlowRenderer>
);
+4 -7
View File
@@ -128,7 +128,8 @@ export function Pane({
};
const onMouseMove = (event: ReactMouseEvent): void => {
const { userSelectionRect, edges, transform, nodeOrigin, nodes, onNodesChange, onEdgesChange } = store.getState();
const { userSelectionRect, edges, transform, nodeOrigin, nodes, triggerNodeChanges, triggerEdgeChanges } =
store.getState();
if (!isSelecting || !containerBounds.current || !userSelectionRect) {
return;
}
@@ -172,17 +173,13 @@ export function Pane({
if (prevSelectedNodesCount.current !== selectedNodeIds.size) {
prevSelectedNodesCount.current = selectedNodeIds.size;
const changes = getSelectionChanges(nodes, selectedNodeIds, true) as NodeChange[];
if (changes.length) {
onNodesChange?.(changes);
}
triggerNodeChanges(changes);
}
if (prevSelectedEdgesCount.current !== selectedEdgeIds.size) {
prevSelectedEdgesCount.current = selectedEdgeIds.size;
const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[];
if (changes.length) {
onEdgesChange?.(changes);
}
triggerEdgeChanges(changes);
}
store.setState({
@@ -9,7 +9,7 @@ import { StoreUpdater } from '../../components/StoreUpdater';
import { useColorModeClass } from '../../hooks/useColorModeClass';
import { GraphView } from '../GraphView';
import { Wrapper } from './Wrapper';
import type { Edge, Node, ReactFlowProps, ReactFlowRefType } from '../../types';
import type { Edge, Node, ReactFlowProps } from '../../types';
import { defaultViewport as initViewport, defaultNodeOrigin } from './init-values';
const wrapperStyle: CSSProperties = {
@@ -142,7 +142,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
debug,
...rest
}: ReactFlowProps<NodeType, EdgeType>,
ref: ForwardedRef<ReactFlowRefType>
ref: ForwardedRef<HTMLDivElement>
) {
const rfId = id || '1';
const colorModeClassName = useColorModeClass(colorMode);
@@ -286,4 +286,4 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
);
}
export default forwardRef(ReactFlow) as typeof ReactFlow;
export default forwardRef(ReactFlow);
+2 -8
View File
@@ -1,15 +1,9 @@
import { useCallback } from 'react';
import { shallow } from 'zustand/shallow';
import { shallowNodeData } from '@xyflow/system';
import { useStore } from '../hooks/useStore';
import type { Node } from '../types';
export interface NodeDataReturn<NodeType extends Node> {
id: string;
type: NodeType['type'];
data: NodeType['data'];
}
/**
* Hook for receiving data of one or multiple nodes
*
@@ -46,7 +40,7 @@ export function useNodesData(nodeIds: any): any {
},
[nodeIds]
),
shallow
shallowNodeData
);
return nodesData;
-1
View File
@@ -64,7 +64,6 @@ export {
SelectionMode,
type SelectionRect,
type OnError,
type NodeProps,
type NodeOrigin,
type OnSelectionDrag,
Position,
@@ -509,5 +509,3 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
*/
debug?: boolean;
}
export type ReactFlowRefType = HTMLDivElement;
+4 -4
View File
@@ -98,10 +98,10 @@ export type EdgeTextProps = HTMLAttributes<SVGElement> &
* Custom edge component props
* @public
*/
export type EdgeProps<
EdgeData extends Record<string, unknown> = Record<string, unknown>,
EdgeType extends string | undefined = string | undefined
> = Pick<Edge<EdgeData, EdgeType>, 'id' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target'> &
export type EdgeProps<EdgeType extends Edge = Edge> = Pick<
EdgeType,
'id' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target'
> &
EdgePosition &
EdgeLabelOptions & {
sourceHandleId?: string | null;
+32 -5
View File
@@ -1,3 +1,4 @@
import { ComponentType } from 'react';
import {
FitViewParamsBase,
FitViewOptionsBase,
@@ -9,13 +10,19 @@ import {
SetCenter,
FitBounds,
XYPosition,
NodeProps,
OnBeforeDeleteBase,
Connection,
} from '@xyflow/system';
import type { NodeChange, EdgeChange, Node, Edge, ReactFlowInstance, EdgeProps } from '.';
import { ComponentType } from 'react';
import type { NodeChange, EdgeChange, Node, Edge, ReactFlowInstance, EdgeProps, NodeProps } from '.';
// this is needed, to use generics + forwardRef
declare module 'react' {
// eslint-disable-next-line @typescript-eslint/ban-types
function forwardRef<T, P = {}>(
render: (props: P, ref: React.Ref<T>) => React.ReactElement | null
): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
}
export type OnNodesChange<NodeType extends Node = Node> = (changes: NodeChange<NodeType>[]) => void;
export type OnEdgesChange<EdgeType extends Edge = Edge> = (changes: EdgeChange<EdgeType>[]) => void;
@@ -27,8 +34,28 @@ export type OnDelete<NodeType extends Node = Node, EdgeType extends Edge = Edge>
edges: EdgeType[];
}) => void;
export type NodeTypes = { [key: string]: ComponentType<NodeProps> };
export type EdgeTypes = { [key: string]: ComponentType<EdgeProps> };
export type NodeTypes = Record<
string,
ComponentType<
NodeProps & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: any;
}
>
>;
export type EdgeTypes = Record<
string,
ComponentType<
EdgeProps & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: any;
}
>
>;
export type UnselectNodesAndEdgesParams = {
nodes?: Node[];
+4 -2
View File
@@ -1,5 +1,5 @@
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import type { CoordinateExtent, NodeBase, NodeOrigin, OnError } from '@xyflow/system';
import type { CoordinateExtent, NodeBase, NodeOrigin, OnError, NodeProps as NodePropsBase } from '@xyflow/system';
import { NodeTypes } from './general';
@@ -9,7 +9,7 @@ import { NodeTypes } from './general';
*/
export type Node<
NodeData extends Record<string, unknown> = Record<string, unknown>,
NodeType extends string | undefined = string | undefined
NodeType extends string = string
> = NodeBase<NodeData, NodeType> & {
style?: CSSProperties;
className?: string;
@@ -49,3 +49,5 @@ export type NodeWrapperProps<NodeType extends Node> = {
};
export type BuiltInNode = Node<{ label: string }, 'input' | 'output' | 'default'>;
export type NodeProps<NodeType extends Node = Node> = NodePropsBase<NodeType>;
+11
View File
@@ -1,5 +1,16 @@
# @xyflow/svelte
## 0.0.38
## ⚠️ Breaking changes
- `NodeProps` generic is a node and not only node data. `type $$Props = NodeProps<AppNode>`
## Patch changes
- unify `Edge` and `Node` type handling
- fix safari: prevent selection of viewport
## 0.0.37
## ⚠️ Breaking changes
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@xyflow/svelte",
"version": "0.0.37",
"version": "0.0.38",
"description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
"keywords": [
"svelte",
@@ -46,9 +46,9 @@
"classcat": "^5.0.4"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.1.0",
"@sveltejs/kit": "^1.22.6",
"@sveltejs/package": "^2.2.1",
"@sveltejs/adapter-auto": "^3.1.1",
"@sveltejs/kit": "^2.5.2",
"@sveltejs/package": "^2.2.7",
"@typescript-eslint/eslint-plugin": "^5.60.0",
"@typescript-eslint/parser": "^5.60.0",
"autoprefixer": "^10.4.15",
@@ -65,8 +65,8 @@
"postcss-rename": "^0.6.1",
"prettier": "^2.8.8",
"prettier-plugin-svelte": "^2.10.1",
"svelte": "^4.2.0",
"svelte-check": "^3.5.0",
"svelte": "^4.2.12",
"svelte-check": "^3.6.6",
"svelte-eslint-parser": "^0.32.2",
"tslib": "^2.5.3",
"typescript": "5.1.3"
@@ -51,7 +51,8 @@
edgecontextmenu: { edge: Edge; event: MouseEvent };
}>();
$: edgeComponent = $edgeTypes[type!] || BezierEdgeInternal;
const edgeType = type || 'default';
$: edgeComponent = $edgeTypes[edgeType] || BezierEdgeInternal;
$: markerStartUrl = markerStart ? `url(#${getMarkerId(markerStart, $flowId)})` : undefined;
$: markerEndUrl = markerEnd ? `url(#${getMarkerId(markerEnd, $flowId)})` : undefined;
$: isSelectable = selectable || ($elementsSelectable && typeof selectable === 'undefined');
@@ -113,6 +114,7 @@
{data}
{style}
{interactionWidth}
type={edgeType}
sourceHandleId={sourceHandle}
targetHandleId={targetHandle}
markerStart={markerStartUrl}
@@ -1,16 +1,10 @@
<svelte:options immutable />
<script lang="ts">
import {
createEventDispatcher,
setContext,
SvelteComponent,
type ComponentType,
onDestroy
} from 'svelte';
import { createEventDispatcher, setContext, onDestroy } from 'svelte';
import { get, writable } from 'svelte/store';
import cc from 'classcat';
import { errorMessages, Position, type NodeProps } from '@xyflow/system';
import { errorMessages, Position } from '@xyflow/system';
import drag from '$lib/actions/drag';
import { useStore } from '$lib/store';
@@ -71,8 +65,7 @@
console.warn('003', errorMessages['error003'](type!));
}
const nodeComponent: ComponentType<SvelteComponent<NodeProps>> =
$nodeTypes[nodeType] || DefaultNode;
const nodeComponent = $nodeTypes[nodeType] || DefaultNode;
const dispatch = createEventDispatcher<{
nodeclick: { node: Node; event: MouseEvent | TouchEvent };
nodecontextmenu: { node: Node; event: MouseEvent | TouchEvent };
@@ -211,11 +204,11 @@
{selected}
{sourcePosition}
{targetPosition}
{type}
{zIndex}
{dragging}
{dragHandle}
isConnectable={connectable}
type={nodeType}
isConnectable={$connectableStore}
positionAbsoluteX={positionX}
positionAbsoluteY={positionY}
{width}
@@ -10,6 +10,7 @@
export let id: $$Props['id'] = '';
export let source: $$Props['source'] = '';
export let target: $$Props['target'] = '';
export let type: $$Props['type'] = 'default';
export let animated: $$Props['animated'] = undefined;
export let selected: $$Props['selected'] = undefined;
export let label: $$Props['label'] = undefined;
@@ -43,6 +44,7 @@
id;
source;
target;
type;
animated;
selected;
data;
@@ -10,6 +10,7 @@
export let id: $$Props['id'] = '';
export let source: $$Props['source'] = '';
export let target: $$Props['target'] = '';
export let type: $$Props['type'] = 'smoothstep';
export let animated: $$Props['animated'] = undefined;
export let selected: $$Props['selected'] = undefined;
@@ -43,6 +44,7 @@
id;
source;
target;
type;
animated;
selected;
data;
@@ -10,6 +10,7 @@
export let id: $$Props['id'] = '';
export let source: $$Props['source'] = '';
export let target: $$Props['target'] = '';
export let type: $$Props['type'] = 'step';
export let animated: $$Props['animated'] = undefined;
export let selected: $$Props['selected'] = undefined;
@@ -44,6 +45,7 @@
id;
source;
target;
type;
animated;
selected;
data;
@@ -10,6 +10,7 @@
export let id: $$Props['id'] = '';
export let source: $$Props['source'] = '';
export let target: $$Props['target'] = '';
export let type: $$Props['type'] = 'straight';
export let animated: $$Props['animated'] = undefined;
export let selected: $$Props['selected'] = undefined;
@@ -41,6 +42,7 @@
id;
source;
target;
type;
animated;
selected;
data;
@@ -1,9 +1,10 @@
<script lang="ts">
import { Position, type NodeProps } from '@xyflow/system';
import { Position } from '@xyflow/system';
import { Handle } from '$lib/components/Handle';
import type { NodeProps } from '$lib/types';
interface $$Props extends NodeProps<{ label: string }> {}
interface $$Props extends NodeProps {}
export let data: $$Props['data'] = { label: 'Node' };
export let targetPosition: $$Props['targetPosition'] = Position.Top;
@@ -15,12 +16,12 @@
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;
export let isConnectable: $$Props['isConnectable'];
export let zIndex: $$Props['zIndex'];
// @todo: there must be a better way to do this
id;
@@ -1,7 +1,7 @@
<script lang="ts">
import type { NodeProps } from '@xyflow/system';
import type { NodeProps } from '$lib/types';
interface $$Props extends NodeProps<{}> {}
interface $$Props extends NodeProps {}
// unused props - we need to list them here in order to prevent warnings
export let id: $$Props['id'] = '';
@@ -12,12 +12,12 @@
export let sourcePosition: $$Props['sourcePosition'] = undefined;
export let targetPosition: $$Props['targetPosition'] = undefined;
export let type: $$Props['type'] = undefined;
export let zIndex: $$Props['zIndex'] = undefined;
export let dragging: $$Props['dragging'] = false;
export let dragHandle: $$Props['dragHandle'] = undefined;
export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
export let isConnectable: $$Props['isConnectable'] = undefined;
export let isConnectable: $$Props['isConnectable'];
export let zIndex: $$Props['zIndex'];
// @todo: there must be a better way to do this
id;
@@ -1,9 +1,10 @@
<script lang="ts">
import { Position, type NodeProps } from '@xyflow/system';
import { Position } from '@xyflow/system';
import type { NodeProps } from '$lib/types';
import { Handle } from '$lib/components/Handle';
interface $$Props extends NodeProps<{ label: string }> {}
interface $$Props extends NodeProps {}
export let data: $$Props['data'] = { label: 'Node' };
export let sourcePosition: $$Props['sourcePosition'] = Position.Bottom;
@@ -15,12 +16,12 @@
export let selected: $$Props['selected'] = undefined;
export let targetPosition: $$Props['targetPosition'] = undefined;
export let type: $$Props['type'] = undefined;
export let zIndex: $$Props['zIndex'] = undefined;
export let dragging: $$Props['dragging'] = false;
export let dragHandle: $$Props['dragHandle'] = undefined;
export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
export let isConnectable: $$Props['isConnectable'] = undefined;
export let isConnectable: $$Props['isConnectable'];
export let zIndex: $$Props['zIndex'];
// @todo: there must be a better way to do this
id;
@@ -1,9 +1,10 @@
<script lang="ts">
import { Position, type NodeProps } from '@xyflow/system';
import { Position } from '@xyflow/system';
import type { NodeProps } from '$lib/types';
import { Handle } from '$lib/components/Handle';
interface $$Props extends NodeProps<{ label: string }> {}
interface $$Props extends NodeProps {}
export let data: $$Props['data'] = { label: 'Node' };
export let targetPosition: $$Props['targetPosition'] = Position.Top;
@@ -15,12 +16,12 @@
export let selected: $$Props['selected'] = undefined;
export let sourcePosition: $$Props['sourcePosition'] = undefined;
export let type: $$Props['type'] = undefined;
export let zIndex: $$Props['zIndex'] = undefined;
export let dragging: $$Props['dragging'] = false;
export let dragHandle: $$Props['dragHandle'] = undefined;
export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
export let isConnectable: $$Props['isConnectable'] = undefined;
export let isConnectable: $$Props['isConnectable'];
export let zIndex: $$Props['zIndex'];
// @todo: there must be a better way to do this
id;
+2 -15
View File
@@ -1,22 +1,9 @@
import { derived, type Readable } from 'svelte/store';
import { shallowNodeData } from '@xyflow/system';
import type { Node } from '$lib/types';
import { useStore } from '$lib/store';
function areNodesDataEqual(a: any[], b: any[]) {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i].data !== b[i].data) {
return false;
}
}
return true;
}
/**
* Hook for receiving data of one or multiple nodes
*
@@ -51,7 +38,7 @@ export function useNodesData(nodeIds: any): any {
}
}
if (!areNodesDataEqual(nextNodesData, prevNodesData)) {
if (!shallowNodeData(nextNodesData, prevNodesData)) {
prevNodesData = nextNodesData;
set(isArrayOfIds ? nextNodesData : nextNodesData[0] ?? null);
}
+1 -2
View File
@@ -45,7 +45,7 @@ export type {
DefaultEdgeOptions
} from '$lib/types/edges';
export type { HandleComponentProps, FitViewOptions } from '$lib/types/general';
export type { Node, NodeTypes, DefaultNodeOptions, BuiltInNode } from '$lib/types/nodes';
export type { Node, NodeTypes, DefaultNodeOptions, BuiltInNode, NodeProps } from '$lib/types/nodes';
export type { SvelteFlowStore } from '$lib/store/types';
// system types
@@ -77,7 +77,6 @@ export {
SelectionMode,
type SelectionRect,
type OnError,
type NodeProps,
type NodeOrigin,
type OnSelectionDrag,
Position,
+20 -5
View File
@@ -49,11 +49,14 @@ export type BuiltInEdge = SmoothStepEdge | BezierEdge | StepEdge;
/**
* Custom edge component props.
*/
export type EdgeProps<
EdgeData extends Record<string, unknown> = Record<string, unknown>,
EdgeType extends string | undefined = string | undefined
> = Omit<Edge<EdgeData, EdgeType>, 'sourceHandle' | 'targetHandle' | 'type'> &
export type EdgeProps<EdgeType extends Edge = Edge> = Omit<
EdgeType,
'sourceHandle' | 'targetHandle'
> &
EdgePosition & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any;
type: string;
markerStart?: string;
markerEnd?: string;
sourceHandleId?: string | null;
@@ -103,7 +106,19 @@ export type StepEdgeProps = EdgeComponentWithPathOptions<StepPathOptions>;
*/
export type StraightEdgeProps = Omit<EdgeComponentProps, 'sourcePosition' | 'targetPosition'>;
export type EdgeTypes = Record<string, ComponentType<SvelteComponent<EdgeProps>>>;
export type EdgeTypes = Record<
string,
ComponentType<
SvelteComponent<
EdgeProps & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: any;
}
>
>
>;
export type DefaultEdgeOptions = DefaultEdgeOptionsBase<Edge>;
+22 -3
View File
@@ -1,5 +1,5 @@
import type { ComponentType, SvelteComponent } from 'svelte';
import type { NodeBase, NodeProps } from '@xyflow/system';
import type { NodeBase, NodeProps as NodePropsBase } from '@xyflow/system';
/**
* The node data structure that gets used for the nodes prop.
@@ -7,13 +7,32 @@ import type { NodeBase, NodeProps } from '@xyflow/system';
*/
export type Node<
NodeData extends Record<string, unknown> = Record<string, unknown>,
NodeType extends string | undefined = string | undefined
NodeType extends string = string
> = NodeBase<NodeData, NodeType> & {
class?: string;
style?: string;
};
export type NodeTypes = Record<string, ComponentType<SvelteComponent<NodeProps>>>;
// @todo: currently generics for nodes are not really supported
// let's fix `type: any` when we migrate to Svelte 5
export type NodeProps<NodeType extends Node = Node> = NodePropsBase<NodeType> & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: any;
};
export type NodeTypes = Record<
string,
ComponentType<
SvelteComponent<
NodeProps & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: any;
}
>
>
>;
export type DefaultNodeOptions = Partial<Omit<Node, 'id'>>;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@xyflow/system",
"version": "0.0.18",
"version": "0.0.19",
"description": "xyflow core system that powers React Flow and Svelte Flow.",
"keywords": [
"node-based UI",
+1
View File
@@ -324,6 +324,7 @@ svg.xy-flow__connectionline {
height: 100%;
left: 0;
top: 0;
user-select: none;
}
.xy-flow__minimap {
+13 -19
View File
@@ -10,7 +10,7 @@ import { Optional } from '../utils/types';
*/
export type NodeBase<
NodeData extends Record<string, unknown> = Record<string, unknown>,
NodeType extends string | undefined = string | undefined
NodeType extends string = string
> = {
/** Unique id of a node */
id: string;
@@ -82,25 +82,19 @@ export type NodeBase<
* The node data structure that gets used for the nodes prop.
*
* @public
* @param id - The id of the node.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type NodeProps<NodeData = any> = {
id: NodeBase['id'];
data: NodeData;
dragHandle: NodeBase['dragHandle'];
type: NodeBase['type'];
selected: NodeBase['selected'];
isConnectable: NodeBase['connectable'];
zIndex: NodeBase['zIndex'];
positionAbsoluteX: number;
positionAbsoluteY: number;
width?: number;
height?: number;
dragging: NodeBase['dragging'];
sourcePosition?: NodeBase['sourcePosition'];
targetPosition?: NodeBase['targetPosition'];
};
export type NodeProps<NodeType extends NodeBase> = Pick<
NodeType,
'id' | 'data' | 'width' | 'height' | 'sourcePosition' | 'targetPosition' | 'selected' | 'dragHandle'
> &
Required<Pick<NodeType, 'type' | 'dragging' | 'zIndex'>> & {
/** whether a node is connectable or not */
isConnectable: boolean;
/** position absolute x value */
positionAbsoluteX: number;
/** position absolute x value */
positionAbsoluteY: number;
};
export type NodeHandleBounds = {
source: HandleElement[] | null;
+1
View File
@@ -7,3 +7,4 @@ export * from './marker';
export * from './node-toolbar';
export * from './store';
export * from './types';
export * from './shallow-node-data';
@@ -0,0 +1,20 @@
import { NodeBase } from '../types';
type NodeData = Pick<NodeBase, 'id' | 'type' | 'data'>;
export function shallowNodeData(a: NodeData | NodeData[], b: NodeData | NodeData[]) {
const _a = Array.isArray(a) ? a : [a];
const _b = Array.isArray(b) ? b : [b];
if (_a.length !== _b.length) {
return false;
}
for (let i = 0; i < _a.length; i++) {
if (_a[i].id !== _b[i].id || _a[i].type !== _b[i].type || !Object.is(_a[i].data, _b[i].data)) {
return false;
}
}
return true;
}
+876 -326
View File
File diff suppressed because it is too large Load Diff
+6 -7
View File
@@ -30,11 +30,12 @@ test.describe('Edges', () => {
await edge1.click();
await expect(edge1).toHaveClass(/selected/);
// FIXME: I cannot get this to work
await page.keyboard.down('MetaLeft');
// we are using "s" here because Meta doesn't work for some reason
await page.keyboard.down('s');
await edge2.click();
await expect(edge2).toHaveClass(/selected/);
await page.keyboard.up('s');
await expect(edge2).toHaveClass(/selected/);
await expect(edge1).toHaveClass(/selected/);
});
});
@@ -88,8 +89,6 @@ test.describe('Edges', () => {
await expect(edge).toBeAttached();
const edgeBox = await edge.boundingBox();
await edge.click();
await expect(edge).toHaveClass(/selected/);
@@ -154,8 +153,8 @@ test.describe('Edges', () => {
await expect(edge).toBeAttached();
await expect(edge).toHaveAttribute('marker-start', 'url(#1__type=arrowclosed)');
await expect(edge).toHaveAttribute('marker-end', 'url(#1__type=arrow)');
await expect(edge).toHaveAttribute('marker-start', "url('#1__type=arrowclosed')");
await expect(edge).toHaveAttribute('marker-end', "url('#1__type=arrow')");
});
});
});
+7 -6
View File
@@ -52,7 +52,7 @@ test.describe('Pane default', () => {
});
test.describe('minZoom & maxZoom', () => {
test('minZoom works as intended', async ({ page }) => {
test('minZoom', async ({ page }) => {
const pane = page.locator(`.${FRAMEWORK}-flow__pane`);
const viewport = page.locator(`.${FRAMEWORK}-flow__viewport`);
@@ -67,7 +67,7 @@ test.describe('Pane default', () => {
expect(transformsMinZoom.scale).toBe(0.25);
});
test('maxZoom works as intended', async ({ page }) => {
test('maxZoom', async ({ page }) => {
const pane = page.locator(`.${FRAMEWORK}-flow__pane`);
const viewport = page.locator(`.${FRAMEWORK}-flow__viewport`);
@@ -84,7 +84,7 @@ test.describe('Pane default', () => {
});
test.describe('autoPan', () => {
test('autoPanOnNodeDrag works as intended', async ({ page }) => {
test('autoPanOnNodeDrag', async ({ page }) => {
const viewport = page.locator(`.${FRAMEWORK}-flow__viewport`);
const node = page.locator('[data-id="1"]');
@@ -96,7 +96,8 @@ test.describe('Pane default', () => {
await page.mouse.down();
await page.mouse.move(0, 0);
await page.waitForTimeout(500);
await page.mouse.move(100, 100);
await page.mouse.move(1000, 100, { steps: 100 });
await page.mouse.up();
const transformAfter = await getTransform(viewport);
@@ -104,7 +105,7 @@ test.describe('Pane default', () => {
await expect(transformAfter.translateY).toBeGreaterThan(transformBefore.translateY);
});
test('autoPanOnConnect works as intended', async ({ page }) => {
test('autoPanOnConnect', async ({ page }) => {
const viewport = page.locator(`.${FRAMEWORK}-flow__viewport`);
const handle = page.locator(`[data-id="1"] .${FRAMEWORK}-flow__handle`);
@@ -116,7 +117,7 @@ test.describe('Pane default', () => {
await page.mouse.down();
await page.mouse.move(0, 0);
await page.waitForTimeout(500);
await page.mouse.move(100, 100);
await page.mouse.move(100, 100, { steps: 100 });
const transformAfter = await getTransform(viewport);
+2 -2
View File
@@ -13,11 +13,11 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.40.1",
"@playwright/test": "^1.42.1",
"@types/node": "^18.7.16"
},
"dependencies": {
"@playwright/experimental-ct-react": "^1.40.1",
"@playwright/experimental-ct-react": "^1.42.1",
"@types/react": "^18.2.31",
"@types/react-dom": "^18.2.14",
"react": "^18.2.0",