chore(svelte): run prettier

This commit is contained in:
moklick
2023-02-28 12:48:52 +01:00
parent 62a6278682
commit dda21e1fd2
64 changed files with 1669 additions and 1646 deletions
+18 -18
View File
@@ -1,20 +1,20 @@
module.exports = { module.exports = {
root: true, root: true,
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
plugins: ['svelte3', '@typescript-eslint'], plugins: ['svelte3', '@typescript-eslint'],
ignorePatterns: ['*.cjs'], ignorePatterns: ['*.cjs'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
settings: { settings: {
'svelte3/typescript': () => require('typescript') 'svelte3/typescript': () => require('typescript')
}, },
parserOptions: { parserOptions: {
sourceType: 'module', sourceType: 'module',
ecmaVersion: 2020 ecmaVersion: 2020
}, },
env: { env: {
browser: true, browser: true,
es2017: true, es2017: true,
node: true node: true
} }
}; };
+6 -7
View File
@@ -1,9 +1,8 @@
{ {
"useTabs": true, "singleQuote": true,
"singleQuote": true, "trailingComma": "none",
"trailingComma": "none", "printWidth": 100,
"printWidth": 100, "plugins": ["prettier-plugin-svelte"],
"plugins": ["prettier-plugin-svelte"], "pluginSearchDirs": ["."],
"pluginSearchDirs": ["."], "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
} }
+45 -45
View File
@@ -1,47 +1,47 @@
{ {
"name": "svelte", "name": "svelte",
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"build": "svelte-kit sync && svelte-package", "build": "svelte-kit sync && svelte-package",
"prepublishOnly": "echo 'Did you mean to publish `./package/`, instead of `./`?' && exit 1", "prepublishOnly": "echo 'Did you mean to publish `./package/`, instead of `./`?' && exit 1",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --plugin-search-dir . --check . && eslint .", "lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write ." "format": "prettier --plugin-search-dir . --write ."
}, },
"type": "module", "type": "module",
"module": "index.ts", "module": "index.ts",
"exports": { "exports": {
".": "./index.js" ".": "./index.js"
}, },
"dependencies": { "dependencies": {
"@reactflow/edge-utils": "workspace:*", "@reactflow/edge-utils": "workspace:*",
"@reactflow/system": "workspace:*", "@reactflow/system": "workspace:*",
"@reactflow/utils": "workspace:*", "@reactflow/utils": "workspace:*",
"@svelte-put/shortcut": "^2.0.0", "@svelte-put/shortcut": "^2.0.0",
"classcat": "^5.0.4", "classcat": "^5.0.4",
"d3-drag": "^3.0.0", "d3-drag": "^3.0.0",
"d3-selection": "^3.0.0", "d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0", "d3-zoom": "^3.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0", "@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/kit": "^1.5.6", "@sveltejs/kit": "^1.5.6",
"@sveltejs/package": "^1.0.2", "@sveltejs/package": "^1.0.2",
"@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0", "@typescript-eslint/parser": "^5.45.0",
"eslint": "^8.28.0", "eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte3": "^4.0.0", "eslint-plugin-svelte3": "^4.0.0",
"prettier": "^2.8.0", "prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.8.1", "prettier-plugin-svelte": "^2.8.1",
"svelte": "^3.55.1", "svelte": "^3.55.1",
"svelte-check": "^3.0.1", "svelte-check": "^3.0.1",
"tslib": "^2.4.1", "tslib": "^2.4.1",
"typescript": "^4.9.3", "typescript": "^4.9.3",
"vite": "^4.1.1" "vite": "^4.1.1"
} }
} }
+6 -6
View File
@@ -1,12 +1,12 @@
// See https://kit.svelte.dev/docs/types#app // See https://kit.svelte.dev/docs/types#app
// for information about these interfaces // for information about these interfaces
declare global { declare global {
namespace App { namespace App {
// interface Error {} // interface Error {}
// interface Locals {} // interface Locals {}
// interface PageData {} // interface PageData {}
// interface Platform {} // interface Platform {}
} }
} }
export {}; export {};
+18 -18
View File
@@ -1,21 +1,21 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<style> <style>
html, html,
body, body,
#root { #root {
height: 100%; height: 100%;
margin: 0; margin: 0;
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
} }
</style> </style>
%sveltekit.head% %sveltekit.head%
</head> </head>
<body> <body>
<div id="root">%sveltekit.body%</div> <div id="root">%sveltekit.body%</div>
</body> </body>
</html> </html>
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import Handle from '$lib/components/Handle/index.svelte'; import Handle from '$lib/components/Handle/index.svelte';
import { Position } from '@reactflow/system'; import { Position } from '@reactflow/system';
export let data: { label: string } = { label: 'Node' }; export let data: { label: string } = { label: 'Node' };
export let xPos: number = 0; export let xPos: number = 0;
+86 -86
View File
@@ -9,113 +9,113 @@ import type { Node } from '$lib/types';
export type UseDragData = { dx: number; dy: number }; export type UseDragData = { dx: number; dy: number };
export type UseDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>; export type UseDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>;
export type NodeDragItem = { export type NodeDragItem = {
id: string; id: string;
position: XYPosition; position: XYPosition;
positionAbsolute: XYPosition; positionAbsolute: XYPosition;
// distance from the mouse cursor to the node when start dragging // distance from the mouse cursor to the node when start dragging
distance: XYPosition; distance: XYPosition;
width?: number | null; width?: number | null;
height?: number | null; height?: number | null;
extent?: 'parent' | CoordinateExtent; extent?: 'parent' | CoordinateExtent;
parentNode?: string; parentNode?: string;
dragging?: boolean; dragging?: boolean;
}; };
type UseDragParams = { type UseDragParams = {
handleSelector?: string; handleSelector?: string;
nodeId?: string; nodeId?: string;
updateNodePositions: (dragItems: NodeDragItem[], d: boolean, p: boolean) => void; updateNodePositions: (dragItems: NodeDragItem[], d: boolean, p: boolean) => void;
nodes: Writable<Node[]>; nodes: Writable<Node[]>;
transform: Writable<Transform>; transform: Writable<Transform>;
}; };
export default function drag( export default function drag(
nodeRef: Element, nodeRef: Element,
{ handleSelector, nodeId, updateNodePositions, nodes, transform: transformStore }: UseDragParams { handleSelector, nodeId, updateNodePositions, nodes, transform: transformStore }: UseDragParams
) { ) {
let dragging = false; let dragging = false;
let dragItems: NodeDragItem[] = []; let dragItems: NodeDragItem[] = [];
let lastPos: { x: number | null; y: number | null } = { x: null, y: null }; let lastPos: { x: number | null; y: number | null } = { x: null, y: null };
const selection = select(nodeRef); const selection = select(nodeRef);
const getPointerPosition = ({ sourceEvent }: UseDragEvent) => { const getPointerPosition = ({ sourceEvent }: UseDragEvent) => {
const x = sourceEvent.touches ? sourceEvent.touches[0].clientX : sourceEvent.clientX; const x = sourceEvent.touches ? sourceEvent.touches[0].clientX : sourceEvent.clientX;
const y = sourceEvent.touches ? sourceEvent.touches[0].clientY : sourceEvent.clientY; const y = sourceEvent.touches ? sourceEvent.touches[0].clientY : sourceEvent.clientY;
const transform = get(transformStore); const transform = get(transformStore);
const pointerPos = { const pointerPos = {
x: (x - transform[0]) / transform[2], x: (x - transform[0]) / transform[2],
y: (y - transform[1]) / transform[2] y: (y - transform[1]) / transform[2]
}; };
// we need the snapped position in order to be able to skip unnecessary drag events // we need the snapped position in order to be able to skip unnecessary drag events
return { return {
xSnapped: pointerPos.x, xSnapped: pointerPos.x,
ySnapped: pointerPos.y, ySnapped: pointerPos.y,
...pointerPos ...pointerPos
}; };
}; };
const updateNodes = ({ x, y }: XYPosition) => { const updateNodes = ({ x, y }: XYPosition) => {
let hasChange = false; let hasChange = false;
dragItems = dragItems.map((n) => { dragItems = dragItems.map((n) => {
const nextPosition = { x: x - n.distance.x, y: y - n.distance.y }; const nextPosition = { x: x - n.distance.x, y: y - n.distance.y };
const updatedPos = calcNextPosition(n, nextPosition, get(nodes) as RFNode[]); const updatedPos = calcNextPosition(n, nextPosition, get(nodes) as RFNode[]);
// we want to make sure that we only fire a change event when there is a changes // we want to make sure that we only fire a change event when there is a changes
hasChange = hasChange =
hasChange || hasChange ||
n.position.x !== updatedPos.position.x || n.position.x !== updatedPos.position.x ||
n.position.y !== updatedPos.position.y; n.position.y !== updatedPos.position.y;
n.position = updatedPos.position; n.position = updatedPos.position;
n.positionAbsolute = updatedPos.positionAbsolute; n.positionAbsolute = updatedPos.positionAbsolute;
return n; return n;
}); });
if (!hasChange) { if (!hasChange) {
return; return;
} }
updateNodePositions(dragItems, true, true); updateNodePositions(dragItems, true, true);
dragging = true; dragging = true;
}; };
const dragHandler = d3Drag() const dragHandler = d3Drag()
.on('start', (event: UseDragEvent) => { .on('start', (event: UseDragEvent) => {
const pointerPos = getPointerPosition(event); const pointerPos = getPointerPosition(event);
console.log(pointerPos); console.log(pointerPos);
lastPos = pointerPos; lastPos = pointerPos;
dragItems = getDragItems(get(nodes) as RFNode[], pointerPos, nodeId); dragItems = getDragItems(get(nodes) as RFNode[], pointerPos, nodeId);
}) })
.on('drag', (event: UseDragEvent) => { .on('drag', (event: UseDragEvent) => {
const pointerPos = getPointerPosition(event); const pointerPos = getPointerPosition(event);
// skip events without movement // skip events without movement
if ((lastPos.x !== pointerPos.xSnapped || lastPos.y !== pointerPos.ySnapped) && dragItems) { if ((lastPos.x !== pointerPos.xSnapped || lastPos.y !== pointerPos.ySnapped) && dragItems) {
lastPos = pointerPos; lastPos = pointerPos;
updateNodes(pointerPos); updateNodes(pointerPos);
} }
}) })
.on('end', (event: UseDragEvent) => { .on('end', (event: UseDragEvent) => {
dragging = false; dragging = false;
if (dragItems) { if (dragItems) {
updateNodePositions(dragItems, false, false); updateNodePositions(dragItems, false, false);
} }
}) })
.filter((event: MouseEvent) => { .filter((event: MouseEvent) => {
const target = event.target as HTMLDivElement; const target = event.target as HTMLDivElement;
const isDraggable = const isDraggable =
!event.button && !event.button &&
!hasSelector(target, '.nodrag', nodeRef) && !hasSelector(target, '.nodrag', nodeRef) &&
(!handleSelector || hasSelector(target, handleSelector, nodeRef)); (!handleSelector || hasSelector(target, handleSelector, nodeRef));
return isDraggable; return isDraggable;
}); });
selection.call(dragHandler); selection.call(dragHandler);
} }
+113 -113
View File
@@ -1,152 +1,152 @@
import type { import type {
CoordinateExtent, CoordinateExtent,
Node, Node,
NodeDragItem, NodeDragItem,
NodeInternals, NodeInternals,
NodeOrigin, NodeOrigin,
XYPosition XYPosition
} from '@reactflow/system'; } from '@reactflow/system';
import { clampPosition, isNumeric } from '../../../utils'; import { clampPosition, isNumeric } from '../../../utils';
export function isParentSelected(node: Node, nodes: Node[]): boolean { export function isParentSelected(node: Node, nodes: Node[]): boolean {
if (!node.parentNode) { if (!node.parentNode) {
return false; return false;
} }
const parentNode = nodes.find((n) => n.id === node.parentNode); const parentNode = nodes.find((n) => n.id === node.parentNode);
if (!parentNode) { if (!parentNode) {
return false; return false;
} }
if (parentNode.selected) { if (parentNode.selected) {
return true; return true;
} }
return isParentSelected(parentNode, nodes); return isParentSelected(parentNode, nodes);
} }
export function hasSelector(target: Element, selector: string, domNode: Element): boolean { export function hasSelector(target: Element, selector: string, domNode: Element): boolean {
let current = target; let current = target;
do { do {
if (current?.matches(selector)) return true; if (current?.matches(selector)) return true;
if (current === domNode) return false; if (current === domNode) return false;
current = current.parentElement as Element; current = current.parentElement as Element;
} while (current); } while (current);
return false; return false;
} }
// looks for all selected nodes and created a NodeDragItem for each of them // looks for all selected nodes and created a NodeDragItem for each of them
export function getDragItems(nodes: Node[], mousePos: XYPosition, nodeId?: string): NodeDragItem[] { export function getDragItems(nodes: Node[], mousePos: XYPosition, nodeId?: string): NodeDragItem[] {
return nodes return nodes
.filter( .filter(
(n) => (n.selected || n.id === nodeId) && (!n.parentNode || !isParentSelected(n, nodes)) (n) => (n.selected || n.id === nodeId) && (!n.parentNode || !isParentSelected(n, nodes))
) )
.map((n) => ({ .map((n) => ({
id: n.id, id: n.id,
position: n.position ? { ...n.position } : { x: 0, y: 0 }, position: n.position ? { ...n.position } : { x: 0, y: 0 },
positionAbsolute: n.positionAbsolute ? { ...n.positionAbsolute } : { x: 0, y: 0 }, positionAbsolute: n.positionAbsolute ? { ...n.positionAbsolute } : { x: 0, y: 0 },
distance: { distance: {
x: mousePos.x - (n.positionAbsolute?.x ?? 0), x: mousePos.x - (n.positionAbsolute?.x ?? 0),
y: mousePos.y - (n.positionAbsolute?.y ?? 0) y: mousePos.y - (n.positionAbsolute?.y ?? 0)
}, },
delta: { delta: {
x: 0, x: 0,
y: 0 y: 0
}, },
extent: n.extent, extent: n.extent,
parentNode: n.parentNode, parentNode: n.parentNode,
width: n.width, width: n.width,
height: n.height height: n.height
})); }));
} }
export function calcNextPosition( export function calcNextPosition(
node: NodeDragItem | Node, node: NodeDragItem | Node,
nextPosition: XYPosition, nextPosition: XYPosition,
nodes: Node[], nodes: Node[],
nodeExtent?: CoordinateExtent, nodeExtent?: CoordinateExtent,
nodeOrigin: NodeOrigin = [0, 0] nodeOrigin: NodeOrigin = [0, 0]
): { position: XYPosition; positionAbsolute: XYPosition } { ): { position: XYPosition; positionAbsolute: XYPosition } {
let currentExtent = node.extent || nodeExtent; let currentExtent = node.extent || nodeExtent;
if (node.extent === 'parent') { if (node.extent === 'parent') {
if (node.parentNode && node.width && node.height) { if (node.parentNode && node.width && node.height) {
const parent = nodes.find((n) => n.id === node.parentNode)!; const parent = nodes.find((n) => n.id === node.parentNode)!;
const { x: parentX, y: parentY } = parent.positionAbsolute!; const { x: parentX, y: parentY } = parent.positionAbsolute!;
currentExtent = currentExtent =
parent && parent &&
isNumeric(parentX) && isNumeric(parentX) &&
isNumeric(parentY) && isNumeric(parentY) &&
isNumeric(parent.width) && isNumeric(parent.width) &&
isNumeric(parent.height) isNumeric(parent.height)
? [ ? [
[parentX + node.width * nodeOrigin[0], parentY + node.height * nodeOrigin[1]], [parentX + node.width * nodeOrigin[0], parentY + node.height * nodeOrigin[1]],
[ [
parentX + parent.width! - node.width + node.width * nodeOrigin[0], parentX + parent.width! - node.width + node.width * nodeOrigin[0],
parentY + parent.height! - node.height + node.height * nodeOrigin[1] parentY + parent.height! - node.height + node.height * nodeOrigin[1]
] ]
] ]
: currentExtent; : currentExtent;
} else { } else {
currentExtent = nodeExtent; currentExtent = nodeExtent;
} }
} else if (node.extent && node.parentNode) { } else if (node.extent && node.parentNode) {
const parent = nodes.find((n) => n.id === node.parentNode)!; const parent = nodes.find((n) => n.id === node.parentNode)!;
const { x: parentX, y: parentY } = parent.positionAbsolute!; const { x: parentX, y: parentY } = parent.positionAbsolute!;
currentExtent = [ currentExtent = [
[node.extent[0][0] + parentX, node.extent[0][1] + parentY], [node.extent[0][0] + parentX, node.extent[0][1] + parentY],
[node.extent[1][0] + parentX, node.extent[1][1] + parentY] [node.extent[1][0] + parentX, node.extent[1][1] + parentY]
]; ];
} }
let parentPosition = { x: 0, y: 0 }; let parentPosition = { x: 0, y: 0 };
if (node.parentNode) { if (node.parentNode) {
const parent = nodes.find((n) => n.id === node.parentNode)!; const parent = nodes.find((n) => n.id === node.parentNode)!;
parentPosition = parent.positionAbsolute!; parentPosition = parent.positionAbsolute!;
} }
const positionAbsolute = currentExtent const positionAbsolute = currentExtent
? clampPosition(nextPosition, currentExtent as CoordinateExtent) ? clampPosition(nextPosition, currentExtent as CoordinateExtent)
: nextPosition; : nextPosition;
return { return {
position: { position: {
x: positionAbsolute.x - parentPosition.x, x: positionAbsolute.x - parentPosition.x,
y: positionAbsolute.y - parentPosition.y y: positionAbsolute.y - parentPosition.y
}, },
positionAbsolute positionAbsolute
}; };
} }
// returns two params: // returns two params:
// 1. the dragged node (or the first of the list, if we are dragging a node selection) // 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) // 2. array of selected nodes (for multi selections)
export function getEventHandlerParams({ export function getEventHandlerParams({
nodeId, nodeId,
dragItems, dragItems,
nodeInternals nodeInternals
}: { }: {
nodeId?: string; nodeId?: string;
dragItems: NodeDragItem[]; dragItems: NodeDragItem[];
nodeInternals: NodeInternals; nodeInternals: NodeInternals;
}): [Node, Node[]] { }): [Node, Node[]] {
const extentedDragItems: Node[] = dragItems.map((n) => { const extentedDragItems: Node[] = dragItems.map((n) => {
const node = nodeInternals.get(n.id)!; const node = nodeInternals.get(n.id)!;
return { return {
...node, ...node,
position: n.position, position: n.position,
positionAbsolute: n.positionAbsolute positionAbsolute: n.positionAbsolute
}; };
}); });
return [ return [
nodeId ? extentedDragItems.find((n) => n.id === nodeId)! : extentedDragItems[0], nodeId ? extentedDragItems.find((n) => n.id === nodeId)! : extentedDragItems[0],
extentedDragItems extentedDragItems
]; ];
} }
+11 -11
View File
@@ -1,15 +1,15 @@
export default function (node: Element, target = 'body') { export default function (node: Element, target = 'body') {
const targetEl = document.querySelector(target); const targetEl = document.querySelector(target);
if (targetEl) { if (targetEl) {
targetEl.appendChild(node); targetEl.appendChild(node);
} }
return { return {
destroy() { destroy() {
if (node.parentNode) { if (node.parentNode) {
node.parentNode.removeChild(node); node.parentNode.removeChild(node);
} }
} }
}; };
} }
+84 -84
View File
@@ -6,114 +6,114 @@ import type { D3ZoomEvent } from 'd3-zoom';
import type { D3SelectionInstance, D3ZoomInstance, Transform } from '@reactflow/system'; import type { D3SelectionInstance, D3ZoomInstance, Transform } from '@reactflow/system';
const isWrappedWithClass = (event: any, className: string | undefined) => const isWrappedWithClass = (event: any, className: string | undefined) =>
event.target.closest(`.${className}`); event.target.closest(`.${className}`);
function filter(event: any, params: ZoomParams): boolean { function filter(event: any, params: ZoomParams): boolean {
const zoomScroll = true; const zoomScroll = true;
const pinchZoom = true; const pinchZoom = true;
if ( if (
event.button === 1 && event.button === 1 &&
event.type === 'mousedown' && event.type === 'mousedown' &&
(isWrappedWithClass(event, 'react-flow__node') || isWrappedWithClass(event, 'react-flow__edge')) (isWrappedWithClass(event, 'react-flow__node') || isWrappedWithClass(event, 'react-flow__edge'))
) { ) {
return true; return true;
} }
// if all interactions are disabled, we prevent all zoom events // if all interactions are disabled, we prevent all zoom events
// if (!panOnDrag && !zoomScroll && !panOnScroll && !zoomOnDoubleClick && !zoomOnPinch) { // if (!panOnDrag && !zoomScroll && !panOnScroll && !zoomOnDoubleClick && !zoomOnPinch) {
// return false; // return false;
// } // }
// // during a selection we prevent all other interactions // // during a selection we prevent all other interactions
if (params.selecting) { if (params.selecting) {
return false; return false;
} }
// // if zoom on double click is disabled, we prevent the double click event // // if zoom on double click is disabled, we prevent the double click event
// if (!zoomOnDoubleClick && event.type === 'dblclick') { // if (!zoomOnDoubleClick && event.type === 'dblclick') {
// return false; // return false;
// } // }
// // if the target element is inside an element with the nowheel class, we prevent zooming // // if the target element is inside an element with the nowheel class, we prevent zooming
// if (isWrappedWithClass(event, noWheelClassName) && event.type === 'wheel') { // if (isWrappedWithClass(event, noWheelClassName) && event.type === 'wheel') {
// return false; // return false;
// } // }
// // if the target element is inside an element with the nopan class, we prevent panning // // if the target element is inside an element with the nopan class, we prevent panning
if (isWrappedWithClass(event, 'nopan') && event.type !== 'wheel') { if (isWrappedWithClass(event, 'nopan') && event.type !== 'wheel') {
return false; return false;
} }
// if (!zoomOnPinch && event.ctrlKey && event.type === 'wheel') { // if (!zoomOnPinch && event.ctrlKey && event.type === 'wheel') {
// return false; // return false;
// } // }
// // when there is no scroll handling enabled, we prevent all wheel events // // when there is no scroll handling enabled, we prevent all wheel events
// if (!zoomScroll && !panOnScroll && !pinchZoom && event.type === 'wheel') { // if (!zoomScroll && !panOnScroll && !pinchZoom && event.type === 'wheel') {
// return false; // return false;
// } // }
// // if the pane is not movable, we prevent dragging it with mousestart or touchstart // // if the pane is not movable, we prevent dragging it with mousestart or touchstart
// if (!panOnDrag && (event.type === 'mousedown' || event.type === 'touchstart')) { // if (!panOnDrag && (event.type === 'mousedown' || event.type === 'touchstart')) {
// return false; // return false;
// } // }
// // if the pane is only movable using allowed clicks // // if the pane is only movable using allowed clicks
// if ( // if (
// Array.isArray(panOnDrag) && // Array.isArray(panOnDrag) &&
// !panOnDrag.includes(event.button) && // !panOnDrag.includes(event.button) &&
// (event.type === 'mousedown' || event.type === 'touchstart') // (event.type === 'mousedown' || event.type === 'touchstart')
// ) { // ) {
// return false; // return false;
// } // }
// // We only allow right clicks if pan on drag is set to right click // // We only allow right clicks if pan on drag is set to right click
// const buttonAllowed = // const buttonAllowed =
// (Array.isArray(panOnDrag) && panOnDrag.includes(event.button)) || // (Array.isArray(panOnDrag) && panOnDrag.includes(event.button)) ||
// !event.button || // !event.button ||
// event.button <= 1; // event.button <= 1;
// default filter for d3-zoom // default filter for d3-zoom
return ((!event.ctrlKey || event.type === 'wheel') && !event.button) || event.button <= 1; return ((!event.ctrlKey || event.type === 'wheel') && !event.button) || event.button <= 1;
} }
type ZoomParams = { type ZoomParams = {
transform: Writable<Transform>; transform: Writable<Transform>;
selecting: boolean; selecting: boolean;
d3: Writable<{ zoom: D3ZoomInstance | null; selection: D3SelectionInstance | null }>; d3: Writable<{ zoom: D3ZoomInstance | null; selection: D3SelectionInstance | null }>;
}; };
export default function zoom(domNode: Element, params: ZoomParams) { export default function zoom(domNode: Element, params: ZoomParams) {
const { transform, d3 } = params; const { transform, d3 } = params;
const d3ZoomInstance = d3Zoom(); const d3ZoomInstance = d3Zoom();
const selection = select(domNode).call(d3ZoomInstance); const selection = select(domNode).call(d3ZoomInstance);
const d3ZoomHandler = selection.on('wheel.zoom'); const d3ZoomHandler = selection.on('wheel.zoom');
d3ZoomInstance.transform(selection, zoomIdentity); d3ZoomInstance.transform(selection, zoomIdentity);
selection.on('wheel.zoom', function (event: any, d: any) { selection.on('wheel.zoom', function (event: any, d: any) {
if (isWrappedWithClass(event, 'nowheel')) { if (isWrappedWithClass(event, 'nowheel')) {
return null; return null;
} }
event.preventDefault(); event.preventDefault();
d3ZoomHandler!.call(this, event, d); d3ZoomHandler!.call(this, event, d);
}); });
d3.set({ d3.set({
zoom: d3ZoomInstance, zoom: d3ZoomInstance,
selection selection
}); });
d3ZoomInstance.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => { d3ZoomInstance.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
transform.set([event.transform.x, event.transform.y, event.transform.k]); transform.set([event.transform.x, event.transform.y, event.transform.k]);
}); });
d3ZoomInstance.filter((event: any) => filter(event, params)); d3ZoomInstance.filter((event: any) => filter(event, params));
return { return {
update(params: ZoomParams) { update(params: ZoomParams) {
d3ZoomInstance.filter((event: any) => filter(event, params)); d3ZoomInstance.filter((event: any) => filter(event, params));
} }
}; };
} }
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import cc from 'classcat'; import cc from 'classcat';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import type { ConnectionLineProps } from '$lib/types'; import type { ConnectionLineProps } from '$lib/types';
type $$Props = ConnectionLineProps; type $$Props = ConnectionLineProps;
@@ -10,11 +10,7 @@
</script> </script>
{#if $connectionPath} {#if $connectionPath}
<svg <svg width={$width} height={$height} class="react-flow__connectionline">
width={$width}
height={$height}
class="react-flow__connectionline"
>
<g class={cc(['react-flow__connection', $connection.status])}> <g class={cc(['react-flow__connection', $connection.status])}>
<path d={$connectionPath} fill="none" class="react-flow__connection-path" /> <path d={$connectionPath} fill="none" class="react-flow__connection-path" />
</g> </g>
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import portal from '$lib/actions/portal'; import portal from '$lib/actions/portal';
type $$Props = {} type $$Props = {};
</script> </script>
<div use:portal={'.react-flow__edgelabel-renderer'}> <div use:portal={'.react-flow__edgelabel-renderer'}>
@@ -1,210 +1,210 @@
import { import {
getHostForElement, getHostForElement,
calcAutoPan, calcAutoPan,
getEventPosition, getEventPosition,
pointToRendererPoint, pointToRendererPoint,
rendererPointToPoint rendererPointToPoint
} from '@reactflow/utils'; } from '@reactflow/utils';
import type { import type {
OnConnect, OnConnect,
HandleType, HandleType,
Connection, Connection,
ConnectionMode, ConnectionMode,
Node, Node,
XYPosition, XYPosition,
Transform Transform
} from '@reactflow/system'; } from '@reactflow/system';
import { import {
getClosestHandle, getClosestHandle,
getConnectionStatus, getConnectionStatus,
getHandleLookup, getHandleLookup,
getHandleType, getHandleType,
isValidHandle, isValidHandle,
resetRecentHandle, resetRecentHandle,
type ConnectionHandle, type ConnectionHandle,
type ValidConnectionFunc type ValidConnectionFunc
} from './utils'; } from './utils';
import { get, type Writable } from 'svelte/store'; import { get, type Writable } from 'svelte/store';
import type { ConnectionData } from '$lib/types'; import type { ConnectionData } from '$lib/types';
export function handlePointerDown({ export function handlePointerDown({
event, event,
handleId, handleId,
nodeId, nodeId,
onConnect, onConnect,
domNode, domNode,
nodes, nodes,
connectionMode, connectionMode,
connectionRadius, connectionRadius,
isTarget, isTarget,
transform: transformStore, transform: transformStore,
panBy, panBy,
updateConnection, updateConnection,
cancelConnection, cancelConnection,
isValidConnection, isValidConnection,
edgeUpdaterType, edgeUpdaterType,
onEdgeUpdateEnd onEdgeUpdateEnd
}: { }: {
event: MouseEvent | TouchEvent; event: MouseEvent | TouchEvent;
handleId: string | null; handleId: string | null;
nodeId: string; nodeId: string;
onConnect: OnConnect; onConnect: OnConnect;
isTarget: boolean; isTarget: boolean;
connectionMode: ConnectionMode; connectionMode: ConnectionMode;
domNode: HTMLDivElement | null; domNode: HTMLDivElement | null;
nodes: Node[]; nodes: Node[];
connectionRadius: number; connectionRadius: number;
isValidConnection: ValidConnectionFunc; isValidConnection: ValidConnectionFunc;
transform: Writable<Transform>; transform: Writable<Transform>;
updateConnection: (connection: Partial<ConnectionData>) => void; updateConnection: (connection: Partial<ConnectionData>) => void;
cancelConnection: () => void; cancelConnection: () => void;
panBy: (delta: XYPosition) => void; panBy: (delta: XYPosition) => void;
edgeUpdaterType?: HandleType; edgeUpdaterType?: HandleType;
onEdgeUpdateEnd?: (evt: MouseEvent | TouchEvent) => void; onEdgeUpdateEnd?: (evt: MouseEvent | TouchEvent) => void;
}): void { }): void {
// when react-flow is used inside a shadow root we can't use document // when react-flow is used inside a shadow root we can't use document
const doc = getHostForElement(event.target as HTMLElement); const doc = getHostForElement(event.target as HTMLElement);
let autoPanId = 0; let autoPanId = 0;
let prevClosestHandle: ConnectionHandle | null; let prevClosestHandle: ConnectionHandle | null;
const { x, y } = getEventPosition(event); const { x, y } = getEventPosition(event);
const clickedHandle = doc?.elementFromPoint(x, y); const clickedHandle = doc?.elementFromPoint(x, y);
const handleType = getHandleType(edgeUpdaterType, clickedHandle); const handleType = getHandleType(edgeUpdaterType, clickedHandle);
const containerBounds = domNode?.getBoundingClientRect(); const containerBounds = domNode?.getBoundingClientRect();
if (!containerBounds || !handleType) { if (!containerBounds || !handleType) {
return; return;
} }
let prevActiveHandle: Element; let prevActiveHandle: Element;
let connectionPosition = getEventPosition(event, containerBounds); let connectionPosition = getEventPosition(event, containerBounds);
let autoPanStarted = false; let autoPanStarted = false;
let connection: Connection | null = null; let connection: Connection | null = null;
let isValid = false; let isValid = false;
let handleDomNode: Element | null = null; let handleDomNode: Element | null = null;
const autoPanOnConnect = true; const autoPanOnConnect = true;
const handleLookup = getHandleLookup({ const handleLookup = getHandleLookup({
nodes, nodes,
nodeId, nodeId,
handleId, handleId,
handleType handleType
}); });
// when the user is moving the mouse close to the edge of the canvas while connecting we move the canvas // when the user is moving the mouse close to the edge of the canvas while connecting we move the canvas
const autoPan = (): void => { const autoPan = (): void => {
// @todd add prop // @todd add prop
if (!autoPanOnConnect) { if (!autoPanOnConnect) {
return; return;
} }
const [xMovement, yMovement] = calcAutoPan(connectionPosition, containerBounds); const [xMovement, yMovement] = calcAutoPan(connectionPosition, containerBounds);
panBy({ x: xMovement, y: yMovement }); panBy({ x: xMovement, y: yMovement });
autoPanId = requestAnimationFrame(autoPan); autoPanId = requestAnimationFrame(autoPan);
}; };
updateConnection({ updateConnection({
position: connectionPosition, position: connectionPosition,
nodeId, nodeId,
handleId, handleId,
handleType, handleType,
status: null status: null
}); });
// @todo add prop // @todo add prop
// onConnectStart?.(event, { nodeId, handleId, handleType }); // onConnectStart?.(event, { nodeId, handleId, handleType });
function onPointerMove(event: MouseEvent | TouchEvent) { function onPointerMove(event: MouseEvent | TouchEvent) {
const transform = get(transformStore); const transform = get(transformStore);
connectionPosition = getEventPosition(event, containerBounds); connectionPosition = getEventPosition(event, containerBounds);
prevClosestHandle = getClosestHandle( prevClosestHandle = getClosestHandle(
pointToRendererPoint(connectionPosition, transform, false, [1, 1]), pointToRendererPoint(connectionPosition, transform, false, [1, 1]),
connectionRadius, connectionRadius,
handleLookup handleLookup
); );
if (!autoPanStarted) { if (!autoPanStarted) {
autoPan(); autoPan();
autoPanStarted = true; autoPanStarted = true;
} }
const result = isValidHandle( const result = isValidHandle(
event, event,
prevClosestHandle, prevClosestHandle,
connectionMode, connectionMode,
nodeId, nodeId,
handleId, handleId,
isTarget ? 'target' : 'source', isTarget ? 'target' : 'source',
isValidConnection, isValidConnection,
doc doc
); );
handleDomNode = result.handleDomNode; handleDomNode = result.handleDomNode;
connection = result.connection; connection = result.connection;
isValid = result.isValid; isValid = result.isValid;
updateConnection({ updateConnection({
position: position:
prevClosestHandle && isValid prevClosestHandle && isValid
? rendererPointToPoint( ? rendererPointToPoint(
{ {
x: prevClosestHandle.x, x: prevClosestHandle.x,
y: prevClosestHandle.y y: prevClosestHandle.y
}, },
transform transform
) )
: connectionPosition, : connectionPosition,
status: getConnectionStatus(!!prevClosestHandle, isValid) status: getConnectionStatus(!!prevClosestHandle, isValid)
}); });
if (!prevClosestHandle && !isValid && !handleDomNode) { if (!prevClosestHandle && !isValid && !handleDomNode) {
return resetRecentHandle(prevActiveHandle); return resetRecentHandle(prevActiveHandle);
} }
if (connection.source !== connection.target && handleDomNode) { if (connection.source !== connection.target && handleDomNode) {
resetRecentHandle(prevActiveHandle); resetRecentHandle(prevActiveHandle);
prevActiveHandle = handleDomNode; prevActiveHandle = handleDomNode;
// @todo: remove the old class names "react-flow__handle-" in the next major version // @todo: remove the old class names "react-flow__handle-" in the next major version
handleDomNode.classList.add('connecting'); handleDomNode.classList.add('connecting');
handleDomNode.classList.toggle('valid', isValid); handleDomNode.classList.toggle('valid', isValid);
} }
} }
function onPointerUp(event: MouseEvent | TouchEvent) { function onPointerUp(event: MouseEvent | TouchEvent) {
if ((prevClosestHandle || handleDomNode) && connection && isValid) { if ((prevClosestHandle || handleDomNode) && connection && isValid) {
onConnect?.(connection); onConnect?.(connection);
} }
// it's important to get a fresh reference from the store here // it's important to get a fresh reference from the store here
// in order to get the latest state of onConnectEnd // in order to get the latest state of onConnectEnd
// @todo add onConnectEnd prop // @todo add onConnectEnd prop
// getState().onConnectEnd?.(event); // getState().onConnectEnd?.(event);
if (edgeUpdaterType) { if (edgeUpdaterType) {
onEdgeUpdateEnd?.(event); onEdgeUpdateEnd?.(event);
} }
resetRecentHandle(prevActiveHandle); resetRecentHandle(prevActiveHandle);
cancelConnection(); cancelConnection();
cancelAnimationFrame(autoPanId); cancelAnimationFrame(autoPanId);
autoPanStarted = false; autoPanStarted = false;
isValid = false; isValid = false;
connection = null; connection = null;
handleDomNode = null; handleDomNode = null;
doc.removeEventListener('mousemove', onPointerMove as EventListener); doc.removeEventListener('mousemove', onPointerMove as EventListener);
doc.removeEventListener('mouseup', onPointerUp as EventListener); doc.removeEventListener('mouseup', onPointerUp as EventListener);
doc.removeEventListener('touchmove', onPointerMove as EventListener); doc.removeEventListener('touchmove', onPointerMove as EventListener);
doc.removeEventListener('touchend', onPointerUp as EventListener); doc.removeEventListener('touchend', onPointerUp as EventListener);
} }
doc.addEventListener('mousemove', onPointerMove as EventListener); doc.addEventListener('mousemove', onPointerMove as EventListener);
doc.addEventListener('mouseup', onPointerUp as EventListener); doc.addEventListener('mouseup', onPointerUp as EventListener);
doc.addEventListener('touchmove', onPointerMove as EventListener); doc.addEventListener('touchmove', onPointerMove as EventListener);
doc.addEventListener('touchend', onPointerUp as EventListener); doc.addEventListener('touchend', onPointerUp as EventListener);
} }
@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import cc from 'classcat'; import cc from 'classcat';
import { Position, type Connection, type HandleProps } from '@reactflow/system'; import { Position, type Connection, type HandleProps } from '@reactflow/system';
import { isMouseEvent } from '@reactflow/utils'; import { isMouseEvent } from '@reactflow/utils';
import { handlePointerDown } from './handler'; import { handlePointerDown } from './handler';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
type $$Props = HandleProps; type $$Props = HandleProps;
@@ -34,40 +34,40 @@
} = useStore(); } = useStore();
function onConnectExtended(params: Connection) { function onConnectExtended(params: Connection) {
addEdge(params); addEdge(params);
// @todo add props // @todo add props
// onConnectAction?.(edgeParams); // onConnectAction?.(edgeParams);
// onConnect?.(edgeParams); // onConnect?.(edgeParams);
}; }
function onPointerDown(event: MouseEvent | TouchEvent) { function onPointerDown(event: MouseEvent | TouchEvent) {
const isMouseTriggered = isMouseEvent(event); const isMouseTriggered = isMouseEvent(event);
if ((isMouseTriggered && event.button === 0) || !isMouseTriggered) { if ((isMouseTriggered && event.button === 0) || !isMouseTriggered) {
handlePointerDown({ handlePointerDown({
event, event,
handleId, handleId,
nodeId, nodeId,
isTarget, isTarget,
connectionRadius: $connectionRadius, connectionRadius: $connectionRadius,
domNode: $domNode, domNode: $domNode,
nodes: $nodes, nodes: $nodes,
connectionMode: $connectionMode, connectionMode: $connectionMode,
transform, transform,
isValidConnection: isValidConnection!, isValidConnection: isValidConnection!,
onConnect: onConnectExtended, onConnect: onConnectExtended,
updateConnection, updateConnection,
cancelConnection, cancelConnection,
panBy, panBy
}); });
} }
// if (isMouseTriggered) { // if (isMouseTriggered) {
// onMouseDown?.(event); // onMouseDown?.(event);
// } else { // } else {
// onTouchStart?.(event); // onTouchStart?.(event);
// } // }
}; }
</script> </script>
<div <div
@@ -92,7 +92,6 @@
<slot /> <slot />
</div> </div>
<style> <style>
.react-flow__handle { .react-flow__handle {
position: absolute; position: absolute;
+129 -129
View File
@@ -3,11 +3,11 @@ import type { Connection, HandleType, XYPosition, Node, NodeHandleBounds } from
import { getEventPosition } from '@reactflow/utils'; import { getEventPosition } from '@reactflow/utils';
export type ConnectionHandle = { export type ConnectionHandle = {
id: string | null; id: string | null;
type: HandleType; type: HandleType;
nodeId: string; nodeId: string;
x: number; x: number;
y: number; y: number;
}; };
export type ValidConnectionFunc = (connection: Connection) => boolean; export type ValidConnectionFunc = (connection: Connection) => boolean;
@@ -15,174 +15,174 @@ export type ValidConnectionFunc = (connection: Connection) => boolean;
// this functions collects all handles and adds an absolute position // this functions collects all handles and adds an absolute position
// so that we can later find the closest handle to the mouse position // so that we can later find the closest handle to the mouse position
export function getHandles( export function getHandles(
node: Node, node: Node,
handleBounds: NodeHandleBounds, handleBounds: NodeHandleBounds,
type: HandleType, type: HandleType,
currentHandle: string currentHandle: string
): ConnectionHandle[] { ): ConnectionHandle[] {
return (handleBounds[type] || []).reduce<ConnectionHandle[]>((res, h) => { return (handleBounds[type] || []).reduce<ConnectionHandle[]>((res, h) => {
if (`${node.id}-${h.id}-${type}` !== currentHandle) { if (`${node.id}-${h.id}-${type}` !== currentHandle) {
res.push({ res.push({
id: h.id || null, id: h.id || null,
type, type,
nodeId: node.id, nodeId: node.id,
x: (node.positionAbsolute?.x ?? 0) + h.x + h.width / 2, x: (node.positionAbsolute?.x ?? 0) + h.x + h.width / 2,
y: (node.positionAbsolute?.y ?? 0) + h.y + h.height / 2 y: (node.positionAbsolute?.y ?? 0) + h.y + h.height / 2
}); });
} }
return res; return res;
}, []); }, []);
} }
export function getClosestHandle( export function getClosestHandle(
pos: XYPosition, pos: XYPosition,
connectionRadius: number, connectionRadius: number,
handles: ConnectionHandle[] handles: ConnectionHandle[]
): ConnectionHandle | null { ): ConnectionHandle | null {
let closestHandle: ConnectionHandle | null = null; let closestHandle: ConnectionHandle | null = null;
let minDistance = Infinity; let minDistance = Infinity;
handles.forEach((handle) => { handles.forEach((handle) => {
const distance = Math.sqrt(Math.pow(handle.x - pos.x, 2) + Math.pow(handle.y - pos.y, 2)); const distance = Math.sqrt(Math.pow(handle.x - pos.x, 2) + Math.pow(handle.y - pos.y, 2));
if (distance <= connectionRadius && distance < minDistance) { if (distance <= connectionRadius && distance < minDistance) {
minDistance = distance; minDistance = distance;
closestHandle = handle; closestHandle = handle;
} }
}); });
return closestHandle; return closestHandle;
} }
type Result = { type Result = {
handleDomNode: Element | null; handleDomNode: Element | null;
isValid: boolean; isValid: boolean;
connection: Connection; connection: Connection;
}; };
const nullConnection: Connection = { const nullConnection: Connection = {
source: null, source: null,
target: null, target: null,
sourceHandle: null, sourceHandle: null,
targetHandle: null targetHandle: null
}; };
// checks if and returns connection in fom of an object { source: 123, target: 312 } // checks if and returns connection in fom of an object { source: 123, target: 312 }
export function isValidHandle( export function isValidHandle(
event: MouseEvent | TouchEvent, event: MouseEvent | TouchEvent,
handle: Pick<ConnectionHandle, 'nodeId' | 'id' | 'type'> | null, handle: Pick<ConnectionHandle, 'nodeId' | 'id' | 'type'> | null,
connectionMode: ConnectionMode, connectionMode: ConnectionMode,
fromNodeId: string, fromNodeId: string,
fromHandleId: string | null, fromHandleId: string | null,
fromType: string, fromType: string,
isValidConnection: ValidConnectionFunc, isValidConnection: ValidConnectionFunc,
doc: Document | ShadowRoot doc: Document | ShadowRoot
) { ) {
const isTarget = fromType === 'target'; const isTarget = fromType === 'target';
const handleDomNode = doc.querySelector( const handleDomNode = doc.querySelector(
`.react-flow__handle[data-id="${handle?.nodeId}-${handle?.id}-${handle?.type}"]` `.react-flow__handle[data-id="${handle?.nodeId}-${handle?.id}-${handle?.type}"]`
); );
const { x, y } = getEventPosition(event); const { x, y } = getEventPosition(event);
const handleBelow = doc.elementFromPoint(x, y); const handleBelow = doc.elementFromPoint(x, y);
const handleToCheck = handleBelow?.classList.contains('react-flow__handle') const handleToCheck = handleBelow?.classList.contains('react-flow__handle')
? handleBelow ? handleBelow
: handleDomNode; : handleDomNode;
const result: Result = { const result: Result = {
handleDomNode: handleToCheck, handleDomNode: handleToCheck,
isValid: false, isValid: false,
connection: nullConnection connection: nullConnection
}; };
if (handleToCheck) { if (handleToCheck) {
const handleType = getHandleType(undefined, handleToCheck); const handleType = getHandleType(undefined, handleToCheck);
const handleNodeId = handleToCheck.getAttribute('data-nodeid'); const handleNodeId = handleToCheck.getAttribute('data-nodeid');
const handleId = handleToCheck.getAttribute('data-handleid'); const handleId = handleToCheck.getAttribute('data-handleid');
const connection: Connection = { const connection: Connection = {
source: isTarget ? handleNodeId : fromNodeId, source: isTarget ? handleNodeId : fromNodeId,
sourceHandle: isTarget ? handleId : fromHandleId, sourceHandle: isTarget ? handleId : fromHandleId,
target: isTarget ? fromNodeId : handleNodeId, target: isTarget ? fromNodeId : handleNodeId,
targetHandle: isTarget ? fromHandleId : handleId targetHandle: isTarget ? fromHandleId : handleId
}; };
result.connection = connection; result.connection = connection;
// in strict mode we don't allow target to target or source to source connections // in strict mode we don't allow target to target or source to source connections
const isValid = const isValid =
connectionMode === ConnectionMode.Strict connectionMode === ConnectionMode.Strict
? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target') ? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target')
: handleNodeId !== fromNodeId || handleId !== fromHandleId; : handleNodeId !== fromNodeId || handleId !== fromHandleId;
if (isValid) { if (isValid) {
result.isValid = isValidConnection(connection); result.isValid = isValidConnection(connection);
} }
} }
return result; return result;
} }
type GetHandleLookupParams = { type GetHandleLookupParams = {
nodes: Node[]; nodes: Node[];
nodeId: string; nodeId: string;
handleId: string | null; handleId: string | null;
handleType: string; handleType: string;
}; };
export function getHandleLookup({ nodes, nodeId, handleId, handleType }: GetHandleLookupParams) { export function getHandleLookup({ nodes, nodeId, handleId, handleType }: GetHandleLookupParams) {
return nodes.reduce<ConnectionHandle[]>((res, node) => { return nodes.reduce<ConnectionHandle[]>((res, node) => {
if (node[internalsSymbol]) { if (node[internalsSymbol]) {
const { handleBounds } = node[internalsSymbol]; const { handleBounds } = node[internalsSymbol];
let sourceHandles: ConnectionHandle[] = []; let sourceHandles: ConnectionHandle[] = [];
let targetHandles: ConnectionHandle[] = []; let targetHandles: ConnectionHandle[] = [];
if (handleBounds) { if (handleBounds) {
sourceHandles = getHandles( sourceHandles = getHandles(
node, node,
handleBounds, handleBounds,
'source', 'source',
`${nodeId}-${handleId}-${handleType}` `${nodeId}-${handleId}-${handleType}`
); );
targetHandles = getHandles( targetHandles = getHandles(
node, node,
handleBounds, handleBounds,
'target', 'target',
`${nodeId}-${handleId}-${handleType}` `${nodeId}-${handleId}-${handleType}`
); );
} }
res.push(...sourceHandles, ...targetHandles); res.push(...sourceHandles, ...targetHandles);
} }
return res; return res;
}, []); }, []);
} }
export function getHandleType( export function getHandleType(
edgeUpdaterType: HandleType | undefined, edgeUpdaterType: HandleType | undefined,
handleDomNode: Element | null handleDomNode: Element | null
): HandleType | null { ): HandleType | null {
if (edgeUpdaterType) { if (edgeUpdaterType) {
return edgeUpdaterType; return edgeUpdaterType;
} else if (handleDomNode?.classList.contains('target')) { } else if (handleDomNode?.classList.contains('target')) {
return 'target'; return 'target';
} else if (handleDomNode?.classList.contains('source')) { } else if (handleDomNode?.classList.contains('source')) {
return 'source'; return 'source';
} }
return null; return null;
} }
export function resetRecentHandle(handleDomNode: Element): void { export function resetRecentHandle(handleDomNode: Element): void {
handleDomNode?.classList.remove('valid', 'connecting'); handleDomNode?.classList.remove('valid', 'connecting');
} }
export function getConnectionStatus(isInsideConnectionRadius: boolean, isHandleValid: boolean) { export function getConnectionStatus(isInsideConnectionRadius: boolean, isHandleValid: boolean) {
let connectionStatus = null; let connectionStatus = null;
if (isHandleValid) { if (isHandleValid) {
connectionStatus = 'valid'; connectionStatus = 'valid';
} else if (isInsideConnectionRadius && !isHandleValid) { } else if (isInsideConnectionRadius && !isHandleValid) {
connectionStatus = 'invalid'; connectionStatus = 'invalid';
} }
return connectionStatus as ConnectionStatus; return connectionStatus as ConnectionStatus;
} }
@@ -1,6 +1,6 @@
import { KeyDefinition } from '$lib/types'; import { KeyDefinition } from '$lib/types';
export type KeyHandlerProps = { export type KeyHandlerProps = {
selectionKey?: KeyDefinition; selectionKey?: KeyDefinition;
deleteKey?: KeyDefinition; deleteKey?: KeyDefinition;
}; };
@@ -2,8 +2,8 @@
import { shortcut } from '@svelte-put/shortcut'; import { shortcut } from '@svelte-put/shortcut';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import type { KeyHandlerProps } from './KeyHandler' import type { KeyHandlerProps } from './KeyHandler';
import type { KeyDefinition, KeyDefinitionObject } from '$lib/types'; import type { KeyDefinition, KeyDefinitionObject } from '$lib/types';
const { selectionKeyPressed, deleteKeyPressed } = useStore(); const { selectionKeyPressed, deleteKeyPressed } = useStore();
@@ -25,19 +25,43 @@
<svelte:window <svelte:window
use:shortcut={{ use:shortcut={{
trigger: [{ key: selectionKeyString, modifier: selectionKeyModifier, callback: () => selectionKeyPressed.set(true) }], trigger: [
{
key: selectionKeyString,
modifier: selectionKeyModifier,
callback: () => selectionKeyPressed.set(true)
}
],
type: 'keydown' type: 'keydown'
}} }}
use:shortcut={{ use:shortcut={{
trigger: [{ key: selectionKeyString, modifier: selectionKeyModifier, callback: () => selectionKeyPressed.set(false) }], trigger: [
{
key: selectionKeyString,
modifier: selectionKeyModifier,
callback: () => selectionKeyPressed.set(false)
}
],
type: 'keyup' type: 'keyup'
}} }}
use:shortcut={{ use:shortcut={{
trigger: [{ key: deleteKeyString, modifier: deleteKeyModifier, callback: () => deleteKeyPressed.set(true) }], trigger: [
{
key: deleteKeyString,
modifier: deleteKeyModifier,
callback: () => deleteKeyPressed.set(true)
}
],
type: 'keydown' type: 'keydown'
}} }}
use:shortcut={{ use:shortcut={{
trigger: [{ key: deleteKeyString, modifier: deleteKeyModifier, callback: () => deleteKeyPressed.set(false) }], trigger: [
{
key: deleteKeyString,
modifier: deleteKeyModifier,
callback: () => deleteKeyPressed.set(false)
}
],
type: 'keyup' type: 'keyup'
}} }}
/> />
@@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
import { getRectOfNodes } from '@reactflow/utils'; import { getRectOfNodes } from '@reactflow/utils';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import Selection from '$lib/components/Selection/index.svelte'; import Selection from '$lib/components/Selection/index.svelte';
import drag from '$lib/actions/drag' import drag from '$lib/actions/drag';
const { selectionRectMode, nodes, nodeOrigin, transform, updateNodePositions } = useStore(); const { selectionRectMode, nodes, nodeOrigin, transform, updateNodePositions } = useStore();
$: selectedNodes = $nodes.filter(n => n.selected); $: selectedNodes = $nodes.filter((n) => n.selected);
$: rect = getRectOfNodes(selectedNodes, $nodeOrigin); $: rect = getRectOfNodes(selectedNodes, $nodeOrigin);
</script> </script>
@@ -11,11 +11,7 @@
export let interactionWidth: $$Props['interactionWidth'] = 20; export let interactionWidth: $$Props['interactionWidth'] = 20;
</script> </script>
<path <path d={path} fill="none" class="react-flow__edge-path" />
d={path}
fill="none"
class="react-flow__edge-path"
/>
{#if interactionWidth} {#if interactionWidth}
<path <path
@@ -33,7 +29,7 @@
class="react-flow__edge-label" class="react-flow__edge-label"
style:transform={`translate(-50%, -50%) translate(${labelX}px,${labelY}px)`} style:transform={`translate(-50%, -50%) translate(${labelX}px,${labelY}px)`}
> >
{label} {label}
</div> </div>
</EdgeLabelRenderer> </EdgeLabelRenderer>
{/if} {/if}
@@ -2,7 +2,7 @@
import { getBezierPath } from '@reactflow/edge-utils'; import { getBezierPath } from '@reactflow/edge-utils';
import type { EdgeProps } from '$lib/types'; import type { EdgeProps } from '$lib/types';
import BaseEdge from './BaseEdge.svelte'; import BaseEdge from './BaseEdge.svelte';
type $$Props = EdgeProps; type $$Props = EdgeProps;
@@ -12,13 +12,8 @@
targetX: $$props.targetX, targetX: $$props.targetX,
targetY: $$props.targetY, targetY: $$props.targetY,
sourcePosition: $$props.sourcePosition, sourcePosition: $$props.sourcePosition,
targetPosition: $$props.targetPosition, targetPosition: $$props.targetPosition
}); });
</script> </script>
<BaseEdge <BaseEdge {path} {labelX} {labelY} {...$$props} />
{path}
{labelX}
{labelY}
{...$$props}
/>
@@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import type { SvelteComponentTyped } from 'svelte'; import type { SvelteComponentTyped } from 'svelte';
import { Position } from '@reactflow/system'; import { Position } from '@reactflow/system';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import BezierEdge from '$lib/components/edges/StraightEdge.svelte'; import BezierEdge from '$lib/components/edges/StraightEdge.svelte';
import type { EdgeProps, WrapEdgeProps } from '$lib/types'; import type { EdgeProps, WrapEdgeProps } from '$lib/types';
type $$Props = WrapEdgeProps; type $$Props = WrapEdgeProps;
@@ -17,7 +17,7 @@
export let targetX: $$Props['targetX'] = 0; export let targetX: $$Props['targetX'] = 0;
export let targetY: $$Props['targetY'] = 0; export let targetY: $$Props['targetY'] = 0;
export let sourcePosition: $$Props['sourcePosition'] = Position.Bottom; export let sourcePosition: $$Props['sourcePosition'] = Position.Bottom;
export let targetPosition: $$Props['targetPosition'] = Position.Top; export let targetPosition: $$Props['targetPosition'] = Position.Top;
export let animated: $$Props['animated'] = false; export let animated: $$Props['animated'] = false;
export let selected: $$Props['selected'] = false; export let selected: $$Props['selected'] = false;
export let label: $$Props['label'] = undefined; export let label: $$Props['label'] = undefined;
@@ -26,11 +26,7 @@
const edgeComponent: typeof SvelteComponentTyped<EdgeProps> = $edgeTypes[type!] || BezierEdge; const edgeComponent: typeof SvelteComponentTyped<EdgeProps> = $edgeTypes[type!] || BezierEdge;
</script> </script>
<g <g class="react-flow__edge" class:animated data-id={id}>
class="react-flow__edge"
class:animated
data-id={id}
>
<svelte:component <svelte:component
this={edgeComponent} this={edgeComponent}
{id} {id}
@@ -12,13 +12,8 @@
targetX: $$props.targetX, targetX: $$props.targetX,
targetY: $$props.targetY, targetY: $$props.targetY,
sourcePosition: $$props.sourcePosition, sourcePosition: $$props.sourcePosition,
targetPosition: $$props.targetPosition, targetPosition: $$props.targetPosition
}); });
</script> </script>
<BaseEdge <BaseEdge {path} {labelX} {labelY} {...$$props} />
{path}
{labelX}
{labelY}
{...$$props}
/>
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { getStraightPath } from '@reactflow/edge-utils'; import { getStraightPath } from '@reactflow/edge-utils';
import type { EdgeProps } from '$lib/types'; import type { EdgeProps } from '$lib/types';
import BaseEdge from './BaseEdge.svelte'; import BaseEdge from './BaseEdge.svelte';
type $$Props = EdgeProps; type $$Props = EdgeProps;
@@ -10,13 +10,8 @@
sourceX: $$props.sourceX, sourceX: $$props.sourceX,
sourceY: $$props.sourceY, sourceY: $$props.sourceY,
targetX: $$props.targetX, targetX: $$props.targetX,
targetY: $$props.targetY, targetY: $$props.targetY
}); });
</script> </script>
<BaseEdge <BaseEdge {path} {labelX} {labelY} {...$$props} />
{path}
{labelX}
{labelY}
{...$$props}
/>
@@ -2,9 +2,9 @@
import { Position } from '@reactflow/system'; import { Position } from '@reactflow/system';
import Handle from '$lib/components/Handle/index.svelte'; import Handle from '$lib/components/Handle/index.svelte';
import type { NodeProps } from '$lib/types'; import type { NodeProps } from '$lib/types';
interface $$Props extends NodeProps<{ label: string }>{} interface $$Props extends NodeProps<{ label: string }> {}
export let id: $$Props['id']; export let id: $$Props['id'];
export let data: $$Props['data'] = { label: 'Node' }; export let data: $$Props['data'] = { label: 'Node' };
@@ -10,4 +10,3 @@
{data?.label} {data?.label}
<Handle type="source" position={sourcePosition} /> <Handle type="source" position={sourcePosition} />
@@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import { onMount, setContext, SvelteComponentTyped } from 'svelte'; import { onMount, setContext, SvelteComponentTyped } from 'svelte';
import cc from 'classcat'; import cc from 'classcat';
import { type XYPosition, Position } from '@reactflow/system'; import { type XYPosition, Position } from '@reactflow/system';
import drag from '$lib/actions/drag' import drag from '$lib/actions/drag';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import DefaultNode from './DefaultNode.svelte'; import DefaultNode from './DefaultNode.svelte';
import type { WrapNodeProps, NodeProps } from '$lib/types'; import type { WrapNodeProps, NodeProps } from '$lib/types';
interface $$Props extends WrapNodeProps {} interface $$Props extends WrapNodeProps {}
@@ -29,7 +29,8 @@
let nodeRef: HTMLDivElement; let nodeRef: HTMLDivElement;
const { nodes, transform, nodeTypes, updateNodePositions, addSelectedNodes } = useStore(); const { nodes, transform, nodeTypes, updateNodePositions, addSelectedNodes } = useStore();
const nodeComponent: typeof SvelteComponentTyped<Partial<NodeProps>> = $nodeTypes[type] || DefaultNode; const nodeComponent: typeof SvelteComponentTyped<Partial<NodeProps>> =
$nodeTypes[type] || DefaultNode;
const isSelectable = true; const isSelectable = true;
const selectNodesOnDrag = false; const selectNodesOnDrag = false;
const isDraggable = true; const isDraggable = true;
@@ -41,7 +42,7 @@
return () => { return () => {
resizeObserver?.unobserve(nodeRef); resizeObserver?.unobserve(nodeRef);
} };
}); });
function onSelectNodeHandler(event: MouseEvent) { function onSelectNodeHandler(event: MouseEvent) {
@@ -54,15 +55,15 @@
// const node = store.getState().nodeInternals.get(id)!; // const node = store.getState().nodeInternals.get(id)!;
// onClick(event, { ...node }); // onClick(event, { ...node });
// } // }
}; }
</script> </script>
<div <div
use:drag={{ nodeId: id, nodes, transform, updateNodePositions }} use:drag={{ nodeId: id, nodes, transform, updateNodePositions }}
class={cc(['react-flow__node', `react-flow__node-${type}`, className])} class={cc(['react-flow__node', `react-flow__node-${type}`, className])}
class:initializing={!width && !height} class:initializing={!width && !height}
class:dragging={dragging} class:dragging
class:selected={selected} class:selected
bind:this={nodeRef} bind:this={nodeRef}
on:click={onSelectNodeHandler} on:click={onSelectNodeHandler}
style:transform={`translate(${positionAbsolute.x}px, ${positionAbsolute.y}px)`} style:transform={`translate(${positionAbsolute.x}px, ${positionAbsolute.y}px)`}
@@ -10,4 +10,3 @@
{data?.label} {data?.label}
<Handle type="target" position={targetPosition} /> <Handle type="target" position={targetPosition} />
@@ -3,13 +3,9 @@
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
const { edgesLayouted, width, height } = useStore(); const { edgesLayouted, width, height } = useStore();
</script>
<svg width={$width} height={$height} class="react-flow__edges">
<svg
width={$width}
height={$height}
class="react-flow__edges"
{#each $edgesLayouted as edge (edge.id)} {#each $edgesLayouted as edge (edge.id)}
<EdgeWrapper {...edge} /> <EdgeWrapper {...edge} />
{/each} {/each}
@@ -1,105 +1,105 @@
import { import {
type NodeHandleBounds, type NodeHandleBounds,
type Rect, type Rect,
type Node, type Node,
type HandleElement, type HandleElement,
type XYPosition, type XYPosition,
Position, Position,
internalsSymbol internalsSymbol
} from '@reactflow/system'; } from '@reactflow/system';
export function getNodeData(node?: Node): [Rect, NodeHandleBounds | null, boolean] { export function getNodeData(node?: Node): [Rect, NodeHandleBounds | null, boolean] {
const handleBounds = node?.[internalsSymbol]?.handleBounds || null; const handleBounds = node?.[internalsSymbol]?.handleBounds || null;
const isValid = const isValid =
handleBounds && handleBounds &&
node?.width && node?.width &&
node?.height && node?.height &&
typeof node?.positionAbsolute?.x !== 'undefined' && typeof node?.positionAbsolute?.x !== 'undefined' &&
typeof node?.positionAbsolute?.y !== 'undefined'; typeof node?.positionAbsolute?.y !== 'undefined';
return [ return [
{ {
x: node?.positionAbsolute?.x || 0, x: node?.positionAbsolute?.x || 0,
y: node?.positionAbsolute?.y || 0, y: node?.positionAbsolute?.y || 0,
width: node?.width || 0, width: node?.width || 0,
height: node?.height || 0 height: node?.height || 0
}, },
handleBounds, handleBounds,
!!isValid !!isValid
]; ];
} }
export function getHandlePosition( export function getHandlePosition(
position: Position, position: Position,
nodeRect: Rect, nodeRect: Rect,
handle: HandleElement | null = null handle: HandleElement | null = null
): XYPosition { ): XYPosition {
const x = (handle?.x || 0) + nodeRect.x; const x = (handle?.x || 0) + nodeRect.x;
const y = (handle?.y || 0) + nodeRect.y; const y = (handle?.y || 0) + nodeRect.y;
const width = handle?.width || nodeRect.width; const width = handle?.width || nodeRect.width;
const height = handle?.height || nodeRect.height; const height = handle?.height || nodeRect.height;
switch (position) { switch (position) {
case Position.Top: case Position.Top:
return { return {
x: x + width / 2, x: x + width / 2,
y y
}; };
case Position.Right: case Position.Right:
return { return {
x: x + width, x: x + width,
y: y + height / 2 y: y + height / 2
}; };
case Position.Bottom: case Position.Bottom:
return { return {
x: x + width / 2, x: x + width / 2,
y: y + height y: y + height
}; };
case Position.Left: case Position.Left:
return { return {
x, x,
y: y + height / 2 y: y + height / 2
}; };
} }
} }
export function getHandle(bounds: HandleElement[], handleId?: string | null): HandleElement | null { export function getHandle(bounds: HandleElement[], handleId?: string | null): HandleElement | null {
if (!bounds) { if (!bounds) {
return null; return null;
} }
if (handleId) { if (handleId) {
return bounds.find((d) => d.id === handleId)!; return bounds.find((d) => d.id === handleId)!;
} else if (bounds.length === 1) { } else if (bounds.length === 1) {
return bounds[0]; return bounds[0];
} }
return null; return null;
} }
export type EdgePosition = { export type EdgePosition = {
sourceX: number; sourceX: number;
sourceY: number; sourceY: number;
targetX: number; targetX: number;
targetY: number; targetY: number;
}; };
export function getEdgePositions( export function getEdgePositions(
sourceNodeRect: Rect, sourceNodeRect: Rect,
sourceHandle: HandleElement, sourceHandle: HandleElement,
sourcePosition: Position, sourcePosition: Position,
targetNodeRect: Rect, targetNodeRect: Rect,
targetHandle: HandleElement, targetHandle: HandleElement,
targetPosition: Position targetPosition: Position
): EdgePosition { ): EdgePosition {
const sourceHandlePos = getHandlePosition(sourcePosition, sourceNodeRect, sourceHandle); const sourceHandlePos = getHandlePosition(sourcePosition, sourceNodeRect, sourceHandle);
const targetHandlePos = getHandlePosition(targetPosition, targetNodeRect, targetHandle); const targetHandlePos = getHandlePosition(targetPosition, targetNodeRect, targetHandle);
return { return {
sourceX: sourceHandlePos.x, sourceX: sourceHandlePos.x,
sourceY: sourceHandlePos.y, sourceY: sourceHandlePos.y,
targetX: targetHandlePos.x, targetX: targetHandlePos.x,
targetY: targetHandlePos.y targetY: targetHandlePos.y
}; };
} }
@@ -5,15 +5,18 @@
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
const { nodes, updateNodeDimensions } = useStore(); const { nodes, updateNodeDimensions } = useStore();
const resizeObserver: ResizeObserver | null = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver((entries: ResizeObserverEntry[]) => { const resizeObserver: ResizeObserver | null =
const updates = entries.map((entry: ResizeObserverEntry) => ({ typeof ResizeObserver === 'undefined'
id: entry.target.getAttribute('data-id') as string, ? null
nodeElement: entry.target as HTMLDivElement, : new ResizeObserver((entries: ResizeObserverEntry[]) => {
forceUpdate: true, const updates = entries.map((entry: ResizeObserverEntry) => ({
})) id: entry.target.getAttribute('data-id') as string,
nodeElement: entry.target as HTMLDivElement,
forceUpdate: true
}));
updateNodeDimensions(updates); updateNodeDimensions(updates);
}); });
onDestroy(() => { onDestroy(() => {
resizeObserver?.disconnect(); resizeObserver?.disconnect();
@@ -10,7 +10,7 @@
handler?.(event); handler?.(event);
}; };
}; }
export function toggleSelected<Item extends Node | Edge>(ids: string[]) { export function toggleSelected<Item extends Node | Edge>(ids: string[]) {
return (item: Item) => { return (item: Item) => {
@@ -19,39 +19,48 @@
if (item.selected !== isSelected) { if (item.selected !== isSelected) {
return { return {
...item, ...item,
selected: isSelected, selected: isSelected
}; };
} }
return item; return item;
} };
} }
</script> </script>
<script lang="ts"> <script lang="ts">
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import { SelectionMode, type Node, type Edge } from '@reactflow/system'; import { SelectionMode, type Node, type Edge } from '@reactflow/system';
import { getConnectedEdges, getEventPosition, getNodesInside } from '@reactflow/utils'; import { getConnectedEdges, getEventPosition, getNodesInside } from '@reactflow/utils';
const { nodes, edges, transform, nodeOrigin, dragging, selectionRect, selectionRectMode, selectionKeyPressed, resetSelectedElements } = useStore(); const {
nodes,
edges,
transform,
nodeOrigin,
dragging,
selectionRect,
selectionRectMode,
selectionKeyPressed,
resetSelectedElements
} = useStore();
// @todo take from props // @todo take from props
const elementsSelectable = true; const elementsSelectable = true;
const selectionMode = SelectionMode.Partial const selectionMode = SelectionMode.Partial;
let container: HTMLDivElement; let container: HTMLDivElement;
let containerBounds: DOMRect | null = null let containerBounds: DOMRect | null = null;
let selectedNodes: Node[] = []; let selectedNodes: Node[] = [];
$: isSelecting = $selectionKeyPressed; $: isSelecting = $selectionKeyPressed;
$: hasActiveSelection = elementsSelectable && (isSelecting || $selectionRectMode === 'user'); $: hasActiveSelection = elementsSelectable && (isSelecting || $selectionRectMode === 'user');
function onClick(event: MouseEvent) {
function onClick (event: MouseEvent) {
// onPaneClick?.(event); // onPaneClick?.(event);
resetSelectedElements(); resetSelectedElements();
selectionRectMode.set(null) selectionRectMode.set(null);
} }
function onMouseDown(event: MouseEvent) { function onMouseDown(event: MouseEvent) {
@@ -80,11 +89,8 @@
y y
}); });
// onSelectionStart?.(event); // onSelectionStart?.(event);
}; }
function onMouseMove(event: MouseEvent) { function onMouseMove(event: MouseEvent) {
if (!isSelecting || !containerBounds || !$selectionRect) { if (!isSelecting || !containerBounds || !$selectionRect) {
@@ -98,11 +104,11 @@
x: mousePos.x < startX ? mousePos.x : startX, x: mousePos.x < startX ? mousePos.x : startX,
y: mousePos.y < startY ? mousePos.y : startY, y: mousePos.y < startY ? mousePos.y : startY,
width: Math.abs(mousePos.x - startX), width: Math.abs(mousePos.x - startX),
height: Math.abs(mousePos.y - startY), height: Math.abs(mousePos.y - startY)
}; };
selectedNodes = getNodesInside( selectedNodes = getNodesInside(
new Map($nodes.map(node => [node.id, node])), new Map($nodes.map((node) => [node.id, node])),
nextUserSelectRect, nextUserSelectRect,
$transform, $transform,
selectionMode === SelectionMode.Partial, selectionMode === SelectionMode.Partial,
@@ -112,8 +118,8 @@
const selectedEdgeIds = getConnectedEdges(selectedNodes, $edges).map((e) => e.id); const selectedEdgeIds = getConnectedEdges(selectedNodes, $edges).map((e) => e.id);
const selectedNodeIds = selectedNodes.map((n) => n.id); const selectedNodeIds = selectedNodes.map((n) => n.id);
nodes.update(nodes => nodes.map(toggleSelected(selectedNodeIds))); nodes.update((nodes) => nodes.map(toggleSelected(selectedNodeIds)));
edges.update(edges => edges.map(toggleSelected(selectedEdgeIds))); edges.update((edges) => edges.map(toggleSelected(selectedEdgeIds)));
selectionRectMode.set('user'); selectionRectMode.set('user');
selectionRect.set(nextUserSelectRect); selectionRect.set(nextUserSelectRect);
@@ -135,7 +141,6 @@
selectionRectMode.set('nodes'); selectionRectMode.set('nodes');
} }
// onSelectionEnd?.(event); // onSelectionEnd?.(event);
} }
@@ -156,7 +161,7 @@
class:selection={isSelecting} class:selection={isSelecting}
on:click={hasActiveSelection ? undefined : wrapHandler(onClick, container)} on:click={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
on:mousedown={hasActiveSelection ? onMouseDown : undefined} on:mousedown={hasActiveSelection ? onMouseDown : undefined}
on:mousemove={hasActiveSelection? onMouseMove : undefined} on:mousemove={hasActiveSelection ? onMouseMove : undefined}
on:mouseup={hasActiveSelection ? onMouseUp : undefined} on:mouseup={hasActiveSelection ? onMouseUp : undefined}
on:mouseleave={hasActiveSelection ? onMouseLeave : undefined} on:mouseleave={hasActiveSelection ? onMouseLeave : undefined}
> >
+3 -3
View File
@@ -1,7 +1,7 @@
import type { PanelPosition } from '@reactflow/system'; import type { PanelPosition } from '@reactflow/system';
export type PanelProps = { export type PanelProps = {
position?: PanelPosition; position?: PanelPosition;
style?: string; style?: string;
class?: string; class?: string;
}; };
@@ -7,15 +7,12 @@
export let position: $$Props['position'] = 'top-right'; export let position: $$Props['position'] = 'top-right';
export let style: $$Props['style'] = ''; export let style: $$Props['style'] = '';
let className: $$Props['class'] = undefined; let className: $$Props['class'] = undefined;
export { className as class } export { className as class };
$: positionClasses = `${position}`.split('-'); $: positionClasses = `${position}`.split('-');
</script> </script>
<div <div class={cc(['react-flow__panel', className, ...positionClasses])} {style}>
class={cc(['react-flow__panel', className, ...positionClasses])}
style={style}
>
<slot /> <slot />
</div> </div>
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { setContext, onMount } from 'svelte' import { setContext, onMount } from 'svelte';
import cc from 'classcat'; import cc from 'classcat';
import { key, createStore } from '$lib/store'; import { key, createStore } from '$lib/store';
import Zoom from '$lib/container/Zoom/index.svelte'; import Zoom from '$lib/container/Zoom/index.svelte';
import Pane from '$lib/container/Pane/index.svelte'; import Pane from '$lib/container/Pane/index.svelte';
import Viewport from '$lib/container/Viewport.svelte'; import Viewport from '$lib/container/Viewport.svelte';
@@ -12,7 +12,7 @@
import NodeSelection from '$lib/components/NodeSelection/index.svelte'; import NodeSelection from '$lib/components/NodeSelection/index.svelte';
import { KeyHandler } from '$lib/components/KeyHandler'; import { KeyHandler } from '$lib/components/KeyHandler';
import { ConnectionLine } from '$lib/components/ConnectionLine'; import { ConnectionLine } from '$lib/components/ConnectionLine';
import type { SvelteFlowProps } from '$lib/types'; import type { SvelteFlowProps } from '$lib/types';
type $$Props = SvelteFlowProps; type $$Props = SvelteFlowProps;
@@ -51,7 +51,6 @@
}); });
}); });
// $: { // $: {
// const updatableProps = { // const updatableProps = {
// defaultEdgeOptions, // defaultEdgeOptions,
@@ -86,7 +85,7 @@
{...$$restProps} {...$$restProps}
class={cc(['react-flow', className])} class={cc(['react-flow', className])}
data-testid="rf__wrapper" data-testid="rf__wrapper"
id={id} {id}
bind:this={domNode} bind:this={domNode}
> >
<KeyHandler {selectionKey} {deleteKey} /> <KeyHandler {selectionKey} {deleteKey} />
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
const { transform } = useStore(); const { transform } = useStore();
</script> </script>
@@ -4,7 +4,7 @@
import DotPattern from './DotPattern.svelte'; import DotPattern from './DotPattern.svelte';
import LinePattern from './LinePattern.svelte'; import LinePattern from './LinePattern.svelte';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import { BackgroundVariant } from './types'; import { BackgroundVariant } from './types';
export let variant: BackgroundVariant = BackgroundVariant.Dots; export let variant: BackgroundVariant = BackgroundVariant.Dots;
export let gap = 20; export let gap = 20;
@@ -18,13 +18,13 @@
const defaultColor = { const defaultColor = {
[BackgroundVariant.Dots]: '#91919a', [BackgroundVariant.Dots]: '#91919a',
[BackgroundVariant.Lines]: '#eee', [BackgroundVariant.Lines]: '#eee',
[BackgroundVariant.Cross]: '#e2e2e2', [BackgroundVariant.Cross]: '#e2e2e2'
}; };
const defaultSize = { const defaultSize = {
[BackgroundVariant.Dots]: 1, [BackgroundVariant.Dots]: 1,
[BackgroundVariant.Lines]: 1, [BackgroundVariant.Lines]: 1,
[BackgroundVariant.Cross]: 6, [BackgroundVariant.Cross]: 6
}; };
const { transform, id } = useStore(); const { transform, id } = useStore();
@@ -43,7 +43,7 @@
: [patternDimensions[0] / 2, patternDimensions[1] / 2]; : [patternDimensions[0] / 2, patternDimensions[1] / 2];
</script> </script>
<svg class={cc(['react-flow__background', className])} > <svg class={cc(['react-flow__background', className])}>
<pattern <pattern
id={patternId} id={patternId}
x={$transform[0] % scaledGap[0]} x={$transform[0] % scaledGap[0]}
@@ -56,7 +56,7 @@
{#if isDots} {#if isDots}
<DotPattern color={patternColor} radius={scaledSize / 2} /> <DotPattern color={patternColor} radius={scaledSize / 2} />
{:else} {:else}
<LinePattern dimensions={patternDimensions } color={patternColor} lineWidth={lineWidth} /> <LinePattern dimensions={patternDimensions} color={patternColor} {lineWidth} />
{/if} {/if}
</pattern> </pattern>
<rect x="0" y="0" width="100%" height="100%" fill={`url(#${patternId})`} /> <rect x="0" y="0" width="100%" height="100%" fill={`url(#${patternId})`} />
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
export let radius = 5; export let radius = 5;
export let color = "#000"; export let color = '#000';
</script> </script>
<circle cx={radius} cy={radius} r={radius} fill={color} /> <circle cx={radius} cy={radius} r={radius} fill={color} />
@@ -1,13 +1,11 @@
<script lang="ts"> <script lang="ts">
export let lineWidth = 1; export let lineWidth = 1;
export let color = "#777"; export let color = '#777';
export let dimensions: [number, number]; export let dimensions: [number, number];
</script> </script>
<path <path
stroke={color} stroke={color}
stroke-width={lineWidth} stroke-width={lineWidth}
d={`M${dimensions[0] / 2} 0 V${dimensions[1]} M0 ${dimensions[1] / 2} H${ d={`M${dimensions[0] / 2} 0 V${dimensions[1]} M0 ${dimensions[1] / 2} H${dimensions[0]}`}
dimensions[0]
}`}
/> />
@@ -1,5 +1,5 @@
export enum BackgroundVariant { export enum BackgroundVariant {
Lines = 'lines', Lines = 'lines',
Dots = 'dots', Dots = 'dots',
Cross = 'cross' Cross = 'cross'
} }
@@ -3,10 +3,15 @@
let className: string; let className: string;
export { className as class } export { className as class };
</script> </script>
<button type="button" on:click class={cc(['react-flow__controls-button', className])} {...$$restProps} > <button
type="button"
on:click
class={cc(['react-flow__controls-button', className])}
{...$$restProps}
>
<slot class="button-svg" /> <slot class="button-svg" />
</button> </button>
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { PanelPosition } from '@reactflow/system'; import type { PanelPosition } from '@reactflow/system';
import Panel from '$lib/container/Panel/index.svelte'; import Panel from '$lib/container/Panel/index.svelte';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
@@ -37,13 +37,11 @@
// nodesConnectable: !isInteractive, // nodesConnectable: !isInteractive,
// elementsSelectable: !isInteractive, // elementsSelectable: !isInteractive,
// }); // });
// onInteractiveChange?.(!isInteractive); // onInteractiveChange?.(!isInteractive);
}; };
</script> </script>
<Panel class="react-flow__controls" position={position}> <Panel class="react-flow__controls" {position}>
{#if showZoom} {#if showZoom}
<ControlButton <ControlButton
on:click={onZoomInHandler} on:click={onZoomInHandler}
@@ -1,3 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 30"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 30">
<path d="M3.692 4.63c0-.53.4-.938.939-.938h5.215V0H4.708C2.13 0 0 2.054 0 4.63v5.216h3.692V4.631zM27.354 0h-5.2v3.692h5.17c.53 0 .984.4.984.939v5.215H32V4.631A4.624 4.624 0 0027.354 0zm.954 24.83c0 .532-.4.94-.939.94h-5.215v3.768h5.215c2.577 0 4.631-2.13 4.631-4.707v-5.139h-3.692v5.139zm-23.677.94c-.531 0-.939-.4-.939-.94v-5.138H0v5.139c0 2.577 2.13 4.707 4.708 4.707h5.138V25.77H4.631z" /> <path
d="M3.692 4.63c0-.53.4-.938.939-.938h5.215V0H4.708C2.13 0 0 2.054 0 4.63v5.216h3.692V4.631zM27.354 0h-5.2v3.692h5.17c.53 0 .984.4.984.939v5.215H32V4.631A4.624 4.624 0 0027.354 0zm.954 24.83c0 .532-.4.94-.939.94h-5.215v3.768h5.215c2.577 0 4.631-2.13 4.631-4.707v-5.139h-3.692v5.139zm-23.677.94c-.531 0-.939-.4-.939-.94v-5.138H0v5.139c0 2.577 2.13 4.707 4.708 4.707h5.138V25.77H4.631z"
/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 469 B

@@ -1,3 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 32"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 32">
<path d="M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0 8 0 4.571 3.429 4.571 7.619v3.048H3.048A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047zm4.724-13.866H7.467V7.619c0-2.59 2.133-4.724 4.723-4.724 2.591 0 4.724 2.133 4.724 4.724v3.048z" /> <path
d="M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0 8 0 4.571 3.429 4.571 7.619v3.048H3.048A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047zm4.724-13.866H7.467V7.619c0-2.59 2.133-4.724 4.723-4.724 2.591 0 4.724 2.133 4.724 4.724v3.048z"
/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 529 B

After

Width:  |  Height:  |  Size: 536 B

@@ -1,3 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 32"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 32">
<path d="M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0c-4.114 1.828-1.37 2.133.305 2.438 1.676.305 4.42 2.59 4.42 5.181v3.048H3.047A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047z" /> <path
d="M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0c-4.114 1.828-1.37 2.133.305 2.438 1.676.305 4.42 2.59 4.42 5.181v3.048H3.047A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047z"
/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 471 B

After

Width:  |  Height:  |  Size: 478 B

@@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import cc from 'classcat'; import cc from 'classcat';
import type { PanelPosition, Rect, Node } from '@reactflow/system'; import type { PanelPosition, Rect, Node } from '@reactflow/system';
import { getBoundsOfRects, getNodePositionWithOrigin, getRectOfNodes } from '@reactflow/utils'; import { getBoundsOfRects, getNodePositionWithOrigin, getRectOfNodes } from '@reactflow/utils';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import Panel from '$lib/container/Panel/index.svelte'; import Panel from '$lib/container/Panel/index.svelte';
import MinimapNode from './MinimapNode.svelte'; import MinimapNode from './MinimapNode.svelte';
let position: PanelPosition = 'bottom-right'; let position: PanelPosition = 'bottom-right';
@@ -27,20 +27,23 @@
const { nodes, transform, width, height, nodeOrigin, id } = useStore(); const { nodes, transform, width, height, nodeOrigin, id } = useStore();
type GetMiniMapNodeAttribute = (node: Node) => string; type GetMiniMapNodeAttribute = (node: Node) => string;
const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func); const getAttrFunction = (func: any): GetMiniMapNodeAttribute =>
func instanceof Function ? func : () => func;
const nodeColorFunc = getAttrFunction(nodeColor); const nodeColorFunc = getAttrFunction(nodeColor);
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor); const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
const nodeClassNameFunc = getAttrFunction(nodeClassName); const nodeClassNameFunc = getAttrFunction(nodeClassName);
const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision'; const shapeRendering =
typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision';
const labelledBy = `react-flow__minimap-desc-${$id}`; const labelledBy = `react-flow__minimap-desc-${$id}`;
$: viewBB = { $: viewBB = {
x: -$transform[0] / $transform[2], x: -$transform[0] / $transform[2],
y: -$transform[1] / $transform[2], y: -$transform[1] / $transform[2],
width: $width / $transform[2], width: $width / $transform[2],
height: $height / $transform[2], height: $height / $transform[2]
} as Rect; } as Rect;
$: boundingRect = $nodes.length > 0 ? getBoundsOfRects(getRectOfNodes($nodes, $nodeOrigin), viewBB) : viewBB $: boundingRect =
$nodes.length > 0 ? getBoundsOfRects(getRectOfNodes($nodes, $nodeOrigin), viewBB) : viewBB;
$: elementWidth = (style?.width as number) ?? defaultWidth; $: elementWidth = (style?.width as number) ?? defaultWidth;
$: elementHeight = (style?.height as number) ?? defaultHeight; $: elementHeight = (style?.height as number) ?? defaultHeight;
$: scaledWidth = boundingRect.width / elementWidth; $: scaledWidth = boundingRect.width / elementWidth;
@@ -55,7 +58,11 @@
$: viewboxHeight = viewHeight + offset * 2; $: viewboxHeight = viewHeight + offset * 2;
</script> </script>
<Panel position={position} class={cc(['react-flow__minimap', className])} style={`background-color: ${bgColor};`}> <Panel
{position}
class={cc(['react-flow__minimap', className])}
style={`background-color: ${bgColor};`}
>
<svg <svg
width={elementWidth} width={elementWidth}
height={elementHeight} height={elementHeight}
@@ -65,7 +72,7 @@
> >
{#if ariaLabel}<title id={labelledBy}>{ariaLabel}</title>{/if} {#if ariaLabel}<title id={labelledBy}>{ariaLabel}</title>{/if}
{#each $nodes as node(node.id)} {#each $nodes as node (node.id)}
{#if node.width && node.height} {#if node.width && node.height}
{@const pos = getNodePositionWithOrigin(node, $nodeOrigin).positionAbsolute} {@const pos = getNodePositionWithOrigin(node, $nodeOrigin).positionAbsolute}
<MinimapNode <MinimapNode
@@ -77,7 +84,7 @@
borderRadius={nodeBorderRadius} borderRadius={nodeBorderRadius}
strokeColor={nodeStrokeColorFunc(node)} strokeColor={nodeStrokeColorFunc(node)}
strokeWidth={nodeStrokeWidth} strokeWidth={nodeStrokeWidth}
shapeRendering={shapeRendering} {shapeRendering}
class={nodeClassNameFunc(node)} class={nodeClassNameFunc(node)}
style={{}} style={{}}
/> />
@@ -85,7 +92,9 @@
{/each} {/each}
<path <path
class="react-flow__minimap-mask" class="react-flow__minimap-mask"
d={`M${x - offset},${y - offset}h${viewboxWidth + offset * 2}v${viewboxHeight + offset * 2}h${-viewboxWidth - offset * 2}z d={`M${x - offset},${y - offset}h${viewboxWidth + offset * 2}v${viewboxHeight + offset * 2}h${
-viewboxWidth - offset * 2
}z
M${viewBB.x},${viewBB.y}h${viewBB.width}v${viewBB.height}h${-viewBB.width}z`} M${viewBB.x},${viewBB.y}h${viewBB.width}v${viewBB.height}h${-viewBB.width}z`}
fill={maskColor} fill={maskColor}
fill-rule="evenodd" fill-rule="evenodd"
@@ -20,13 +20,13 @@
<rect <rect
class={cc(['react-flow__minimap-node', className])} class={cc(['react-flow__minimap-node', className])}
x={x} {x}
y={y} {y}
rx={borderRadius} rx={borderRadius}
ry={borderRadius} ry={borderRadius}
width={width} {width}
height={height} {height}
fill={fill} {fill}
stroke={strokeColor} stroke={strokeColor}
stroke-width={strokeWidth} stroke-width={strokeWidth}
shape-rendering={shapeRendering} shape-rendering={shapeRendering}
@@ -5,72 +5,72 @@ import type { SvelteFlowStoreState } from './types';
import { derived } from 'svelte/store'; import { derived } from 'svelte/store';
const oppositePosition = { const oppositePosition = {
[Position.Left]: Position.Right, [Position.Left]: Position.Right,
[Position.Right]: Position.Left, [Position.Right]: Position.Left,
[Position.Top]: Position.Bottom, [Position.Top]: Position.Bottom,
[Position.Bottom]: Position.Top [Position.Bottom]: Position.Top
}; };
export function getConnectionPath(store: SvelteFlowStoreState) { export function getConnectionPath(store: SvelteFlowStoreState) {
return derived( return derived(
[ [
store.connection, store.connection,
store.connectionLineType, store.connectionLineType,
store.connectionMode, store.connectionMode,
store.nodes, store.nodes,
store.transform store.transform
], ],
([$connection, $connectionLineType, $connectionMode, $nodes, $transform]) => { ([$connection, $connectionLineType, $connectionMode, $nodes, $transform]) => {
if (!$connection.nodeId) { if (!$connection.nodeId) {
return null; return null;
} }
const fromNode = $nodes.find((n) => n.id === $connection.nodeId); const fromNode = $nodes.find((n) => n.id === $connection.nodeId);
const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds; const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds;
const handleBoundsStrict = fromHandleBounds?.[$connection.handleType || 'source'] || []; const handleBoundsStrict = fromHandleBounds?.[$connection.handleType || 'source'] || [];
const handleBoundsLoose = handleBoundsStrict const handleBoundsLoose = handleBoundsStrict
? handleBoundsStrict ? handleBoundsStrict
: fromHandleBounds?.[$connection.handleType === 'source' ? 'target' : 'source']!; : fromHandleBounds?.[$connection.handleType === 'source' ? 'target' : 'source']!;
const handleBounds = const handleBounds =
$connectionMode === ConnectionMode.Strict ? handleBoundsStrict : handleBoundsLoose; $connectionMode === ConnectionMode.Strict ? handleBoundsStrict : handleBoundsLoose;
const fromHandle = $connection.handleId const fromHandle = $connection.handleId
? handleBounds.find((d) => d.id === $connection.handleId) ? handleBounds.find((d) => d.id === $connection.handleId)
: handleBounds[0]; : handleBounds[0];
const fromHandleX = fromHandle const fromHandleX = fromHandle
? fromHandle.x + fromHandle.width / 2 ? fromHandle.x + fromHandle.width / 2
: (fromNode?.width ?? 0) / 2; : (fromNode?.width ?? 0) / 2;
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode?.height ?? 0; const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode?.height ?? 0;
const fromX = (fromNode?.positionAbsolute?.x ?? 0) + fromHandleX; const fromX = (fromNode?.positionAbsolute?.x ?? 0) + fromHandleX;
const fromY = (fromNode?.positionAbsolute?.y ?? 0) + fromHandleY; const fromY = (fromNode?.positionAbsolute?.y ?? 0) + fromHandleY;
const fromPosition = fromHandle?.position; const fromPosition = fromHandle?.position;
const toPosition = fromPosition ? oppositePosition[fromPosition] : undefined; const toPosition = fromPosition ? oppositePosition[fromPosition] : undefined;
const pathParams = { const pathParams = {
sourceX: fromX, sourceX: fromX,
sourceY: fromY, sourceY: fromY,
sourcePosition: fromPosition, sourcePosition: fromPosition,
targetX: (($connection.position?.x ?? 0) - $transform[0]) / $transform[2], targetX: (($connection.position?.x ?? 0) - $transform[0]) / $transform[2],
targetY: (($connection.position?.y ?? 0) - $transform[1]) / $transform[2], targetY: (($connection.position?.y ?? 0) - $transform[1]) / $transform[2],
targetPosition: toPosition targetPosition: toPosition
}; };
let path = ''; let path = '';
if ($connectionLineType === ConnectionLineType.Bezier) { if ($connectionLineType === ConnectionLineType.Bezier) {
// we assume the destination position is opposite to the source position // we assume the destination position is opposite to the source position
[path] = getBezierPath(pathParams); [path] = getBezierPath(pathParams);
} else if ($connectionLineType === ConnectionLineType.Step) { } else if ($connectionLineType === ConnectionLineType.Step) {
[path] = getSmoothStepPath({ [path] = getSmoothStepPath({
...pathParams, ...pathParams,
borderRadius: 0 borderRadius: 0
}); });
} else if ($connectionLineType === ConnectionLineType.SmoothStep) { } else if ($connectionLineType === ConnectionLineType.SmoothStep) {
[path] = getSmoothStepPath(pathParams); [path] = getSmoothStepPath(pathParams);
} else { } else {
[path] = getStraightPath(pathParams); [path] = getStraightPath(pathParams);
} }
return path; return path;
} }
); );
} }
+45 -45
View File
@@ -6,56 +6,56 @@ import type { WrapEdgeProps } from '$lib/types';
import type { SvelteFlowStoreState } from './types'; import type { SvelteFlowStoreState } from './types';
export function getEdgesLayouted(store: SvelteFlowStoreState) { export function getEdgesLayouted(store: SvelteFlowStoreState) {
return derived([store.edges, store.nodes], ([$edges, $nodes]) => { return derived([store.edges, store.nodes], ([$edges, $nodes]) => {
return $edges return $edges
.map((edge) => { .map((edge) => {
const sourceNode = $nodes.find((node) => node.id === edge.source); const sourceNode = $nodes.find((node) => node.id === edge.source);
const targetNode = $nodes.find((node) => node.id === edge.target); const targetNode = $nodes.find((node) => node.id === edge.target);
const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(sourceNode); const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(sourceNode);
const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(targetNode); const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(targetNode);
if (!sourceIsValid || !targetIsValid) { if (!sourceIsValid || !targetIsValid) {
return null; return null;
} }
const edgeType = edge.type || 'default'; const edgeType = edge.type || 'default';
const targetNodeHandles = targetHandleBounds!.target; const targetNodeHandles = targetHandleBounds!.target;
const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle); const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle);
const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle); const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle);
const sourcePosition = sourceHandle?.position || Position.Bottom; const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top; const targetPosition = targetHandle?.position || Position.Top;
if (!sourceHandle || !targetHandle) { if (!sourceHandle || !targetHandle) {
return null; return null;
} }
const { sourceX, sourceY, targetX, targetY } = getEdgePositions( const { sourceX, sourceY, targetX, targetY } = getEdgePositions(
sourceNodeRect, sourceNodeRect,
sourceHandle, sourceHandle,
sourcePosition, sourcePosition,
targetNodeRect, targetNodeRect,
targetHandle, targetHandle,
targetPosition targetPosition
); );
// we nee to do this to match the types // we nee to do this to match the types
const sourceHandleId = edge.sourceHandle; const sourceHandleId = edge.sourceHandle;
const targetHandleId = edge.targetHandle; const targetHandleId = edge.targetHandle;
return { return {
...edge, ...edge,
type: edgeType, type: edgeType,
sourceX, sourceX,
sourceY, sourceY,
targetX, targetX,
targetY, targetY,
sourcePosition, sourcePosition,
targetPosition, targetPosition,
sourceHandleId, sourceHandleId,
targetHandleId targetHandleId
}; };
}) })
.filter((e) => e !== null) as WrapEdgeProps[]; .filter((e) => e !== null) as WrapEdgeProps[];
}); });
} }
+262 -262
View File
@@ -1,23 +1,23 @@
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { import {
type Transform, type Transform,
type NodeDragItem, type NodeDragItem,
type NodeDimensionUpdate, type NodeDimensionUpdate,
internalsSymbol, internalsSymbol,
type NodeOrigin, type NodeOrigin,
type ViewportHelperFunctionOptions, type ViewportHelperFunctionOptions,
type Node as RFNode, type Node as RFNode,
type Connection, type Connection,
type XYPosition, type XYPosition,
type CoordinateExtent type CoordinateExtent
} from '@reactflow/system'; } from '@reactflow/system';
import { import {
fitView as fitViewUtil, fitView as fitViewUtil,
getConnectedEdges, getConnectedEdges,
getD3Transition, getD3Transition,
getDimensions, getDimensions,
addEdge as addEdgeUtil addEdge as addEdgeUtil
} from '@reactflow/utils'; } from '@reactflow/utils';
import { zoomIdentity } from 'd3-zoom'; import { zoomIdentity } from 'd3-zoom';
@@ -32,307 +32,307 @@ import type { SvelteFlowStore } from './types';
export const key = Symbol(); export const key = Symbol();
type CreateStoreProps = { type CreateStoreProps = {
fitView?: boolean; fitView?: boolean;
nodeOrigin?: NodeOrigin; nodeOrigin?: NodeOrigin;
transform?: Transform; transform?: Transform;
nodeTypes?: NodeTypes; nodeTypes?: NodeTypes;
edgeTypes?: EdgeTypes; edgeTypes?: EdgeTypes;
id?: string; id?: string;
}; };
export function createStore({ export function createStore({
transform = [0, 0, 1], transform = [0, 0, 1],
nodeOrigin = [0, 0], nodeOrigin = [0, 0],
fitView: fitViewOnInit = false, fitView: fitViewOnInit = false,
nodeTypes = {}, nodeTypes = {},
edgeTypes = {}, edgeTypes = {},
id = '1' id = '1'
}: CreateStoreProps): SvelteFlowStore { }: CreateStoreProps): SvelteFlowStore {
const store = { const store = {
...initialStoreState ...initialStoreState
}; };
let fitViewOnInitDone = false; let fitViewOnInitDone = false;
function setEdges(edges: Edge[]) { function setEdges(edges: Edge[]) {
store.edges.set(edges); store.edges.set(edges);
} }
function addEdge(edgeParams: Edge | Connection) { function addEdge(edgeParams: Edge | Connection) {
const edges = get(store.edges); const edges = get(store.edges);
store.edges.set(addEdgeUtil(edgeParams, edges)); store.edges.set(addEdgeUtil(edgeParams, edges));
} }
function setNodes(nodes: Node[]) { function setNodes(nodes: Node[]) {
store.nodes.update((currentNodes) => { store.nodes.update((currentNodes) => {
const nextNodes = nodes.map((n) => { const nextNodes = nodes.map((n) => {
const currentNode = currentNodes.find((cn) => cn.id === n.id) || {}; const currentNode = currentNodes.find((cn) => cn.id === n.id) || {};
return { return {
...currentNode, ...currentNode,
...n, ...n,
positionAbsolute: n.position positionAbsolute: n.position
}; };
}); });
return nextNodes; return nextNodes;
}); });
} }
function updateNodePositions(nodeDragItems: NodeDragItem[], dragging = false) { function updateNodePositions(nodeDragItems: NodeDragItem[], dragging = false) {
store.nodes.update((nds) => { store.nodes.update((nds) => {
return nds.map((n) => { return nds.map((n) => {
const nodeDragItem = nodeDragItems.find((ndi) => ndi.id === n.id); const nodeDragItem = nodeDragItems.find((ndi) => ndi.id === n.id);
if (nodeDragItem) { if (nodeDragItem) {
return { return {
...n, ...n,
dragging, dragging,
positionAbsolute: nodeDragItem.positionAbsolute, positionAbsolute: nodeDragItem.positionAbsolute,
position: nodeDragItem.position position: nodeDragItem.position
}; };
} }
return n; return n;
}); });
}); });
} }
function updateNodeDimensions(updates: NodeDimensionUpdate[]) { function updateNodeDimensions(updates: NodeDimensionUpdate[]) {
const viewportNode = document?.querySelector('.react-flow__viewport'); const viewportNode = document?.querySelector('.react-flow__viewport');
if (!viewportNode) { if (!viewportNode) {
return; return;
} }
const style = window.getComputedStyle(viewportNode); const style = window.getComputedStyle(viewportNode);
const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform); const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform);
const nextNodes = get(store.nodes).map((node) => { const nextNodes = get(store.nodes).map((node) => {
const update = updates.find((u) => u.id === node.id); const update = updates.find((u) => u.id === node.id);
if (update) { if (update) {
const dimensions = getDimensions(update.nodeElement); const dimensions = getDimensions(update.nodeElement);
const doUpdate = !!( const doUpdate = !!(
dimensions.width && dimensions.width &&
dimensions.height && dimensions.height &&
(node.width !== dimensions.width || (node.width !== dimensions.width ||
node.height !== dimensions.height || node.height !== dimensions.height ||
update.forceUpdate) update.forceUpdate)
); );
if (doUpdate) { if (doUpdate) {
node[internalsSymbol] = { node[internalsSymbol] = {
...node[internalsSymbol], ...node[internalsSymbol],
handleBounds: { handleBounds: {
source: getHandleBounds('.source', update.nodeElement, zoom), source: getHandleBounds('.source', update.nodeElement, zoom),
target: getHandleBounds('.target', update.nodeElement, zoom) target: getHandleBounds('.target', update.nodeElement, zoom)
} }
}; };
node.width = dimensions.width; node.width = dimensions.width;
node.height = dimensions.height; node.height = dimensions.height;
} }
} }
return node; return node;
}); });
const { zoom: d3Zoom, selection: d3Selection } = get(store.d3); const { zoom: d3Zoom, selection: d3Selection } = get(store.d3);
fitViewOnInitDone = fitViewOnInitDone =
fitViewOnInitDone || (fitViewOnInit && !!d3Zoom && !!d3Selection && fitView()); fitViewOnInitDone || (fitViewOnInit && !!d3Zoom && !!d3Selection && fitView());
store.nodes.set(nextNodes); store.nodes.set(nextNodes);
} }
function zoomIn(options?: ViewportHelperFunctionOptions) { function zoomIn(options?: ViewportHelperFunctionOptions) {
const { zoom: d3Zoom, selection: d3Selection } = get(store.d3); const { zoom: d3Zoom, selection: d3Selection } = get(store.d3);
if (d3Zoom && d3Selection) { if (d3Zoom && d3Selection) {
d3Zoom.scaleBy(getD3Transition(d3Selection, options?.duration), 1.2); d3Zoom.scaleBy(getD3Transition(d3Selection, options?.duration), 1.2);
} }
} }
function zoomOut(options?: ViewportHelperFunctionOptions) { function zoomOut(options?: ViewportHelperFunctionOptions) {
const { zoom: d3Zoom, selection: d3Selection } = get(store.d3); const { zoom: d3Zoom, selection: d3Selection } = get(store.d3);
if (d3Zoom && d3Selection) { if (d3Zoom && d3Selection) {
d3Zoom.scaleBy(getD3Transition(d3Selection, options?.duration), 1 / 1.2); d3Zoom.scaleBy(getD3Transition(d3Selection, options?.duration), 1 / 1.2);
} }
} }
function fitView() { function fitView() {
const { zoom: d3Zoom, selection: d3Selection } = get(store.d3); const { zoom: d3Zoom, selection: d3Selection } = get(store.d3);
if (!d3Zoom || !d3Selection) { if (!d3Zoom || !d3Selection) {
return false; return false;
} }
return fitViewUtil( return fitViewUtil(
{ {
nodes: get(store.nodes) as RFNode[], nodes: get(store.nodes) as RFNode[],
width: get(store.width), width: get(store.width),
height: get(store.height), height: get(store.height),
minZoom: 0.2, minZoom: 0.2,
maxZoom: 2, maxZoom: 2,
d3Selection, d3Selection,
d3Zoom, d3Zoom,
nodeOrigin: get(store.nodeOrigin) nodeOrigin: get(store.nodeOrigin)
}, },
{} {}
); );
} }
function resetSelectedItem<T extends Node | Edge>(item: T) { function resetSelectedItem<T extends Node | Edge>(item: T) {
if (item.selected) { if (item.selected) {
return { return {
...item, ...item,
selected: false selected: false
}; };
} }
return item; return item;
} }
function resetSelectedElements() { function resetSelectedElements() {
store.nodes.update((ns) => ns.map(resetSelectedItem)); store.nodes.update((ns) => ns.map(resetSelectedItem));
store.edges.update((es) => es.map(resetSelectedItem)); store.edges.update((es) => es.map(resetSelectedItem));
} }
store.deleteKeyPressed.subscribe((deleteKeyPressed) => { store.deleteKeyPressed.subscribe((deleteKeyPressed) => {
if (deleteKeyPressed) { if (deleteKeyPressed) {
const nodes = get(store.nodes); const nodes = get(store.nodes);
const edges = get(store.edges); const edges = get(store.edges);
const selectedNodes = nodes.filter((node) => node.selected); const selectedNodes = nodes.filter((node) => node.selected);
const selectedEdges = edges.filter((edge) => edge.selected); const selectedEdges = edges.filter((edge) => edge.selected);
// @todo can we put this stuff into @reactflow/utils? // @todo can we put this stuff into @reactflow/utils?
const nodeIds = selectedNodes.map((node) => node.id); const nodeIds = selectedNodes.map((node) => node.id);
const edgeIds = selectedEdges.map((edge) => edge.id); const edgeIds = selectedEdges.map((edge) => edge.id);
const nodesToRemove = nodes.reduce<Node[]>((res, node) => { const nodesToRemove = nodes.reduce<Node[]>((res, node) => {
const parentHit = const parentHit =
!nodeIds.includes(node.id) && !nodeIds.includes(node.id) &&
node.parentNode && node.parentNode &&
res.find((n) => n.id === node.parentNode); res.find((n) => n.id === node.parentNode);
const deletable = typeof node.deletable === 'boolean' ? node.deletable : true; const deletable = typeof node.deletable === 'boolean' ? node.deletable : true;
if (deletable && (nodeIds.includes(node.id) || parentHit)) { if (deletable && (nodeIds.includes(node.id) || parentHit)) {
res.push(node); res.push(node);
} }
return res; return res;
}, []); }, []);
const deletableEdges = edges.filter((e) => const deletableEdges = edges.filter((e) =>
typeof e.deletable === 'boolean' ? e.deletable : true typeof e.deletable === 'boolean' ? e.deletable : true
); );
const initialHitEdges = deletableEdges.filter((e) => edgeIds.includes(e.id)); const initialHitEdges = deletableEdges.filter((e) => edgeIds.includes(e.id));
if (nodesToRemove || initialHitEdges) { if (nodesToRemove || initialHitEdges) {
const connectedEdges = getConnectedEdges(nodesToRemove as RFNode[], deletableEdges); const connectedEdges = getConnectedEdges(nodesToRemove as RFNode[], deletableEdges);
const edgesToRemove = [...initialHitEdges, ...connectedEdges]; const edgesToRemove = [...initialHitEdges, ...connectedEdges];
const edgeIdsToRemove = edgesToRemove.reduce<string[]>((res, edge) => { const edgeIdsToRemove = edgesToRemove.reduce<string[]>((res, edge) => {
if (!res.includes(edge.id)) { if (!res.includes(edge.id)) {
res.push(edge.id); res.push(edge.id);
} }
return res; return res;
}, []); }, []);
store.nodes.update((nds) => nds.filter((node) => !nodeIds.includes(node.id))); store.nodes.update((nds) => nds.filter((node) => !nodeIds.includes(node.id)));
store.edges.update((eds) => eds.filter((edge) => !edgeIdsToRemove.includes(edge.id))); store.edges.update((eds) => eds.filter((edge) => !edgeIdsToRemove.includes(edge.id)));
} }
} }
}); });
function addSelectedNodes(ids: string[]) { function addSelectedNodes(ids: string[]) {
store.selectionRect.set(null); store.selectionRect.set(null);
store.selectionRectMode.set(null); store.selectionRectMode.set(null);
if (get(store.multiselectionKeyPressed)) { if (get(store.multiselectionKeyPressed)) {
// @todo handle multiselection key // @todo handle multiselection key
} }
store.nodes.update((ns) => store.nodes.update((ns) =>
ns.map((node) => { ns.map((node) => {
return { return {
...node, ...node,
selected: ids.includes(node.id) selected: ids.includes(node.id)
}; };
}) })
); );
} }
function panBy(delta: XYPosition) { function panBy(delta: XYPosition) {
const { zoom: d3Zoom, selection: d3Selection } = get(store.d3); const { zoom: d3Zoom, selection: d3Selection } = get(store.d3);
const transform = get(store.transform); const transform = get(store.transform);
const width = get(store.width); const width = get(store.width);
const height = get(store.height); const height = get(store.height);
if (!d3Zoom || !d3Selection || (!delta.x && !delta.y)) { if (!d3Zoom || !d3Selection || (!delta.x && !delta.y)) {
return; return;
} }
const nextTransform = zoomIdentity const nextTransform = zoomIdentity
.translate(transform[0] + delta.x, transform[1] + delta.y) .translate(transform[0] + delta.x, transform[1] + delta.y)
.scale(transform[2]); .scale(transform[2]);
const extent: CoordinateExtent = [ const extent: CoordinateExtent = [
[0, 0], [0, 0],
[width, height] [width, height]
]; ];
const constrainedTransform = d3Zoom?.constrain()(nextTransform, extent, [ const constrainedTransform = d3Zoom?.constrain()(nextTransform, extent, [
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY], [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY] [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY]
]); ]);
d3Zoom.transform(d3Selection, constrainedTransform); d3Zoom.transform(d3Selection, constrainedTransform);
} }
function updateConnection(connectionUpdate: Partial<ConnectionData> | null) { function updateConnection(connectionUpdate: Partial<ConnectionData> | null) {
const currentConnectionData = get(store.connection); const currentConnectionData = get(store.connection);
const nextConnectionData = currentConnectionData const nextConnectionData = currentConnectionData
? { ? {
...initConnectionData, ...initConnectionData,
...currentConnectionData, ...currentConnectionData,
...connectionUpdate ...connectionUpdate
} }
: { : {
...initConnectionData, ...initConnectionData,
...connectionUpdate ...connectionUpdate
}; };
store.connection.set(nextConnectionData); store.connection.set(nextConnectionData);
} }
function cancelConnection() { function cancelConnection() {
updateConnection(initConnectionData); updateConnection(initConnectionData);
} }
return { return {
// state // state
...store, ...store,
// derived state // derived state
edgesLayouted: getEdgesLayouted(store), edgesLayouted: getEdgesLayouted(store),
connectionPath: getConnectionPath(store), connectionPath: getConnectionPath(store),
// actions // actions
setNodes, setNodes,
setEdges, setEdges,
addEdge, addEdge,
updateNodePositions, updateNodePositions,
updateNodeDimensions, updateNodeDimensions,
zoomIn, zoomIn,
zoomOut, zoomOut,
fitView, fitView,
resetSelectedElements, resetSelectedElements,
addSelectedNodes, addSelectedNodes,
panBy, panBy,
updateConnection, updateConnection,
cancelConnection cancelConnection
}; };
} }
export function useStore(): SvelteFlowStore { export function useStore(): SvelteFlowStore {
const { getStore } = getContext<{ getStore: () => SvelteFlowStore }>(key); const { getStore } = getContext<{ getStore: () => SvelteFlowStore }>(key);
return getStore(); return getStore();
} }
+48 -48
View File
@@ -1,13 +1,13 @@
import { readable, writable } from 'svelte/store'; import { readable, writable } from 'svelte/store';
import { import {
SelectionMode, SelectionMode,
type D3ZoomInstance, type D3ZoomInstance,
type D3SelectionInstance, type D3SelectionInstance,
ConnectionMode, ConnectionMode,
ConnectionLineType, ConnectionLineType,
type Transform, type Transform,
type NodeOrigin, type NodeOrigin,
type Rect type Rect
} from '@reactflow/system'; } from '@reactflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte'; import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
@@ -19,47 +19,47 @@ import StraightEdge from '$lib/components/edges/StraightEdge.svelte';
import SmoothStepEdge from '$lib/components/edges/SmoothStepEdge.svelte'; import SmoothStepEdge from '$lib/components/edges/SmoothStepEdge.svelte';
export const initConnectionData = { export const initConnectionData = {
nodeId: null, nodeId: null,
handleId: null, handleId: null,
handleType: null, handleType: null,
position: null, position: null,
status: null status: null
}; };
export const initialStoreState = { export const initialStoreState = {
nodes: writable<Node[]>([]), nodes: writable<Node[]>([]),
edges: writable<Edge[]>([]), edges: writable<Edge[]>([]),
edgesLayouted: readable<Edge[]>([]), edgesLayouted: readable<Edge[]>([]),
height: writable<number>(500), height: writable<number>(500),
width: writable<number>(500), width: writable<number>(500),
nodeOrigin: writable<NodeOrigin>([0.5, 0.5]), nodeOrigin: writable<NodeOrigin>([0.5, 0.5]),
d3: writable<{ zoom: D3ZoomInstance | null; selection: D3SelectionInstance | null }>({ d3: writable<{ zoom: D3ZoomInstance | null; selection: D3SelectionInstance | null }>({
zoom: null, zoom: null,
selection: null selection: null
}), }),
id: writable<string | null>(null), id: writable<string | null>(null),
dragging: writable<boolean>(false), dragging: writable<boolean>(false),
selectionRect: writable<(Rect & { startX: number; startY: number }) | null>(null), selectionRect: writable<(Rect & { startX: number; startY: number }) | null>(null),
selectionKeyPressed: writable<boolean>(false), selectionKeyPressed: writable<boolean>(false),
multiselectionKeyPressed: writable<boolean>(false), multiselectionKeyPressed: writable<boolean>(false),
deleteKeyPressed: writable<boolean>(false), deleteKeyPressed: writable<boolean>(false),
selectionRectMode: writable<string | null>(null), selectionRectMode: writable<string | null>(null),
selectionMode: writable<SelectionMode>(SelectionMode.Partial), selectionMode: writable<SelectionMode>(SelectionMode.Partial),
nodeTypes: writable<NodeTypes>({ nodeTypes: writable<NodeTypes>({
input: InputNode, input: InputNode,
output: OutputNode, output: OutputNode,
default: DefaultNode default: DefaultNode
}), }),
edgeTypes: writable<EdgeTypes>({ edgeTypes: writable<EdgeTypes>({
straight: StraightEdge, straight: StraightEdge,
smoothstep: SmoothStepEdge, smoothstep: SmoothStepEdge,
default: BezierEdge default: BezierEdge
}), }),
transform: writable<Transform>([0, 0, 1]), transform: writable<Transform>([0, 0, 1]),
connectionMode: writable<ConnectionMode>(ConnectionMode.Strict), connectionMode: writable<ConnectionMode>(ConnectionMode.Strict),
domNode: writable<HTMLDivElement | null>(null), domNode: writable<HTMLDivElement | null>(null),
connectionPath: readable<string | null>(null), connectionPath: readable<string | null>(null),
connection: writable<ConnectionData>(initConnectionData), connection: writable<ConnectionData>(initConnectionData),
connectionRadius: writable<number>(25), connectionRadius: writable<number>(25),
connectionLineType: writable<ConnectionLineType>(ConnectionLineType.Bezier) connectionLineType: writable<ConnectionLineType>(ConnectionLineType.Bezier)
}; };
+22 -22
View File
@@ -1,32 +1,32 @@
import type { import type {
NodeDimensionUpdate, NodeDimensionUpdate,
XYPosition, XYPosition,
ViewportHelperFunctionOptions, ViewportHelperFunctionOptions,
Connection, Connection,
NodeDragItem NodeDragItem
} from '@reactflow/system'; } from '@reactflow/system';
import { initialStoreState } from './initial-store'; import { initialStoreState } from './initial-store';
import type { Node, Edge, ConnectionData } from '$lib/types'; import type { Node, Edge, ConnectionData } from '$lib/types';
export type SvelteFlowStoreActions = { export type SvelteFlowStoreActions = {
setNodes: (nodes: Node[]) => void; setNodes: (nodes: Node[]) => void;
setEdges: (edges: Edge[]) => void; setEdges: (edges: Edge[]) => void;
addEdge: (edge: Edge | Connection) => void; addEdge: (edge: Edge | Connection) => void;
zoomIn: (options?: ViewportHelperFunctionOptions) => void; zoomIn: (options?: ViewportHelperFunctionOptions) => void;
zoomOut: (options?: ViewportHelperFunctionOptions) => void; zoomOut: (options?: ViewportHelperFunctionOptions) => void;
fitView: (options?: ViewportHelperFunctionOptions) => boolean; fitView: (options?: ViewportHelperFunctionOptions) => boolean;
updateNodePositions: ( updateNodePositions: (
nodeDragItems: NodeDragItem[], nodeDragItems: NodeDragItem[],
positionChanged?: boolean, positionChanged?: boolean,
dragging?: boolean dragging?: boolean
) => void; ) => void;
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void; updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
resetSelectedElements: () => void; resetSelectedElements: () => void;
addSelectedNodes: (ids: string[]) => void; addSelectedNodes: (ids: string[]) => void;
panBy: (delta: XYPosition) => void; panBy: (delta: XYPosition) => void;
updateConnection: (connection: Partial<ConnectionData>) => void; updateConnection: (connection: Partial<ConnectionData>) => void;
cancelConnection: () => void; cancelConnection: () => void;
}; };
export type SvelteFlowStoreState = typeof initialStoreState; export type SvelteFlowStoreState = typeof initialStoreState;
+15 -15
View File
@@ -7,25 +7,25 @@ export type KeyDefinitionObject = { key: string; modifier?: KeyModifier };
export type KeyDefinition = string | KeyDefinitionObject; export type KeyDefinition = string | KeyDefinitionObject;
export type ConnectionData = { export type ConnectionData = {
position: XYPosition | null; position: XYPosition | null;
nodeId: string | null; nodeId: string | null;
handleId: string | null; handleId: string | null;
handleType: HandleType | null; handleType: HandleType | null;
status: string | null; status: string | null;
}; };
export type ConnectionLineProps = {}; export type ConnectionLineProps = {};
export type SvelteFlowProps = { export type SvelteFlowProps = {
nodes: Node[]; nodes: Node[];
edges: Edge[]; edges: Edge[];
connectionLineType?: ConnectionLineType; connectionLineType?: ConnectionLineType;
selectionKey?: KeyDefinition; selectionKey?: KeyDefinition;
deleteKey?: KeyDefinition; deleteKey?: KeyDefinition;
nodeTypes?: NodeTypes; nodeTypes?: NodeTypes;
fitView?: boolean; fitView?: boolean;
class?: string; class?: string;
style?: string; style?: string;
id?: string; id?: string;
}; };
+6 -6
View File
@@ -1,10 +1,10 @@
export type { export type {
Position, Position,
XYPosition, XYPosition,
Edge, Edge,
BaseEdgeProps, BaseEdgeProps,
WrapEdgeProps, WrapEdgeProps,
EdgeProps EdgeProps
} from '@reactflow/system'; } from '@reactflow/system';
export * from './nodes'; export * from './nodes';
+27 -27
View File
@@ -5,43 +5,43 @@ import type { internalsSymbol, NodeHandleBounds, Position, XYPosition } from '@r
// we need a base node type or helpes that accept Node like types // we need a base node type or helpes that accept Node like types
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Node<NodeData = any> = { export type Node<NodeData = any> = {
id: string; id: string;
type: string; type: string;
data: NodeData; data: NodeData;
position: XYPosition; position: XYPosition;
sourcePosition?: Position; sourcePosition?: Position;
targetPosition?: Position; targetPosition?: Position;
positionAbsolute?: XYPosition; positionAbsolute?: XYPosition;
width?: number; width?: number;
height?: number; height?: number;
selected?: boolean; selected?: boolean;
class?: string; class?: string;
style?: string; style?: string;
deletable?: boolean; deletable?: boolean;
// not supported yet // not supported yet
parentNode?: string; parentNode?: string;
// only used internally // only used internally
[internalsSymbol]?: { [internalsSymbol]?: {
z?: number; z?: number;
handleBounds?: NodeHandleBounds; handleBounds?: NodeHandleBounds;
isParent?: boolean; isParent?: boolean;
}; };
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export type NodeProps<NodeData = any> = Pick< export type NodeProps<NodeData = any> = Pick<
Node<NodeData>, Node<NodeData>,
'id' | 'data' | 'selected' | 'sourcePosition' | 'targetPosition' 'id' | 'data' | 'selected' | 'sourcePosition' | 'targetPosition'
> & { > & {
xPos: number; xPos: number;
yPos: number; yPos: number;
isConnectable?: boolean; isConnectable?: boolean;
}; };
export type WrapNodeProps = Node & { export type WrapNodeProps = Node & {
resizeObserver?: ResizeObserver | null; resizeObserver?: ResizeObserver | null;
}; };
export type NodeTypes = Record<string, typeof SvelteComponentTyped<Partial<NodeProps>>>; export type NodeTypes = Record<string, typeof SvelteComponentTyped<Partial<NodeProps>>>;
+70 -57
View File
@@ -1,10 +1,16 @@
<script lang="ts"> <script lang="ts">
import type { Node, Edge, NodeTypes } from '../lib/types'; import type { Node, Edge, NodeTypes } from '../lib/types';
import SvelteFlow, { Controls, Background, BackgroundVariant, Minimap, Panel } from '../lib/index'; import SvelteFlow, {
Controls,
Background,
BackgroundVariant,
Minimap,
Panel
} from '../lib/index';
import CustomNode from '../customnodes/Custom.svelte'; import CustomNode from '../customnodes/Custom.svelte';
const nodeTypes: NodeTypes = { const nodeTypes: NodeTypes = {
custom: CustomNode, custom: CustomNode
}; };
// const yNodes = 10; // const yNodes = 10;
@@ -13,8 +19,6 @@
// const nodes: Node[] = []; // const nodes: Node[] = [];
// const edges: Edge[] = []; // const edges: Edge[] = [];
// let source = null; // let source = null;
// for (let y = 0; y < yNodes; y++) { // for (let y = 0; y < yNodes; y++) {
@@ -43,60 +47,69 @@
// } // }
// } // }
let nodes: Node<{ label: string }>[] = [{ let nodes: Node<{ label: string }>[] = [
id: '1', {
type: 'input', id: '1',
data: { label: 'Input Node' }, type: 'input',
position: { x: 150, y: 5 } data: { label: 'Input Node' },
}, { position: { x: 150, y: 5 }
id: '2', },
type: 'default', {
data: { label: 'Node' }, id: '2',
position: { x: 0, y: 150 }, type: 'default',
}, { data: { label: 'Node' },
id: 'A', position: { x: 0, y: 150 }
type: 'default', },
data: { label: 'Styled with class' }, {
class: 'custom-style', id: 'A',
position: { x: 150, y: 150 }, type: 'default',
}, { data: { label: 'Styled with class' },
id: '3', class: 'custom-style',
type: 'output', position: { x: 150, y: 150 }
data: { label: 'Output Node' }, },
position: { x: 300, y: 150 }, {
}, id: '3',
{ type: 'output',
id: 'B', data: { label: 'Output Node' },
type: 'default', position: { x: 300, y: 150 }
data: { label: 'Styled with style' }, },
style: 'border: 2px solid #ff5050;', {
position: { x: 450, y: 150 }, id: 'B',
}, type: 'default',
{ data: { label: 'Styled with style' },
id: '4', style: 'border: 2px solid #ff5050;',
type: 'custom', position: { x: 450, y: 150 }
data: { label: 'Custom Node' }, },
position: { x: 150, y: 300 }, {
}]; id: '4',
type: 'custom',
data: { label: 'Custom Node' },
position: { x: 150, y: 300 }
}
];
let edges: Edge[] = [{ let edges: Edge[] = [
id: '1-2', {
type: 'default', id: '1-2',
source: '1', type: 'default',
target: '2', source: '1',
label: 'Edge Text' target: '2',
}, { label: 'Edge Text'
id: '1-3', },
type: 'smoothstep', {
source: '1', id: '1-3',
target: '3' type: 'smoothstep',
}, { source: '1',
id: '2-4', target: '3'
type: 'default', },
source: '2', {
target: '4', id: '2-4',
animated: true type: 'default',
}]; source: '2',
target: '4',
animated: true
}
];
function updateNode() { function updateNode() {
nodes[0].position = { x: nodes[0].position.x + 20, y: nodes[0].position.y }; nodes[0].position = { x: nodes[0].position.x + 20, y: nodes[0].position.y };
+63 -63
View File
@@ -1,97 +1,97 @@
import { import {
type CoordinateExtent, type CoordinateExtent,
type Dimensions, type Dimensions,
type XYPosition, type XYPosition,
type Node, type Node,
type XYZPosition, type XYZPosition,
type HandleElement, type HandleElement,
internalsSymbol, internalsSymbol,
Position Position
} from '@reactflow/system'; } from '@reactflow/system';
export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max); export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max);
export const clampPosition = (position: XYPosition = { x: 0, y: 0 }, extent: CoordinateExtent) => ({ export const clampPosition = (position: XYPosition = { x: 0, y: 0 }, extent: CoordinateExtent) => ({
x: clamp(position.x, extent[0][0], extent[1][0]), x: clamp(position.x, extent[0][0], extent[1][0]),
y: clamp(position.y, extent[0][1], extent[1][1]) y: clamp(position.y, extent[0][1], extent[1][1])
}); });
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isNumeric = (n: any): n is number => !isNaN(n) && isFinite(n); export const isNumeric = (n: any): n is number => !isNaN(n) && isFinite(n);
export const getDimensions = (node: HTMLDivElement): Dimensions => ({ export const getDimensions = (node: HTMLDivElement): Dimensions => ({
width: node.offsetWidth, width: node.offsetWidth,
height: node.offsetHeight height: node.offsetHeight
}); });
type ParentNodes = Record<string, boolean>; type ParentNodes = Record<string, boolean>;
function calculateXYZPosition(node: Node, nodes: Node[], result: XYZPosition): XYZPosition { function calculateXYZPosition(node: Node, nodes: Node[], result: XYZPosition): XYZPosition {
if (!node.parentNode) { if (!node.parentNode) {
return result; return result;
} }
const parentNode = nodes.find((n) => n.id === node.parentNode)!; const parentNode = nodes.find((n) => n.id === node.parentNode)!;
const parentNodePosition = parentNode.positionAbsolute!; const parentNodePosition = parentNode.positionAbsolute!;
return calculateXYZPosition(parentNode, nodes, { return calculateXYZPosition(parentNode, nodes, {
x: (result.x ?? 0) + parentNodePosition.x, x: (result.x ?? 0) + parentNodePosition.x,
y: (result.y ?? 0) + parentNodePosition.y, y: (result.y ?? 0) + parentNodePosition.y,
z: z:
(parentNode[internalsSymbol]?.z ?? 0) > (result.z ?? 0) (parentNode[internalsSymbol]?.z ?? 0) > (result.z ?? 0)
? parentNode[internalsSymbol]?.z ?? 0 ? parentNode[internalsSymbol]?.z ?? 0
: result.z ?? 0 : result.z ?? 0
}); });
} }
export function updateAbsoluteNodePositions(nodes: Node[], parentNodes?: ParentNodes) { export function updateAbsoluteNodePositions(nodes: Node[], parentNodes?: ParentNodes) {
nodes.forEach((node) => { nodes.forEach((node) => {
if (node.parentNode) { if (node.parentNode) {
throw new Error(`Parent node ${node.parentNode} not found`); throw new Error(`Parent node ${node.parentNode} not found`);
} }
if (node.parentNode || parentNodes?.[node.id]) { if (node.parentNode || parentNodes?.[node.id]) {
const { x, y, z } = calculateXYZPosition(node, nodes, { const { x, y, z } = calculateXYZPosition(node, nodes, {
...node.position, ...node.position,
z: node[internalsSymbol]?.z ?? 0 z: node[internalsSymbol]?.z ?? 0
}); });
node.positionAbsolute = { node.positionAbsolute = {
x, x,
y y
}; };
node[internalsSymbol]!.z = z; node[internalsSymbol]!.z = z;
if (parentNodes?.[node.id]) { if (parentNodes?.[node.id]) {
node[internalsSymbol]!.isParent = true; node[internalsSymbol]!.isParent = true;
} }
} }
}); });
} }
export const getHandleBounds = ( export const getHandleBounds = (
selector: string, selector: string,
nodeElement: HTMLDivElement, nodeElement: HTMLDivElement,
zoom: number zoom: number
): HandleElement[] | null => { ): HandleElement[] | null => {
const handles = nodeElement.querySelectorAll(selector); const handles = nodeElement.querySelectorAll(selector);
if (!handles || !handles.length) { if (!handles || !handles.length) {
return null; return null;
} }
const handlesArray = Array.from(handles) as HTMLDivElement[]; const handlesArray = Array.from(handles) as HTMLDivElement[];
const nodeBounds = nodeElement.getBoundingClientRect(); const nodeBounds = nodeElement.getBoundingClientRect();
return handlesArray.map((handle): HandleElement => { return handlesArray.map((handle): HandleElement => {
const handleBounds = handle.getBoundingClientRect(); const handleBounds = handle.getBoundingClientRect();
return { return {
id: handle.getAttribute('data-handleid'), id: handle.getAttribute('data-handleid'),
position: handle.getAttribute('data-handlepos') as unknown as Position, position: handle.getAttribute('data-handlepos') as unknown as Position,
x: (handleBounds.left - nodeBounds.left) / zoom, x: (handleBounds.left - nodeBounds.left) / zoom,
y: (handleBounds.top - nodeBounds.top) / zoom, y: (handleBounds.top - nodeBounds.top) / zoom,
...getDimensions(handle) ...getDimensions(handle)
}; };
}); });
}; };
+6 -6
View File
@@ -3,13 +3,13 @@ import { vitePreprocess } from '@sveltejs/kit/vite';
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors // Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors // for more information about preprocessors
preprocess: vitePreprocess(), preprocess: vitePreprocess(),
kit: { kit: {
adapter: adapter() adapter: adapter()
} }
}; };
export default config; export default config;
+15 -15
View File
@@ -1,17 +1,17 @@
{ {
"extends": "./.svelte-kit/tsconfig.json", "extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"allowJs": true, "allowJs": true,
"checkJs": true, "checkJs": true,
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"skipLibCheck": true, "skipLibCheck": true,
"sourceMap": true, "sourceMap": true,
"strict": true "strict": true
} }
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// //
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in // from the referenced tsconfig.json - TypeScript does not merge them in
} }
+1 -1
View File
@@ -2,7 +2,7 @@ import { sveltekit } from '@sveltejs/kit/vite';
import type { UserConfig } from 'vite'; import type { UserConfig } from 'vite';
const config: UserConfig = { const config: UserConfig = {
plugins: [sveltekit()] plugins: [sveltekit()]
}; };
export default config; export default config;