feat(packages): add svelte version

This commit is contained in:
moklick
2023-02-01 09:41:09 +01:00
parent 079e380a31
commit 2af833aee1
30 changed files with 2279 additions and 21 deletions

View File

@@ -0,0 +1,13 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View File

@@ -0,0 +1,20 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
plugins: ['svelte3', '@typescript-eslint'],
ignorePatterns: ['*.cjs'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
settings: {
'svelte3/typescript': () => require('typescript')
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020
},
env: {
browser: true,
es2017: true,
node: true
}
};

10
packages/svelte/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
packages/svelte/.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=true

View File

@@ -0,0 +1,13 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View File

@@ -0,0 +1,9 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

38
packages/svelte/README.md Normal file
View File

@@ -0,0 +1,38 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

View File

@@ -0,0 +1,44 @@
{
"name": "svelte",
"version": "0.0.1",
"scripts": {
"dev": "vite dev",
"build": "svelte-kit sync && svelte-package",
"prepublishOnly": "echo 'Did you mean to publish `./package/`, instead of `./`?' && exit 1",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^1.0.0",
"@sveltejs/kit": "^1.0.0",
"@sveltejs/package": "^1.0.0",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte3": "^4.0.0",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.8.1",
"svelte": "^3.54.0",
"svelte-check": "^3.0.1",
"tslib": "^2.4.1",
"typescript": "^4.9.3",
"vite": "^4.0.0"
},
"type": "module",
"module": "index.ts",
"exports": {
".": "./index.js"
},
"dependencies": {
"@reactflow/core": "^11.5.1",
"cc": "^3.0.1",
"d3-drag": "^3.0.0",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}

12
packages/svelte/src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
}
export {};

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
html,
body,
#root {
height: 100%;
margin: 0;
}
</style>
%sveltekit.head%
</head>
<body>
<div id="root">%sveltekit.body%</div>
</body>
</html>

View File

@@ -0,0 +1,72 @@
<script lang="ts">
import { getContext } from 'svelte';
import cc from 'classcat';
import { type HandleType, Position } from '@reactflow/core';
export let type: HandleType = 'source';
export let position: Position = Position.Top;
export let id: string | null = null;
export let className: string | null = null;;
const isTarget = type === 'target';
const nodeId = getContext('rf_nodeid');
</script>
<div
data-handleid={id}
data-nodeid={nodeId}
data-handlepos={position}
data-id={`${nodeId}-${id}-${type}`}
class={cc([
'react-flow__handle',
`react-flow__handle-${position}`,
'nodrag',
className,
position
])}
class:source={!isTarget}
class:target={isTarget}
>
<slot></slot>
</div>
<style>
.react-flow__handle {
position: absolute;
pointer-events: none;
min-width: 5px;
min-height: 5px;
width: 6px;
height: 6px;
background: #1a192b;
border: 1px solid white;
border-radius: 100%;
}
.bottom {
top: auto;
left: 50%;
bottom: -4px;
transform: translate(-50%, 0);
}
.top {
left: 50%;
top: -4px;
transform: translate(-50%, 0);
}
.left {
top: 50%;
left: -4px;
transform: translate(0, -50%);
}
.right {
right: -4px;
top: 50%;
transform: translate(0, -50%);
}
</style>

View File

@@ -0,0 +1,29 @@
<script lang="ts">
export let id: string = '1';
export let type: string = 'default';
export let sourceX: number = 0;
export let sourceY: number = 0;
export let targetX: number = 0;
export let targetY: number = 0;
const path = `M ${sourceX},${sourceY}L ${targetX},${targetY}`;
</script>
<g
class="react-flow__edge"
data-id={id}
>
<path
d={path}
fill="none"
class="react-flow__edge-path"
/>
</g>
<style>
.react-flow__edge-path {
stroke: #ccc;
stroke-width: 1;
}
</style>

View File

