Merge branch 'xyflow' into svelte-flow-get-intersecting-nodes

This commit is contained in:
moklick
2023-10-05 20:14:42 +02:00
17 changed files with 313 additions and 38 deletions

View File

@@ -49,9 +49,9 @@ const DnDFlow = () => {
if (reactFlowInstance) {
const type = event.dataTransfer.getData('application/reactflow');
const position = reactFlowInstance.project({
const position = reactFlowInstance.screenToFlowCoordinate({
x: event.clientX,
y: event.clientY - 40,
y: event.clientY,
});
const newNode: Node = {
id: getId(),

View File

@@ -16,7 +16,8 @@
'usesvelteflow',
'useupdatenodeinternals',
'validation',
'intersections'
'intersections',
'add-node-on-drop'
];
const onChange = (event: Event) => {

View File

@@ -0,0 +1,9 @@
<script lang="ts">
import { SvelteFlowProvider } from '@xyflow/svelte';
import Flow from './Flow.svelte';
</script>
<!-- You need the SvelteFlowProvider so you can useSvelteFlow -->
<SvelteFlowProvider>
<Flow />
</SvelteFlowProvider>

View File

@@ -0,0 +1,115 @@
<script lang="ts">
import { SvelteFlow, useSvelteFlow } from '@xyflow/svelte';
import type { Edge, Node } from '@xyflow/svelte';
import { writable } from 'svelte/store';
import '@xyflow/svelte/dist/style.css';
const initialNodes: Node[] = [
{
id: '0',
type: 'input',
data: { label: 'Node' },
position: { x: 0, y: 50 }
}
];
const nodes = writable<Node[]>(initialNodes);
const edges = writable<Edge[]>([]);
let connectingNodeId: string = '0';
let rect: DOMRectReadOnly;
let id = 1;
const getId = () => `${id++}`;
const { screenToFlowCoordinate, flowToScreenCoordinate } = useSvelteFlow();
function handleConnectEnd({ detail: { event } }: { detail: { event: MouseEvent | TouchEvent } }) {
// See of connection landed inside the flow pane
const targetIsPane = event.target?.classList.contains('svelte-flow__pane');
if (targetIsPane) {
const id = getId();
const position = {
x: event.clientX,
y: event.clientY
};
const doubleTransformedPosition = flowToScreenCoordinate(screenToFlowCoordinate(position));
console.log(
'Is transforming in both directions (screen-flow, flow-screen) the same?',
position.x === doubleTransformedPosition.x && position.y === doubleTransformedPosition.y
);
const newNode: Node = {
id,
data: { label: `Node ${id}` },
// project the screen coordinates to pane coordinates
position: screenToFlowCoordinate(position),
// set the origin of the new node so it is centered
origin: [0.5, 0.0]
};
$nodes.push(newNode);
$edges.push({
source: connectingNodeId,
target: id,
id: `${connectingNodeId}--${id}`
});
$nodes = $nodes;
$edges = $edges;
}
}
</script>
<svelte:window />
<div class="wrapper" bind:contentRect={rect}>
<SvelteFlow
{nodes}
{edges}
fitView
fitViewOptions={{ padding: 2 }}
on:connectstart={({ detail: { nodeId } }) => {
// Memorize the nodeId you start draggin a connection line from a node
connectingNodeId = nodeId;
}}
on:connectend={handleConnectEnd}
/>
</div>
<style>
:global(.svelte-flow .svelte-flow__handle) {
width: 30px;
height: 14px;
border-radius: 3px;
background-color: #784be8;
}
:global(.svelte-flow .svelte-flow__handle-top) {
top: -10px;
}
:global(.svelte-flow .svelte-flow__handle-bottom) {
bottom: -10px;
}
:global(.svelte-flow .svelte-flow__node) {
height: 40px;
width: 150px;
justify-content: center;
align-items: center;
display: flex;
border-width: 2px;
font-weight: 700;
}
:global(.svelte-flow .svelte-flow__edge path, .svelte-flow__connectionline path) {
stroke-width: 2;
}
.wrapper {
height: 100vh;
width: 100vw;
}
</style>

View File

@@ -2,7 +2,6 @@ import type { ReactNode } from 'react';
import { useStore } from '../../hooks/useStore';
import type { ReactFlowState } from '../../types';
import { Viewport } from '@xyflow/system';
const selector = (s: ReactFlowState) => `translate(${s.transform[0]}px,${s.transform[1]}px) scale(${s.transform[2]})`;
@@ -10,7 +9,7 @@ type ViewportProps = {
children: ReactNode;
};
function Viewport({ children }: ViewportProps) {
export default function Viewport({ children }: ViewportProps) {
const transform = useStore(selector);
return (
@@ -19,5 +18,3 @@ function Viewport({ children }: ViewportProps) {
</div>
);
}
export default Viewport;

View File

@@ -164,6 +164,8 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
}
}
}
return { deletedNodes: matchingNodes, deletedEdges: matchingEdges };
}, []);
const getNodeRect = useCallback(

View File

@@ -1,5 +1,11 @@
import { useMemo } from 'react';
import { pointToRendererPoint, getTransformForBounds, fitView, type XYPosition } from '@xyflow/system';
import {
pointToRendererPoint,
getTransformForBounds,
fitView,
type XYPosition,
rendererPointToPoint,
} from '@xyflow/system';
import { useStoreApi, useStore } from '../hooks/useStore';
import type { ViewportHelperFunctions, ReactFlowState } from '../types';
@@ -85,6 +91,36 @@ const useViewportHelper = (): ViewportHelperFunctions => {
const { transform, snapToGrid, snapGrid } = store.getState();
return pointToRendererPoint(position, transform, snapToGrid, snapGrid);
},
screenToFlowCoordinate: (position: XYPosition) => {
const { transform, snapToGrid, snapGrid, domNode } = store.getState();
if (domNode) {
const { x: domX, y: domY } = domNode.getBoundingClientRect();
const correctedPosition = {
x: position.x - domX,
y: position.y - domY,
};
return pointToRendererPoint(correctedPosition, transform, snapToGrid, snapGrid || [1, 1]);
}
return { x: 0, y: 0 };
},
flowToScreenCoordinate: (position: XYPosition) => {
const { transform, domNode } = store.getState();
if (domNode) {
const { x: domX, y: domY } = domNode.getBoundingClientRect();
const rendererPosition = rendererPointToPoint(position, transform);
return {
x: rendererPosition.x + domX,
y: rendererPosition.y + domY,
};
}
return { x: 0, y: 0 };
},
viewportInitialized: panZoomInitialized,
};
}, [panZoomInitialized]);