@@ -0,0 +1,62 @@
<script lang="ts">
import { onMount, setContext } from 'svelte';
import type { XYPosition } from '@reactflow/core';
import drag from '$lib/hooks/drag'
import { useStore } from '$lib/store';
import DefaultNode from './DefaultNode.svelte';
export let id: string;
export let positionAbsolute: XYPosition = { x: 0, y: 0 };
export let dragging: boolean = false;
export let data: any = {};
export let resizeObserver: ResizeObserver | null = null;
export let style: any = {};
let nodeRef: HTMLDivElement;
const { nodesStore, transformStore, updateNodePositions } = useStore();
setContext('rf_nodeid', id);
onMount(() => {
resizeObserver?.observe(nodeRef);
return () => {
resizeObserver?.unobserve(nodeRef);
}
});
</script>
<div
use:drag={{ nodeId: id, nodesStore, transformStore, updateNodePositions }}
class="react-flow__node"
class:dragging={dragging}
bind:this={nodeRef}
style="transform: translate({positionAbsolute.x}px, {positionAbsolute.y}px);"
data-id={id}
>
<svelte:component this={DefaultNode} data={data} />
</div>
<style>
.react-flow__node {
padding: 10px;
border-radius: 3px;
width: var(--node-width);
font-size: 12px;
color: #222;
text-align: center;
border-width: 1px;
border-style: solid;
border-color: #1a192b;
background-color: white;
position: absolute;
cursor: grab;
}
.dragging {
cursor: grabbing;
}
</style>

View File

@@ -0,0 +1,15 @@
<script lang="ts">
import { Position } from '@reactflow/core';
import Handle from '$lib/components/Handle.svelte';
export let data: any = {};
export let isConnectable: boolean = true;
export let targetPosition: Position = Position.Top;
export let sourcePosition: Position = Position.Bottom;
</script>
<Handle type="target" position={targetPosition} />
{data?.label}
<Handle type="source" position={sourcePosition} />

View File

@@ -0,0 +1,171 @@
<script lang="ts">
type EdgePositions = {
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
}
import Edge from '$lib/components/edges/BaseEdge.svelte';
import { useStore } from '$lib/store';
import { type NodeHandleBounds, type Rect, type Node, type HandleElement, Position, type XYPosition } from '@reactflow/core';
import { internalsSymbol } from '@reactflow/core';
const { edgesStore, widthStore, heightStore, nodesStore } = useStore();
function getNodeData(node?: Node): [Rect, NodeHandleBounds | null, boolean] {
const handleBounds = node?.[internalsSymbol]?.handleBounds || null;
const isValid =
handleBounds &&
node?.width &&
node?.height &&
typeof node?.positionAbsolute?.x !== 'undefined' &&
typeof node?.positionAbsolute?.y !== 'undefined';
return [
{
x: node?.positionAbsolute?.x || 0,
y: node?.positionAbsolute?.y || 0,
width: node?.width || 0,
height: node?.height || 0,
},
handleBounds,
!!isValid,
];
}
function getHandlePosition(position: Position, nodeRect: Rect, handle: HandleElement | null = null): XYPosition {
const x = (handle?.x || 0) + nodeRect.x;
const y = (handle?.y || 0) + nodeRect.y;
const width = handle?.width || nodeRect.width;
const height = handle?.height || nodeRect.height;
switch (position) {
case Position.Top:
return {
x: x + width / 2,
y,
};
case Position.Right:
return {
x: x + width,
y: y + height / 2,
};
case Position.Bottom:
return {
x: x + width / 2,
y: y + height,
};
case Position.Left:
return {
x,
y: y + height / 2,
};
}
}
function getHandle(bounds: HandleElement[], handleId?: string | null): HandleElement | null {
if (!bounds) {
return null;
}
if (handleId) {
return bounds.find((d) => d.id === handleId)!;
} else if (bounds.length === 1) {
return bounds[0];
}
return null;
}
function getEdgePositions(
sourceNodeRect: Rect,
sourceHandle: HandleElement,
sourcePosition: Position,
targetNodeRect: Rect,
targetHandle: HandleElement,
targetPosition: Position
): EdgePositions {
const sourceHandlePos = getHandlePosition(sourcePosition, sourceNodeRect, sourceHandle);
const targetHandlePos = getHandlePosition(targetPosition, targetNodeRect, targetHandle);
return {
sourceX: sourceHandlePos.x,
sourceY: sourceHandlePos.y,
targetX: targetHandlePos.x,
targetY: targetHandlePos.y,
};
};
let edgesWithData: any = [];
$: {
$nodesStore
edgesWithData = $edgesStore.map((edge) => {
const sourceNode = $nodesStore.find((node) => node.id === edge.source);
const targetNode = $nodesStore.find((node) => node.id === edge.target);
const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(sourceNode);
const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(targetNode);
if (!sourceIsValid || !targetIsValid) {
return null;
}
let edgeType = edge.type || 'default';
const targetNodeHandles = targetHandleBounds!.target;
const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle);
const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle);
const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top;
if (!sourceHandle || !targetHandle) {
return null;
}
const { sourceX, sourceY, targetX, targetY } = getEdgePositions(
sourceNodeRect,
sourceHandle,
sourcePosition,
targetNodeRect,
targetHandle,
targetPosition
);
return {
id: edge.id,
type: edgeType,
sourceX,
sourceY,
targetX,
targetY
};
})
}
</script>
<svg
width={$widthStore}
height={$heightStore}
class="react-flow__edges react-flow__container"
>
{#each edgesWithData as edge}
{#if edge}
<Edge {...edge} />
{/if}
{/each}
</svg>
<style>
.react-flow__edges {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
pointer-events: none;
}

View File

@@ -0,0 +1,34 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import Node from '$lib/components/nodes/BaseNode.svelte';
import { useStore } from '$lib/store';
const { nodesStore, updateNodeDimensions } = useStore();
const resizeObserver: ResizeObserver | null = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver((entries: ResizeObserverEntry[]) => {
const updates = entries.map((entry: ResizeObserverEntry) => ({
id: entry.target.getAttribute('data-id') as string,
nodeElement: entry.target as HTMLDivElement,
forceUpdate: true,
}))
updateNodeDimensions(updates);
});
onDestroy(() => {
resizeObserver?.disconnect();
});
</script>
<div class="react-flow__nodes">
{#each $nodesStore as node}
<Node {...node} {resizeObserver} />
{/each}
</div>
<style>
.react-flow__nodes {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,62 @@
<script lang="ts">
import { setContext, onMount } from 'svelte'
import cc from 'classcat';
import type { Node, Edge } from '@reactflow/core';
import { key, createStore } from '$lib/store';
import Viewport from '$lib/container/Viewport.svelte';
import NodeRenderer from '$lib/container/NodeRenderer.svelte';
import EdgeRenderer from '$lib/container/EdgeRenderer.svelte';
export let nodes: Node[];
export let edges: Edge[];
export let className: string | null = null;
export let id: string = '1';
let props = { ...$$restProps };
let domNode: Element;
const store = createStore({
nodes,
edges,
});
setContext(key, {
getStore: () => store
});
onMount(() => {
const { width, height } = domNode.getBoundingClientRect();
store.widthStore.set(width);
store.heightStore.set(height);
})
</script>
<div
{...props}
class={cc(['react-flow', className])}
data-testid="rf__wrapper"
id={id}
bind:this={domNode}
>
<Viewport>
<NodeRenderer />
<EdgeRenderer />
</Viewport>
</div>
<style>
.react-flow {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
z-index: 0;
}
:root {
--node-width: 150px;
}
</style>

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import zoom from '$lib/hooks/zoom'
import { useStore } from '$lib/store';
const { transformStore } = useStore();
</script>
<div class="react-flow__pane" use:zoom={{ transformStore }}>
<div
class="react-flow__viewport"
style="transform: translate({$transformStore[0]}px, {$transformStore[1]}px) scale({$transformStore[2]})"
>
<slot></slot>
</div>
</div>
<style>
.react-flow__pane {
width: 100%;
height: 100%;
position: absolute;
top:0;
left:0;
cursor: grab;
}
.react-flow__viewport {
width: 100%;
height: 100%;
position: absolute;
transform-origin: 0 0;
top:0;
left:0;
}
</style>

View File

@@ -0,0 +1,128 @@
import { get, type Writable } from 'svelte/store';
import { drag as d3Drag, type D3DragEvent, type SubjectPosition } from 'd3-drag';
import { select } from 'd3-selection';
import type { XYPosition, CoordinateExtent, Node, Transform } from '@reactflow/core';
import { getDragItems, hasSelector, calcNextPosition } from './utils';
export type UseDragData = { dx: number; dy: number };
export type UseDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>;
export type NodeDragItem = {
id: string;
position: XYPosition;
positionAbsolute: XYPosition;
// distance from the mouse cursor to the node when start dragging
distance: XYPosition;
width?: number | null;
height?: number | null;
extent?: 'parent' | CoordinateExtent;
parentNode?: string;
dragging?: boolean;
};
type UseDragParams = {
noDragClassName?: string;
handleSelector?: string;
nodeId?: string;
updateNodePositions: (dragItems: NodeDragItem[], d: boolean, p: boolean) => void;
nodesStore: Writable<Node[]>;
transformStore: Writable<Transform>;
};
export default function drag(
nodeRef: Element,
{
noDragClassName,
handleSelector,
nodeId,
updateNodePositions,
nodesStore,
transformStore
}: UseDragParams
) {
let dragging = false;
let dragItems: NodeDragItem[] = [];
let lastPos: { x: number | null; y: number | null } = { x: null, y: null };
const selection = select(nodeRef);
const getPointerPosition = ({ sourceEvent }: UseDragEvent) => {
const x = sourceEvent.touches ? sourceEvent.touches[0].clientX : sourceEvent.clientX;
const y = sourceEvent.touches ? sourceEvent.touches[0].clientY : sourceEvent.clientY;
const transform = get(transformStore);
const pointerPos = {
x: (x - transform[0]) / transform[2],
y: (y - transform[1]) / transform[2]
};
// we need the snapped position in order to be able to skip unnecessary drag events
return {
xSnapped: pointerPos.x,
ySnapped: pointerPos.y,
...pointerPos
};
};
const updateNodes = ({ x, y }: XYPosition) => {
let hasChange = false;
dragItems = dragItems.map((n) => {
const nextPosition = { x: x - n.distance.x, y: y - n.distance.y };
const updatedPos = calcNextPosition(n, nextPosition, get(nodesStore));
// we want to make sure that we only fire a change event when there is a changes
hasChange =
hasChange ||
n.position.x !== updatedPos.position.x ||
n.position.y !== updatedPos.position.y;
n.position = updatedPos.position;
n.positionAbsolute = updatedPos.positionAbsolute;
return n;
});
if (!hasChange) {
return;
}
updateNodePositions(dragItems, true, true);
dragging = true;
};
const dragHandler = d3Drag()
.on('start', (event: UseDragEvent) => {
const pointerPos = getPointerPosition(event);
lastPos = pointerPos;
dragItems = getDragItems(get(nodesStore), pointerPos, nodeId);
})
.on('drag', (event: UseDragEvent) => {
const pointerPos = getPointerPosition(event);
// skip events without movement
if ((lastPos.x !== pointerPos.xSnapped || lastPos.y !== pointerPos.ySnapped) && dragItems) {
lastPos = pointerPos;
updateNodes(pointerPos);
}
})
.on('end', (event: UseDragEvent) => {
dragging = false;
if (dragItems) {
updateNodePositions(dragItems, false, false);
}
})
.filter((event: MouseEvent) => {
const target = event.target as HTMLDivElement;
const isDraggable =
!event.button &&
(!noDragClassName || !hasSelector(target, `.${noDragClassName}`, nodeRef)) &&
(!handleSelector || hasSelector(target, handleSelector, nodeRef));
return isDraggable;
});
selection.call(dragHandler);
}

View File

@@ -0,0 +1,153 @@
import type {
CoordinateExtent,
Node,
NodeDragItem,
NodeInternals,
NodeOrigin,
XYPosition
} from '@reactflow/core';
import { clampPosition, isNumeric } from '../../../utils';
export function isParentSelected(node: Node, nodes: Node[]): boolean {
if (!node.parentNode) {
return false;
}
const parentNode = nodes.find((n) => n.id === node.parentNode);
if (!parentNode) {
return false;
}
if (parentNode.selected) {
return true;
}
return isParentSelected(parentNode, nodes);
}
export function hasSelector(target: Element, selector: string, domNode: Element): boolean {
let current = target;
do {
if (current?.matches(selector)) return true;
if (current === domNode) return false;
current = current.parentElement as Element;
} while (current);
return false;
}
// looks for all selected nodes and created a NodeDragItem for each of them
export function getDragItems(nodes: Node[], mousePos: XYPosition, nodeId?: string): NodeDragItem[] {
console.log(mousePos, nodes);
return nodes
.filter(
(n) => (n.selected || n.id === nodeId) && (!n.parentNode || !isParentSelected(n, nodes))
)
.map((n) => ({
id: n.id,
position: n.position ? { ...n.position } : { x: 0, y: 0 },
positionAbsolute: n.positionAbsolute ? { ...n.positionAbsolute } : { x: 0, y: 0 },
distance: {
x: mousePos.x - (n.positionAbsolute?.x ?? 0),
y: mousePos.y - (n.positionAbsolute?.y ?? 0)
},
delta: {
x: 0,
y: 0
},
extent: n.extent,
parentNode: n.parentNode,
width: n.width,
height: n.height
}));
}
export function calcNextPosition(
node: NodeDragItem | Node,
nextPosition: XYPosition,
nodes: Node[],
nodeExtent?: CoordinateExtent,
nodeOrigin: NodeOrigin = [0, 0]
): { position: XYPosition; positionAbsolute: XYPosition } {
let currentExtent = node.extent || nodeExtent;
if (node.extent === 'parent') {
if (node.parentNode && node.width && node.height) {
const parent = nodes.find((n) => n.id === node.parentNode)!;
const { x: parentX, y: parentY } = parent.positionAbsolute!;
currentExtent =
parent &&
isNumeric(parentX) &&
isNumeric(parentY) &&
isNumeric(parent.width) &&
isNumeric(parent.height)
? [
[parentX + node.width * nodeOrigin[0], parentY + node.height * nodeOrigin[1]],
[
parentX + parent.width! - node.width + node.width * nodeOrigin[0],
parentY + parent.height! - node.height + node.height * nodeOrigin[1]
]
]
: currentExtent;
} else {
currentExtent = nodeExtent;
}
} else if (node.extent && node.parentNode) {
const parent = nodes.find((n) => n.id === node.parentNode)!;
const { x: parentX, y: parentY } = parent.positionAbsolute!;
currentExtent = [
[node.extent[0][0] + parentX, node.extent[0][1] + parentY],
[node.extent[1][0] + parentX, node.extent[1][1] + parentY]
];
}
let parentPosition = { x: 0, y: 0 };
if (node.parentNode) {
const parent = nodes.find((n) => n.id === node.parentNode)!;
parentPosition = parent.positionAbsolute!;
}
const positionAbsolute = currentExtent
? clampPosition(nextPosition, currentExtent as CoordinateExtent)
: nextPosition;
return {
position: {
x: positionAbsolute.x - parentPosition.x,
y: positionAbsolute.y - parentPosition.y
},
positionAbsolute
};
}
// returns two params:
// 1. the dragged node (or the first of the list, if we are dragging a node selection)
// 2. array of selected nodes (for multi selections)
export function getEventHandlerParams({
nodeId,
dragItems,
nodeInternals
}: {
nodeId?: string;
dragItems: NodeDragItem[];
nodeInternals: NodeInternals;
}): [Node, Node[]] {
const extentedDragItems: Node[] = dragItems.map((n) => {
const node = nodeInternals.get(n.id)!;
return {
...node,
position: n.position,
positionAbsolute: n.positionAbsolute
};
});
return [
nodeId ? extentedDragItems.find((n) => n.id === nodeId)! : extentedDragItems[0],
extentedDragItems
];
}

View File

@@ -0,0 +1,103 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Writable } from 'svelte/store';
import { select } from 'd3-selection';
import { zoom as d3Zoom, zoomIdentity } from 'd3-zoom';
import type { D3ZoomEvent } from 'd3-zoom';
import type { Transform } from '@reactflow/core';
const isWrappedWithClass = (event: any, className: string | undefined) =>
event.target.closest(`.${className}`);
export default function zoom(
domNode: Element,
{ transformStore }: { transformStore: Writable<Transform> }
) {
const d3ZoomInstance = d3Zoom();
const selection = select(domNode).call(d3ZoomInstance);
const d3ZoomHandler = selection.on('wheel.zoom');
d3ZoomInstance.transform(selection, zoomIdentity);
selection.on('wheel.zoom', function (event: any, d: any) {
if (isWrappedWithClass(event, 'nowheel')) {
return null;
}
event.preventDefault();
d3ZoomHandler!.call(this, event, d);
});
d3ZoomInstance.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
transformStore.set([event.transform.x, event.transform.y, event.transform.k]);
});
d3ZoomInstance.filter((event: any) => {
const zoomScroll = true;
const pinchZoom = true;
if (
event.button === 1 &&
event.type === 'mousedown' &&
(isWrappedWithClass(event, 'react-flow__node') ||
isWrappedWithClass(event, 'react-flow__edge'))
) {
return true;
}
// if all interactions are disabled, we prevent all zoom events
// if (!panOnDrag && !zoomScroll && !panOnScroll && !zoomOnDoubleClick && !zoomOnPinch) {
// return false;
// }
// // during a selection we prevent all other interactions
// if (userSelectionActive) {
// return false;
// }
// // if zoom on double click is disabled, we prevent the double click event
// if (!zoomOnDoubleClick && event.type === 'dblclick') {
// return false;
// }
// // if the target element is inside an element with the nowheel class, we prevent zooming
// if (isWrappedWithClass(event, noWheelClassName) && event.type === 'wheel') {
// return false;
// }
// // if the target element is inside an element with the nopan class, we prevent panning
// if (isWrappedWithClass(event, noPanClassName) && event.type !== 'wheel') {
// return false;
// }
// if (!zoomOnPinch && event.ctrlKey && event.type === 'wheel') {
// return false;
// }
// // when there is no scroll handling enabled, we prevent all wheel events
// if (!zoomScroll && !panOnScroll && !pinchZoom && event.type === 'wheel') {
// return false;
// }
// // if the pane is not movable, we prevent dragging it with mousestart or touchstart
// if (!panOnDrag && (event.type === 'mousedown' || event.type === 'touchstart')) {
// return false;
// }
// // if the pane is only movable using allowed clicks
// if (
// Array.isArray(panOnDrag) &&
// !panOnDrag.includes(event.button) &&
// (event.type === 'mousedown' || event.type === 'touchstart')
// ) {
// return false;
// }
// // We only allow right clicks if pan on drag is set to right click
// const buttonAllowed =
// (Array.isArray(panOnDrag) && panOnDrag.includes(event.button)) ||
// !event.button ||
// event.button <= 1;
// default filter for d3-zoom
return ((!event.ctrlKey || event.type === 'wheel') && !event.button) || event.button <= 1;
});
}

View File

@@ -0,0 +1,4 @@
// Reexport your entry components here
import SvelteFlow from '$lib/container/SvelteFlow.svelte';
export default SvelteFlow;

View File

@@ -0,0 +1,116 @@
import { getContext } from 'svelte';
import { get, writable, type Writable } from 'svelte/store';
import type { Node, Transform, NodeDragItem, NodeDimensionUpdate, Edge } from '@reactflow/core';
import { internalsSymbol } from '@reactflow/core';
import { getDimensions, getHandleBounds, updateAbsoluteNodePositions } from '../../utils';
export const key = Symbol();
type CreateStoreProps = {
nodes: Node[];
edges: Edge[];
transform?: Transform;
};
type SvelteFlowStore = {
nodesStore: Writable<CreateStoreProps['nodes']>;
edgesStore: Writable<CreateStoreProps['edges']>;
heightStore: Writable<number>;
widthStore: Writable<number>;
transformStore: Writable<Transform>;
updateNodePositions: (
nodeDragItems: NodeDragItem[],
positionChanged?: boolean,
dragging?: boolean
) => void;
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
};
export function createStore({
nodes = [],
edges = [],
transform = [0, 0, 1]
}: CreateStoreProps): SvelteFlowStore {
const nodesStore = writable(nodes.map((n) => ({ ...n, positionAbsolute: n.position })));
const edgesStore = writable(edges);
const heightStore = writable(500);
const widthStore = writable(500);
const transformStore = writable(transform);
function updateNodePositions(nodeDragItems: NodeDragItem[], dragging = false) {
nodesStore.update((nds) => {
return nds.map((n) => {
const nodeDragItem = nodeDragItems.find((ndi) => ndi.id === n.id);
if (nodeDragItem) {
return {
...n,
dragging,
positionAbsolute: nodeDragItem.positionAbsolute,
position: nodeDragItem.position
};
}
return n;
});
});
}
function updateNodeDimensions(updates: NodeDimensionUpdate[]) {
const viewportNode = document?.querySelector('.react-flow__viewport');
if (!viewportNode) {
return;
}
const style = window.getComputedStyle(viewportNode);
const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform);
const nds = get(nodesStore);
updates.forEach((update) => {
const node = nds.find((n) => n.id === update.id);
if (node) {
const dimensions = getDimensions(update.nodeElement);
const doUpdate = !!(
dimensions.width &&
dimensions.height &&
(node.width !== dimensions.width ||
node.height !== dimensions.height ||
update.forceUpdate)
);
if (doUpdate) {
node[internalsSymbol] = {
...node[internalsSymbol],
handleBounds: {
source: getHandleBounds('.source', update.nodeElement, zoom),
target: getHandleBounds('.target', update.nodeElement, zoom)
}
};
node.width = dimensions.width;
node.height = dimensions.height;
}
}
});
updateAbsoluteNodePositions(nds);
}
return {
nodesStore,
edgesStore,
transformStore,
heightStore,
widthStore,
updateNodePositions,
updateNodeDimensions
};
}
export function useStore(): SvelteFlowStore {
const { getStore } = getContext<{ getStore: () => SvelteFlowStore }>(key);
return getStore();
}