View File

@@ -55,5 +55,7 @@ export type ViewportHelperFunctions = {
setCenter: SetCenter;
fitBounds: FitBounds;
project: Project;
screenToFlowCoordinate: Project;
flowToScreenCoordinate: Project;
viewportInitialized: boolean;
};

View File

@@ -28,7 +28,10 @@ export namespace Instance {
export type GetEdge<EdgeData> = (id: string) => Edge<EdgeData> | undefined;
export type AddEdges<EdgeData> = (payload: Edge<EdgeData>[] | Edge<EdgeData>) => void;
export type ToObject<NodeData = any, EdgeData = any> = () => ReactFlowJsonObject<NodeData, EdgeData>;
export type DeleteElements = ({ nodes, edges }: DeleteElementsOptions) => void;
export type DeleteElements = ({ nodes, edges }: DeleteElementsOptions) => {
deletedNodes: Node[];
deletedEdges: Edge[];
};
export type GetIntersectingNodes<NodeData> = (
node: (Partial<Node<NodeData>> & { id: Node['id'] }) | Rect,
partially?: boolean,

View File

@@ -12,11 +12,11 @@
{#if $connection.path}
<svg width={$width} height={$height} class="svelte-flow__connectionline" style={containerStyle}>
<g class={cc(['svelte-flow__connection', $connection.status])} {style}>
<g class={cc(['svelte-flow__connection', $connection.status])}>
<slot name="connectionLine" />
<!-- slot fallbacks do not work if slots are forwarded in parent -->
{#if !isCustomComponent}
<path d={$connection.path} fill="none" class="svelte-flow__connection-path" />
<path d={$connection.path} {style} fill="none" class="svelte-flow__connection-path" />
{/if}
</g>
</svg>

View File

@@ -1,3 +1,5 @@
<svelte:options immutable />
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { getMarkerId } from '@xyflow/system';

View File

@@ -1,3 +1,5 @@
<svelte:options immutable />
<script lang="ts">
import {
createEventDispatcher,

View File

@@ -71,13 +71,14 @@
export { className as class };
let domNode: HTMLDivElement;
let clientWidth: number;
let clientHeight: number;
const store = hasContext(key) ? useStore() : createStoreContext();
onMount(() => {
const { width, height } = domNode.getBoundingClientRect();
store.width.set(width);
store.height.set(height);
store.width.set(clientWidth);
store.height.set(clientHeight);
store.domNode.set(domNode);
store.syncNodeStores(nodes);
@@ -141,6 +142,8 @@
<div
bind:this={domNode}
bind:clientWidth
bind:clientHeight
{style}
class={cc(['svelte-flow', className])}
data-testid="svelte-flow__wrapper"

View File

@@ -0,0 +1,11 @@
import { useStore } from '$lib/store';
export function useNodes() {
const { nodes } = useStore();
return nodes;
}
export function useEdges() {
const { edges } = useStore();
return edges;
}

View File

@@ -4,18 +4,20 @@ import {
isRectObject,
nodeToRect,
pointToRendererPoint,
type Project,
type Rect,
type FitBoundsOptions,
type SetCenterOptions,
type Viewport,
type ViewportHelperFunctionOptions,
type XYPosition,
type ZoomInOut
type ZoomInOut,
type Rect,
getTransformForBounds,
getElementsToRemove,
rendererPointToPoint
} from '@xyflow/system';
import { useStore } from '$lib/store';
import type { FitViewOptions, Node } from '$lib/types';
import type { SvelteFlowStore } from '$lib/store/types';
import type { Edge, FitViewOptions, Node } from '$lib/types';
export function useSvelteFlow(): {
zoomIn: ZoomInOut;
@@ -27,19 +29,23 @@ export function useSvelteFlow(): {
getViewport: () => Viewport;
fitView: (options?: FitViewOptions) => void;
getIntersectingNodes: (
nodeOrRect: (Partial<Node<any>> & { id: Node['id'] }) | Rect,
nodeOrRect: (Partial<Node> & { id: Node['id'] }) | Rect,
partially?: boolean,
nodesToIntersect?: Node[]
) => Node[];
isNodeIntersecting: (
nodeOrRect: (Partial<Node<any>> & { id: Node['id'] }) | Rect,
nodeOrRect: (Partial<Node> & { id: Node['id'] }) | Rect,
area: Rect,
partially?: boolean
) => boolean;
project: Project;
fitBounds: (bounds: Rect, options?: FitBoundsOptions) => void;
deleteElements: (
nodesToRemove?: Partial<Node> & { id: string }[],
edgesToRemove?: Partial<Edge> & { id: string }[]
) => { deletedNodes: Node[]; deletedEdges: Edge[] };
screenToFlowCoordinate: (position: XYPosition) => XYPosition;
flowToScreenCoordinate: (position: XYPosition) => XYPosition;
viewport: Writable<Viewport>;
nodes: SvelteFlowStore['nodes'];
edges: SvelteFlowStore['edges'];
} {
const {
zoomIn,
@@ -49,20 +55,22 @@ export function useSvelteFlow(): {
viewport,
width,
height,
minZoom,
maxZoom,
panZoom,
nodes,
edges
edges,
domNode
} = useStore();
const getNodeRect = (
nodeOrRect: (Partial<Node<any>> & { id: Node['id'] }) | Rect
): [Rect | null, Node<any> | null | undefined, boolean] => {
nodeOrRect: (Partial<Node> & { id: Node['id'] }) | Rect
): [Rect | null, Node | null | undefined, boolean] => {
const isRect = isRectObject(nodeOrRect);
const node = isRect ? null : get(nodes).find((n) => n.id === nodeOrRect.id);
if (!isRect && !node) {
[null, null, isRect];
return [null, null, isRect];
}
const nodeRect = isRect ? nodeOrRect : nodeToRect(node!);
@@ -107,19 +115,43 @@ export function useSvelteFlow(): {
);
},
fitView,
fitBounds: (bounds: Rect, options?: FitBoundsOptions) => {
const _width = get(width);
const _height = get(height);
const _maxZoom = get(maxZoom);
const _minZoom = get(minZoom);
const [x, y, zoom] = getTransformForBounds(
bounds,
_width,
_height,
_minZoom,
_maxZoom,
options?.padding ?? 0.1
);
get(panZoom)?.setViewport(
{
x,
y,
zoom
},
{ duration: options?.duration }
);
},
getIntersectingNodes: (
nodeOrRect: (Partial<Node<any>> & { id: Node['id'] }) | Rect,
nodeOrRect: (Partial<Node> & { id: Node['id'] }) | Rect,
partially = true,
nodesToIntersect?: Node[]
) => {
const [nodeRect, node, isRect] = getNodeRect(nodeOrRect);
if (!nodeRect) {
if (!nodeRect || !node) {
return [];
}
return (nodesToIntersect || get(nodes)).filter((n) => {
if (!isRect && (n.id === node!.id || !n.positionAbsolute)) {
if (!isRect && (n.id === node.id || !n.positionAbsolute)) {
return false;
}
@@ -131,7 +163,7 @@ export function useSvelteFlow(): {
});
},
isNodeIntersecting: (
nodeOrRect: (Partial<Node<any>> & { id: Node['id'] }) | Rect,
nodeOrRect: (Partial<Node> & { id: Node['id'] }) | Rect,
area: Rect,
partially = true
) => {
@@ -146,14 +178,70 @@ export function useSvelteFlow(): {
return partiallyVisible || overlappingArea >= nodeOrRect.width! * nodeOrRect.height!;
},
project: (position: XYPosition) => {
const _snapGrid = get(snapGrid);
const { x, y, zoom } = get(viewport);
deleteElements: (
nodesToRemove: Partial<Node> & { id: string }[] = [],
edgesToRemove: Partial<Edge> & { id: string }[] = []
) => {
const _nodes = get(nodes);
const _edges = get(edges);
const { matchingNodes, matchingEdges } = getElementsToRemove<Node, Edge>({
nodesToRemove,
edgesToRemove,
nodes: _nodes,
edges: _edges
});
return pointToRendererPoint(position, [x, y, zoom], _snapGrid !== null, _snapGrid || [1, 1]);
if (matchingNodes) {
nodes.set(_nodes.filter((node) => !matchingNodes.some(({ id }) => id === node.id)));
}
if (matchingEdges) {
edges.set(_edges.filter((edge) => !matchingEdges.some(({ id }) => id === edge.id)));
}
return {
deletedNodes: matchingNodes,
deletedEdges: matchingEdges
};
},
screenToFlowCoordinate: (position: XYPosition) => {
const _domNode = get(domNode);
if (_domNode) {
const _snapGrid = get(snapGrid);
const { x, y, zoom } = get(viewport);
const { x: domX, y: domY } = _domNode.getBoundingClientRect();
const correctedPosition = {
x: position.x - domX,
y: position.y - domY
};
return pointToRendererPoint(
correctedPosition,
[x, y, zoom],
_snapGrid !== null,
_snapGrid || [1, 1]
);
}
return { x: 0, y: 0 };
},
flowToScreenCoordinate: (position: XYPosition) => {
const _domNode = get(domNode);
if (_domNode) {
const { x, y, zoom } = get(viewport);
const { x: domX, y: domY } = _domNode.getBoundingClientRect();
const rendererPosition = rendererPointToPoint(position, [x, y, zoom]);
return {
x: rendererPosition.x + domX,
y: rendererPosition.y + domY
};
}
return { x: 0, y: 0 };
},
nodes,
edges,
viewport: viewport
};
}

View File

@@ -19,9 +19,12 @@ export { useStore } from '$lib/store';
// utils
export * from '$lib/utils';
//hooks
export * from '$lib/hooks/useSvelteFlow';
export * from '$lib/hooks/useUpdateNodeInternals';
export * from '$lib/hooks/useConnection';
export * from '$lib/hooks/useNodesEdges';
// types
export type { Edge, EdgeProps, EdgeTypes, DefaultEdgeOptions } from '$lib/types/edges';

View File

@@ -5,4 +5,5 @@
.svelte-flow__edge-label {
text-align: center;
position: absolute;
font-size: 10px;
}