View File

@@ -0,0 +1,59 @@
<script lang="ts">
import type { Node, Edge } from '@reactflow/core';
import SvelteFlow from '../lib/index';
const yNodes = 10;
const xNodes = 10;
const nodes: Node[] = [];
const edges: Edge[] = [];
let source = null;
for (let y = 0; y < yNodes; y++) {
for (let x = 0; x < xNodes; x++) {
const position = { x: x * 100, y: y * 50 };
const id = `${x}-${y}`;
const data = { label: `Node ${id}` };
const node = {
id,
data,
position,
};
nodes.push(node);
if (source) {
const edge = {
id: `${source.id}-${id}`,
source: source.id,
target: id,
};
edges.push(edge);
}
source = node;
}
}
// const nodes = [{
// id: '1',
// type: 'input',
// data: { label: 'Input Node' },
// position: { x: 250, y: 5 }
// }, {
// id: '2',
// type: 'input',
// data: { label: 'Input Node 2' },
// position: { x: 150, y: 250 },
// }]
</script>
<SvelteFlow {nodes} {edges} />
<style>
:root {
--node-width: 50;
}
</style>

View File

@@ -0,0 +1,101 @@
import {
type CoordinateExtent,
type Dimensions,
type XYPosition,
type Node,
type XYZPosition,
type HandleElement,
internalsSymbol,
Position
} from '@reactflow/core';
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) => ({
x: clamp(position.x, extent[0][0], extent[1][0]),
y: clamp(position.y, extent[0][1], extent[1][1])
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isNumeric = (n: any): n is number => !isNaN(n) && isFinite(n);
export const getDimensions = (node: HTMLDivElement): Dimensions => ({
width: node.offsetWidth,
height: node.offsetHeight
});
type ParentNodes = Record<string, boolean>;
function calculateXYZPosition(node: Node, nodes: Node[], result: XYZPosition): XYZPosition {
if (!node.parentNode) {
return result;
}
const parentNode = nodes.find((n) => n.id === node.parentNode)!;
const parentNodePosition = parentNode.positionAbsolute!;
return calculateXYZPosition(parentNode, nodes, {
x: (result.x ?? 0) + parentNodePosition.x,
y: (result.y ?? 0) + parentNodePosition.y,
z:
(parentNode[internalsSymbol]?.z ?? 0) > (result.z ?? 0)
? parentNode[internalsSymbol]?.z ?? 0
: result.z ?? 0
});
}
export function updateAbsoluteNodePositions(nodes: Node[], parentNodes?: ParentNodes) {
nodes.forEach((node) => {
if (node.parentNode) {
throw new Error(`Parent node ${node.parentNode} not found`);
}
if (node.parentNode || parentNodes?.[node.id]) {
const { x, y, z } = calculateXYZPosition(node, nodes, {
...node.position,
z: node[internalsSymbol]?.z ?? 0
});
node.positionAbsolute = {
x,
y
};
node[internalsSymbol]!.z = z;
if (parentNodes?.[node.id]) {
node[internalsSymbol]!.isParent = true;
}
}
});
}
export const getHandleBounds = (
selector: string,
nodeElement: HTMLDivElement,
zoom: number
): HandleElement[] | null => {
const handles = nodeElement.querySelectorAll(selector);
if (!handles || !handles.length) {
return null;
}
const handlesArray = Array.from(handles) as HTMLDivElement[];
const nodeBounds = nodeElement.getBoundingClientRect();
const nodeOffset = {
x: nodeBounds.width,
y: nodeBounds.height
};
return handlesArray.map((handle): HandleElement => {
const handleBounds = handle.getBoundingClientRect();
return {
id: handle.getAttribute('data-handleid'),
position: handle.getAttribute('data-handlepos') as unknown as Position,
x: (handleBounds.left - nodeBounds.left - nodeOffset.x) / zoom,
y: (handleBounds.top - nodeBounds.top - nodeOffset.y) / zoom,
...getDimensions(handle)
};
});
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,15 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/kit/vite';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
adapter: adapter()
}
};
export default config;

View File

@@ -0,0 +1,17 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
}
// 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
// from the referenced tsconfig.json - TypeScript does not merge them in
}

View File

@@ -0,0 +1,8 @@
import { sveltekit } from '@sveltejs/kit/vite';
import type { UserConfig } from 'vite';
const config: UserConfig = {
plugins: [sveltekit()]
};
export default config;

936
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff