feat(svelte): add event handlers and props, rename class names

This commit is contained in:
moklick
2023-03-01 17:28:59 +01:00
parent 9391b36869
commit e2961bfa8c
38 changed files with 410 additions and 163 deletions

View File

@@ -2,7 +2,8 @@
import type { Writable } from 'svelte/store';
import { select } from 'd3-selection';
import { zoom as d3Zoom, zoomIdentity, type D3ZoomEvent } from 'd3-zoom';
import type { D3SelectionInstance, D3ZoomInstance, Transform } from '@reactflow/system';
import type { D3SelectionInstance, D3ZoomInstance, Transform, Viewport } from '@reactflow/system';
import { clamp } from '@reactflow/utils';
const isWrappedWithClass = (event: any, className: string | undefined) =>
event.target.closest(`.${className}`);
@@ -14,7 +15,8 @@ function filter(event: any, params: ZoomParams): boolean {
if (
event.button === 1 &&
event.type === 'mousedown' &&
(isWrappedWithClass(event, 'react-flow__node') || isWrappedWithClass(event, 'react-flow__edge'))
(isWrappedWithClass(event, 'svelte-flow__node') ||
isWrappedWithClass(event, 'svelte-flow__edge'))
) {
return true;
}
@@ -80,15 +82,27 @@ type ZoomParams = {
transform: Writable<Transform>;
selecting: boolean;
d3: Writable<{ zoom: D3ZoomInstance | null; selection: D3SelectionInstance | null }>;
minZoom: number;
maxZoom: number;
initialViewport: Viewport;
};
export default function zoom(domNode: Element, params: ZoomParams) {
const { transform, d3 } = params;
const d3ZoomInstance = d3Zoom();
const { transform, d3, minZoom, maxZoom, initialViewport } = params;
const d3ZoomInstance = d3Zoom().scaleExtent([minZoom, maxZoom]);
const selection = select(domNode).call(d3ZoomInstance);
const updatedTransform = zoomIdentity
.translate(initialViewport.x, initialViewport.y)
.scale(clamp(initialViewport.zoom, minZoom, maxZoom));
const d3ZoomHandler = selection.on('wheel.zoom');
d3ZoomInstance.transform(selection, zoomIdentity);
d3ZoomInstance.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
transform.set([event.transform.x, event.transform.y, event.transform.k]);
});
d3ZoomInstance.transform(selection, updatedTransform);
selection.on('wheel.zoom', function (event: any, d: any) {
if (isWrappedWithClass(event, 'nowheel')) {
@@ -104,10 +118,6 @@ export default function zoom(domNode: Element, params: ZoomParams) {
selection
});
d3ZoomInstance.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
transform.set([event.transform.x, event.transform.y, event.transform.k]);
});
d3ZoomInstance.filter((event: any) => filter(event, params));
return {

View File

@@ -11,7 +11,7 @@
export let interactionWidth: $$Props['interactionWidth'] = 20;
</script>
<path d={path} fill="none" class="react-flow__edge-path" />
<path d={path} fill="none" class="svelte-flow__edge-path" />
{#if interactionWidth}
<path
@@ -19,14 +19,14 @@
fill="none"
stroke-opacity={0}
stroke-width={interactionWidth}
class="react-flow__edge-interaction"
class="svelte-flow__edge-interaction"
/>
{/if}
{#if label}
<EdgeLabelRenderer>
<div
class="react-flow__edge-label"
class="svelte-flow__edge-label"
style:transform={`translate(-50%, -50%) translate(${labelX}px,${labelY}px)`}
>
{label}
@@ -35,7 +35,7 @@
{/if}
<style>
.react-flow__edge-label {
.svelte-flow__edge-label {
position: absolute;
background: white;
}

View File

@@ -7,15 +7,15 @@
</script>
{#if $connectionPath}
<svg width={$width} height={$height} class="react-flow__connectionline">
<g class={cc(['react-flow__connection', $connection.status])}>
<path d={$connectionPath} fill="none" class="react-flow__connection-path" />
<svg width={$width} height={$height} class="svelte-flow__connectionline">
<g class={cc(['svelte-flow__connection', $connection.status])}>
<path d={$connectionPath} fill="none" class="svelte-flow__connection-path" />
</g>
</svg>
{/if}
<style>
.react-flow__connectionline {
.svelte-flow__connectionline {
position: absolute;
top: 0;
left: 0;
@@ -26,12 +26,12 @@
z-index: 1001;
}
.react-flow__connection {
.svelte-flow__connection {
pointer-events: none;
}
.react-flow__edge-path,
.react-flow__connection-path {
.svelte-flow__edge-path,
.svelte-flow__connection-path {
stroke: #b1b1b7;
stroke-width: 1;
fill: none;

View File

@@ -4,6 +4,6 @@
type $$Props = {};
</script>
<div use:portal={'.react-flow__edgelabel-renderer'}>
<div use:portal={'.svelte-flow__edgelabel-renderer'}>
<slot />
</div>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import type { SvelteComponentTyped } from 'svelte';
import { createEventDispatcher, type SvelteComponentTyped } from 'svelte';
import { Position } from '@reactflow/system';
import { useStore } from '$lib/store';
@@ -24,11 +24,23 @@
export let selected: $$Props['selected'] = false;
export let label: $$Props['label'] = undefined;
const { edgeTypes } = useStore();
const { edgeTypes, edges } = useStore();
const dispatch = createEventDispatcher();
const edgeComponent: typeof SvelteComponentTyped<EdgeProps> = $edgeTypes[type!] || BezierEdge;
function onClick() {
const edge = $edges.find(e => e.id === id);
dispatch('edge:click', edge);
}
</script>
<g class="react-flow__edge" class:animated data-id={id}>
<g
class="svelte-flow__edge"
class:animated
data-id={id}
on:click={onClick}
>
<svelte:component
this={edgeComponent}
{id}
@@ -47,12 +59,12 @@
</g>
<style>
.react-flow__edge {
.svelte-flow__edge {
pointer-events: visibleStroke;
cursor: pointer;
}
.react-flow__edge :global(path) {
.svelte-flow__edge :global(path) {
stroke: #ccc;
stroke-width: 1;
fill: none;

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { getContext } from 'svelte';
import { createEventDispatcher } from 'svelte';
import cc from 'classcat';
import { Position, type Connection, type HandleProps } from '@reactflow/system';
import { isMouseEvent } from '@reactflow/utils';
@@ -18,6 +19,8 @@
const isTarget = type === 'target';
const nodeId = getContext<string>('rf_nodeid');
const handleId = id || null;
const dispatch = createEventDispatcher();
const {
connectionMode,
@@ -31,11 +34,16 @@
updateConnection
} = useStore();
function dispatchEvent(eventName: string) {
dispatch(eventName, { nodeId, handleId, type });
}
function onConnectExtended(params: Connection) {
addEdge(params);
// @todo add props
// onConnectAction?.(edgeParams);
// onConnect?.(edgeParams);
dispatchEvent('connect')
}
function onPointerDown(event: MouseEvent | TouchEvent) {
@@ -56,7 +64,9 @@
onConnect: onConnectExtended,
updateConnection,
cancelConnection,
panBy
panBy,
onConnectStart: () => dispatchEvent('connect:start'),
onConnectEnd: () => dispatchEvent('connect:end')
});
}
@@ -74,8 +84,8 @@
data-handlepos={position}
data-id={`${nodeId}-${id}-${type}`}
class={cc([
'react-flow__handle',
`react-flow__handle-${position}`,
'svelte-flow__handle',
`svelte-flow__handle-${position}`,
'nodrag',
'nopan',
position
@@ -90,7 +100,7 @@
</div>
<style>
.react-flow__handle {
.svelte-flow__handle {
position: absolute;
pointer-events: none;
min-width: 5px;

View File

@@ -43,7 +43,9 @@ export function handlePointerDown({
cancelConnection,
isValidConnection,
edgeUpdaterType,
onEdgeUpdateEnd
onEdgeUpdateEnd,
onConnectStart,
onConnectEnd
}: {
event: MouseEvent | TouchEvent;
handleId: string | null;
@@ -61,8 +63,10 @@ export function handlePointerDown({
panBy: (delta: XYPosition) => void;
edgeUpdaterType?: HandleType;
onEdgeUpdateEnd?: (evt: MouseEvent | TouchEvent) => void;
onConnectStart: () => void;
onConnectEnd: () => void;
}): void {
// when react-flow is used inside a shadow root we can't use document
// when svelte-flow is used inside a shadow root we can't use document
const doc = getHostForElement(event.target as HTMLElement);
let autoPanId = 0;
let prevClosestHandle: ConnectionHandle | null;
@@ -113,7 +117,9 @@ export function handlePointerDown({
});
// @todo add prop
// onConnectStart?.(event, { nodeId, handleId, handleType });
onConnectStart();
function onPointerMove(event: MouseEvent | TouchEvent) {
const transform = get(transformStore);
@@ -166,7 +172,7 @@ export function handlePointerDown({
if (connection.source !== connection.target && handleDomNode) {
resetRecentHandle(prevActiveHandle);
prevActiveHandle = handleDomNode;
// @todo: remove the old class names "react-flow__handle-" in the next major version
// @todo: remove the old class names "svelte-flow__handle-" in the next major version
handleDomNode.classList.add('connecting');
handleDomNode.classList.toggle('valid', isValid);
}
@@ -179,8 +185,8 @@ export function handlePointerDown({
// it's important to get a fresh reference from the store here
// in order to get the latest state of onConnectEnd
// @todo add onConnectEnd prop
// getState().onConnectEnd?.(event);
onConnectEnd();
if (edgeUpdaterType) {
onEdgeUpdateEnd?.(event);

View File

@@ -81,11 +81,11 @@ export function isValidHandle(
) {
const isTarget = fromType === 'target';
const handleDomNode = doc.querySelector(
`.react-flow__handle[data-id="${handle?.nodeId}-${handle?.id}-${handle?.type}"]`
`.svelte-flow__handle[data-id="${handle?.nodeId}-${handle?.id}-${handle?.type}"]`
);
const { x, y } = getEventPosition(event);
const handleBelow = doc.elementFromPoint(x, y);
const handleToCheck = handleBelow?.classList.contains('react-flow__handle')
const handleToCheck = handleBelow?.classList.contains('svelte-flow__handle')
? handleBelow
: handleDomNode;

View File

@@ -11,7 +11,7 @@
$: rect = getRectOfNodes(selectedNodes, $nodeOrigin);
</script>
{#if selectedNodes}
{#if selectedNodes && $selectionRectMode === 'nodes'}
<div
class="selection-wrapper nopan"
style={`width: ${rect.width}px; height: ${rect.height}px; transform: translate(${rect.x}px, ${rect.y}px)`}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { onMount, setContext, SvelteComponentTyped } from 'svelte';
import { createEventDispatcher, onMount, setContext, SvelteComponentTyped } from 'svelte';
import cc from 'classcat';
import { type XYPosition, Position, errorMessages } from '@reactflow/system';
@@ -28,10 +28,9 @@
let className: string = '';
export { className as class };
let nodeRef: HTMLDivElement;
const { nodes, transform, nodeTypes, updateNodePositions, addSelectedNodes } = useStore();
let nodeRef: HTMLDivElement;
const nodeTypeValid = !!$nodeTypes[type!];
if (!nodeTypeValid) {
@@ -44,6 +43,7 @@
const isSelectable = true;
const selectNodesOnDrag = false;
const isDraggable = true;
const dispatch = createEventDispatcher();
setContext('rf_nodeid', id);
@@ -55,30 +55,35 @@
};
});
function dispatchEvent(eventName: string) {
const node = $nodes.find(n => n.id === id);
dispatch(eventName, node);
}
function onSelectNodeHandler(event: MouseEvent) {
if (isSelectable && (!selectNodesOnDrag || !isDraggable)) {
// this handler gets called within the drag start event when selectNodesOnDrag=true
addSelectedNodes([id]);
}
// if (onClick) {
// const node = store.getState().nodeInternals.get(id)!;
// onClick(event, { ...node });
// }
dispatchEvent('node:click')
}
</script>
<div
use:drag={{ nodeId: id, nodes, transform, updateNodePositions }}
class={cc(['react-flow__node', `react-flow__node-${type}`, className])}
data-id={id}
class={cc(['svelte-flow__node', `svelte-flow__node-${type}`, className])}
class:initializing={!width && !height}
class:dragging
class:selected
bind:this={nodeRef}
on:click={onSelectNodeHandler}
style:transform={`translate(${positionAbsolute.x}px, ${positionAbsolute.y}px)`}
{style}
data-id={id}
on:click={onSelectNodeHandler}
on:mouseenter={() => dispatchEvent('node:mouseenter')}
on:mouseleave={() => dispatchEvent('node:mouseleave')}
on:mousemove={() => dispatchEvent('node:mousemove')}
>
<svelte:component
this={nodeComponent}
@@ -90,11 +95,14 @@
isConnectable={true}
xPos={positionAbsolute.x}
yPos={positionAbsolute.y}
on:connect:start
on:connect
on:connect:end
/>
</div>
<style>
.react-flow__node {
.svelte-flow__node {
border-radius: 3px;
color: #222;
text-align: center;

View File

@@ -1,5 +1,6 @@
import type { Node } from '$lib/types';
export type NodeWrapperProps = Node & {
'on:nodeclick'?: (event: MouseEvent) => void;
resizeObserver?: ResizeObserver | null;
};

View File

@@ -8,13 +8,13 @@
{#if isVisible}
<div
class="react-flow__selection"
class="svelte-flow__selection"
style={`width: ${width}px; height: ${height}px; transform: translate(${x}px, ${y}px)`}
/>
{/if}
<style>
.react-flow__selection {
.svelte-flow__selection {
position: absolute;
top: 0;
left: 0;
@@ -23,8 +23,8 @@
z-index: 6;
}
.react-flow__selection:focus,
.react-flow__selection:focus-visible {
.svelte-flow__selection:focus,
.svelte-flow__selection:focus-visible {
outline: none;
}
</style>

View File

@@ -16,6 +16,7 @@
export let selected: $$Props['selected'] = false;
</script>
<Handle type="target" position={targetPosition} />
<Handle type="target" position={targetPosition} on:connect:start on:connect on:connect:end />
{data?.label}
<Handle type="source" position={sourcePosition} />
<Handle type="source" position={sourcePosition} on:connect:start on:connect on:connect:end />

View File

@@ -1,12 +1,20 @@
<script lang="ts">
import { Position } from '@reactflow/system';
import type { NodeProps } from '$lib/types';
import { Handle } from '$lib/components/Handle';
export let data: { label: string } = { label: 'Node' };
export let isConnectable: boolean = true;
export let sourcePosition: Position = Position.Bottom;
interface $$Props extends NodeProps<{ label: string }> {}
export let id: $$Props['id'];
export let data: $$Props['data'] = { label: 'Node' };
export let isConnectable: $$Props['isConnectable'] = true;
export let targetPosition: $$Props['targetPosition'] = Position.Top;
export let sourcePosition: $$Props['sourcePosition'] = Position.Bottom;
export let xPos: $$Props['xPos'];
export let yPos: $$Props['yPos'];
export let selected: $$Props['selected'] = false;
</script>
{data?.label}
<Handle type="source" position={sourcePosition} />
<Handle type="source" position={sourcePosition} on:connect:start on:connect on:connect:end />

View File

@@ -1,12 +1,20 @@
<script lang="ts">
import { Position } from '@reactflow/system';
import type { NodeProps } from '$lib/types';
import { Handle } from '$lib/components/Handle';
export let data: { label: string } = { label: 'Node' };
export let isConnectable: boolean = true;
export let targetPosition: Position = Position.Top;
interface $$Props extends NodeProps<{ label: string }> {}
export let id: $$Props['id'];
export let data: $$Props['data'] = { label: 'Node' };
export let isConnectable: $$Props['isConnectable'] = true;
export let targetPosition: $$Props['targetPosition'] = Position.Top;
export let sourcePosition: $$Props['sourcePosition'] = Position.Bottom;
export let xPos: $$Props['xPos'];
export let yPos: $$Props['yPos'];
export let selected: $$Props['selected'] = false;
</script>
{data?.label}
<Handle type="target" position={targetPosition} />
<Handle type="target" position={targetPosition} on:connect:start on:connect on:connect:end />

View File

@@ -5,14 +5,14 @@
const { edgesLayouted, width, height } = useStore();
</script>
<svg width={$width} height={$height} class="react-flow__edges">
<svg width={$width} height={$height} class="svelte-flow__edges">
{#each $edgesLayouted as edge (edge.id)}
<EdgeWrapper {...edge} />
<EdgeWrapper {...edge} on:edge:click />
{/each}
</svg>
<style>
.react-flow__edges {
.svelte-flow__edges {
width: 100%;
height: 100%;
position: absolute;

View File

@@ -21,16 +21,27 @@
onDestroy(() => {
resizeObserver?.disconnect();
});
</script>
<div class="react-flow__nodes">
<div class="svelte-flow__nodes">
{#each $nodes as node (node.id)}
<NodeWrapper {...node} {resizeObserver} />
<NodeWrapper
{...node}
{resizeObserver}
on:node:click
on:node:mouseenter
on:node:mousemove
on:node:mouseleave
on:connect:start
on:connect
on:connect:end
/>
{/each}
</div>
<style>
.react-flow__nodes {
.svelte-flow__nodes {
width: 100%;
height: 100%;
pointer-events: none;

View File

@@ -29,13 +29,15 @@
</script>
<script lang="ts">
import { useStore } from '$lib/store';
import { createEventDispatcher } from 'svelte';
import { SelectionMode } from '@reactflow/system';
import { getEventPosition, getNodesInside } from '@reactflow/utils';
import { useStore } from '$lib/store';
import { getConnectedEdges } from '$lib/utils';
import type { Node, Edge } from '$lib/types';
const dispatch = createEventDispatcher();
const {
nodes,
edges,
@@ -60,7 +62,7 @@
$: hasActiveSelection = elementsSelectable && (isSelecting || $selectionRectMode === 'user');
function onClick(event: MouseEvent) {
// onPaneClick?.(event);
dispatch('pane:click');
resetSelectedElements();
selectionRectMode.set(null);
@@ -155,11 +157,21 @@
selectionRect.set(null);
};
const onContextMenu = (event: MouseEvent) => {
// if (Array.isArray(panOnDrag) && panOnDrag?.includes(2)) {
// event.preventDefault();
// return;
// }
dispatch('pane:contextmenu');
};
</script>
<div
bind:this={container}
class="react-flow__pane"
class="svelte-flow__pane"
class:dragging={$dragging}
class:selection={isSelecting}
on:click={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
@@ -167,12 +179,13 @@
on:mousemove={hasActiveSelection ? onMouseMove : undefined}
on:mouseup={hasActiveSelection ? onMouseUp : undefined}
on:mouseleave={hasActiveSelection ? onMouseLeave : undefined}
on:contextmenu={wrapHandler(onContextMenu, container)}
>
<slot />
</div>
<style>
.react-flow__pane {
.svelte-flow__pane {
cursor: grab;
position: absolute;
top: 0;

View File

@@ -12,34 +12,34 @@
$: positionClasses = `${position}`.split('-');
</script>
<div class={cc(['react-flow__panel', className, ...positionClasses])} {style}>
<div class={cc(['svelte-flow__panel', className, ...positionClasses])} {style}>
<slot />
</div>
<style>
.react-flow__panel {
.svelte-flow__panel {
position: absolute;
z-index: 5;
margin: 15px;
}
.react-flow__panel.top {
.svelte-flow__panel.top {
top: 0;
}
.react-flow__panel.bottom {
.svelte-flow__panel.bottom {
bottom: 0;
}
.react-flow__panel.left {
.svelte-flow__panel.left {
left: 0;
}
.react-flow__panel.right {
.svelte-flow__panel.right {
right: 0;
}
.react-flow__panel.center {
.svelte-flow__panel.center {
left: 50%;
transform: translateX(-50%);
}

View File

@@ -1,6 +1,5 @@
<script lang="ts">
import { setContext, onMount } from 'svelte';
import { ConnectionLineType } from '@reactflow/system';
import { setContext, onMount, createEventDispatcher } from 'svelte';
import cc from 'classcat';
import { key, createStore } from '$lib/store';
@@ -13,21 +12,27 @@
import { NodeSelection } from '$lib/components/NodeSelection';
import { KeyHandler } from '$lib/components/KeyHandler';
import { ConnectionLine } from '$lib/components/ConnectionLine';
import type { SvelteFlowProps } from './types';
import type { SvelteFlowProps, SvelteFlowEvents } from './types';
type $$Props = SvelteFlowProps;
type $$Events = SvelteFlowEvents;
export let id: $$Props['id'] = '1';
export let nodes: $$Props['nodes'] = [];
export let edges: $$Props['edges'] = [];
export let fitView: $$Props['fitView'] = undefined;
export let minZoom: $$Props['minZoom'] = undefined;
export let maxZoom: $$Props['maxZoom'] = undefined;
export let initialViewport: $$Props['initialViewport'] = undefined;
export let nodeTypes: $$Props['nodeTypes'] = undefined;
export let edgeTypes: $$Props['edgeTypes'] = undefined;
export let selectionKey: $$Props['selectionKey'] = undefined;
export let deleteKey: $$Props['deleteKey'] = undefined;
export let connectionLineType: $$Props['connectionLineType'] = ConnectionLineType.Bezier;
export let connectionLineType: $$Props['connectionLineType'] = undefined
let className: $$Props['class'] = undefined;
export { className as class };
const dispatch = createEventDispatcher<{ 'on:nodeclick': number }>();
let domNode: HTMLDivElement;
@@ -51,8 +56,33 @@
store.nodes.subscribe((ns) => {
nodes = ns;
});
store.edges.subscribe((es) => {
edges = es;
});
});
$: {
const updatableProps = {
id,
connectionLineType,
};
Object.keys(updatableProps).forEach(prop => {
// @ts-ignore
if (updatableProps[prop] !== undefined) {
// @ts-ignore
if (!store[prop]) {
// @ts-ignore
console.warn(store[prop], prop, 'ups')
} else {
// @ts-ignore
store[prop].set(updatableProps[prop]);
}
}
})
}
$: {
store.setNodes(nodes);
}
@@ -60,23 +90,47 @@
$: {
store.setEdges(edges);
}
$: {
if (nodeTypes !== undefined) {
store.setNodeTypes(nodeTypes);
}
if (edgeTypes !== undefined) {
store.setEdgeTypes(edgeTypes);
}
if (minZoom !== undefined) {
store.setMinZoom(minZoom);
}
if (maxZoom !== undefined) {
store.setMaxZoom(maxZoom);
}
}
</script>
<div
{...$$restProps}
class={cc(['react-flow', className])}
class={cc(['svelte-flow', className])}
data-testid="rf__wrapper"
{id}
bind:this={domNode}
>
<KeyHandler {selectionKey} {deleteKey} />
<Zoom>
<Pane>
<Zoom {initialViewport}>
<Pane on:pane:click>
<Viewport>
<EdgeRenderer />
<EdgeRenderer on:edge:click />
<ConnectionLine />
<div class="react-flow__edgelabel-renderer" />
<NodeRenderer />
<div class="svelte-flow__edgelabel-renderer" />
<NodeRenderer
on:node:click
on:node:mouseenter
on:node:mousemove
on:node:mouseleave
on:connect:start
on:connect
on:connect:end
/>
<NodeSelection />
</Viewport>
<UserSelection />
@@ -86,7 +140,7 @@
</div>
<style>
.react-flow {
.svelte-flow {
width: 100%;
height: 100%;
overflow: hidden;
@@ -94,13 +148,13 @@
z-index: 0;
}
.react-flow :global(.react-flow__node-default),
.react-flow :global(.react-flow__node-input),
.react-flow :global(.react-flow__node-output) {
.svelte-flow :global(.svelte-flow__node-default),
.svelte-flow :global(.svelte-flow__node-input),
.svelte-flow :global(.svelte-flow__node-output) {
padding: 10px;
}
.react-flow__edgelabel-renderer {
.svelte-flow__edgelabel-renderer {
position: absolute;
width: 100%;
height: 100%;

View File

@@ -1,2 +1,2 @@
export { default as SvelteFlow } from './SvelteFlow.svelte';
export type { SvelteFlowProps } from './types';
export * from './types';

View File

@@ -1,17 +1,49 @@
import type { ConnectionLineType } from '@reactflow/system';
import type { ConnectionLineType, NodeOrigin, Viewport } from '@reactflow/system';
import type { Edge, Node, NodeTypes, KeyDefinition } from '$lib/types';
import type {
Edge,
Node,
NodeTypes,
KeyDefinition,
EdgeTypes,
DefaultEdgeOptions
} from '$lib/types';
export type SvelteFlowProps = {
nodes: Node[];
edges: Edge[];
connectionLineType?: ConnectionLineType;
id?: string;
nodeTypes?: NodeTypes;
edgeTypes?: EdgeTypes;
selectionKey?: KeyDefinition;
deleteKey?: KeyDefinition;
nodeTypes?: NodeTypes;
fitView?: boolean;
nodeOrigin?: NodeOrigin;
minZoom?: number;
maxZoom?: number;
initialViewport?: Viewport;
defaultEdgeOptions?: DefaultEdgeOptions;
class?: string;
style?: string;
id?: string;
connectionLineType?: ConnectionLineType;
};
export type SvelteFlowEvents = {
'node:click': CustomEvent<Node>;
'node:mouseenter': CustomEvent<Node>;
'node:mousemove': CustomEvent<Node>;
'node:mouseleave': CustomEvent<Node>;
'edge:click': CustomEvent<Edge>;
'connect:start': CustomEvent<number>;
connect: CustomEvent<number>;
'connect:end': CustomEvent<number>;
'pane:click': CustomEvent;
'pane:contextmenu': CustomEvent;
};
export type SvelteFlowSlots = {
default: { slotValue: string };
};

View File

@@ -5,14 +5,14 @@
</script>
<div
class="react-flow__viewport"
class="svelte-flow__viewport"
style="transform: translate({$transform[0]}px, {$transform[1]}px) scale({$transform[2]})"
>
<slot />
</div>
<style>
.react-flow__viewport {
.svelte-flow__viewport {
width: 100%;
height: 100%;
position: absolute;

View File

@@ -1,18 +1,23 @@
<script lang="ts">
import { get } from 'svelte/store';
import type { Viewport } from '@reactflow/system';
import { useStore } from '$lib/store';
import zoom from '$lib/actions/zoom';
const { transform, d3, selectionKeyPressed, selectionRectMode } = useStore();
export let initialViewport: Viewport = { x: 0, y: 0, zoom: 1 };
const { transform, d3, selectionKeyPressed, selectionRectMode, minZoom, maxZoom } = useStore();
$: selecting = $selectionKeyPressed || $selectionRectMode === 'user';
</script>
<div class="react-flow__zoom" use:zoom={{ transform, d3, selecting }}>
<div class="svelte-flow__zoom" use:zoom={{ transform, d3, selecting, minZoom: get(minZoom), maxZoom: get(maxZoom), initialViewport }}>
<slot />
</div>
<style>
.react-flow__zoom {
.svelte-flow__zoom {
width: 100%;
height: 100%;
position: absolute;

View File

@@ -1,11 +1,13 @@
import { SvelteFlow, type SvelteFlowProps } from '$lib/container/SvelteFlow';
export { Controls, ControlButton } from '$lib/plugins/Controls';
export { Background, BackgroundVariant } from '$lib/plugins/Background';
export { Minimap } from '$lib/plugins/Minimap';
export { Panel, type PanelProps } from '$lib/container/Panel';
import { SvelteFlow } from '$lib/container/SvelteFlow';
export * from '$lib/container/SvelteFlow';
export * from '$lib/container/Panel';
export * from '$lib/plugins/Controls';
export * from '$lib/plugins/Background';
export * from '$lib/plugins/Minimap';
export * from '$lib/types';
export * from '$lib/utils';
export type { SvelteFlowProps };
export default SvelteFlow;

View File

@@ -47,7 +47,7 @@
: [patternDimensions[0] / 2, patternDimensions[1] / 2];
</script>
<svg class={cc(['react-flow__background', className])} style={style}>
<svg class={cc(['svelte-flow__background', className])} style={style}>
<pattern
id={patternId}
x={$transform[0] % scaledGap[0]}
@@ -67,7 +67,7 @@
</svg>
<style>
.react-flow__background {
.svelte-flow__background {
position: absolute;
width: 100%;
height: 100%;

View File

@@ -9,14 +9,14 @@
<button
type="button"
on:click
class={cc(['react-flow__controls-button', className])}
class={cc(['svelte-flow__controls-button', className])}
{...$$restProps}
>
<slot class="button-svg" />
</button>
<style>
.react-flow__controls-button {
.svelte-flow__controls-button {
border: none;
background: #fefefe;
border-bottom: 1px solid #eee;
@@ -31,7 +31,7 @@
padding: 5px;
}
.react-flow__controls-button :global(svg) {
.svelte-flow__controls-button :global(svg) {
width: 100%;
max-width: 12px;
max-height: 12px;

View File

@@ -43,11 +43,11 @@
};
</script>
<Panel class="react-flow__controls" {position}>
<Panel class="svelte-flow__controls" {position}>
{#if showZoom}
<ControlButton
on:click={onZoomInHandler}
class="react-flow__controls-zoomin"
class="svelte-flow__controls-zoomin"
title="zoom in"
aria-label="zoom in"
>
@@ -56,7 +56,7 @@
<svelte:component
this={ControlButton}
on:click={onZoomOutHandler}
class="react-flow__controls-zoomout"
class="svelte-flow__controls-zoomout"
title="zoom out"
aria-label="zoom out"
>
@@ -66,7 +66,7 @@
{#if showFitView}
<svelte:component
this={ControlButton}
class="react-flow__controls-fitview"
class="svelte-flow__controls-fitview"
on:click={onFitViewHandler}
title="fit view"
aria-label="fit view"
@@ -77,7 +77,7 @@
{#if showInteractive}
<svelte:component
this={ControlButton}
class="react-flow__controls-interactive"
class="svelte-flow__controls-interactive"
on:click={onToggleInteractivity}
title="toggle interactivity"
aria-label="toggle interactivity"

View File

@@ -1,3 +1,3 @@
export { default as Controls } from './Controls.svelte';
export { default as ControlButton } from './ControlButton.svelte';
export { type ControlsProps } from './types';
export * from './types';

View File

@@ -1,8 +1,8 @@
import type { PanelPosition } from '@reactflow/system';
export type ControlsProps = {
position: PanelPosition;
showZoom: boolean;
showFitView: boolean;
showInteractive: boolean;
position?: PanelPosition;
showZoom?: boolean;
showFitView?: boolean;
showInteractive?: boolean;
};

View File

@@ -43,7 +43,7 @@
const nodeClassNameFunc = getAttrFunction(nodeClassName);
const shapeRendering =
typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision';
const labelledBy = `react-flow__minimap-desc-${$id}`;
const labelledBy = `svelte-flow__minimap-desc-${$id}`;
$: viewBB = {
x: -$transform[0] / $transform[2],
@@ -69,7 +69,7 @@
<Panel
{position}
class={cc(['react-flow__minimap', className])}
class={cc(['svelte-flow__minimap', className])}
style={`background-color: ${bgColor}; ${style}`}
>
<svg
@@ -100,7 +100,7 @@
{/if}
{/each}
<path
class="react-flow__minimap-mask"
class="svelte-flow__minimap-mask"
d={`M${x - offset},${y - offset}h${viewboxWidth + offset * 2}v${viewboxHeight + offset * 2}h${
-viewboxWidth - offset * 2
}z

View File

@@ -19,7 +19,7 @@
</script>
<rect
class={cc(['react-flow__minimap-node', className])}
class={cc(['svelte-flow__minimap-node', className])}
{x}
{y}
rx={borderRadius}

View File

@@ -18,7 +18,12 @@ import { getHandleBounds, getConnectedEdges, addEdge as addEdgeUtil } from '$lib
import type { EdgeTypes, NodeTypes, Node, Edge, ConnectionData } from '$lib/types';
import { getEdgesLayouted } from './edges-layouted';
import { getConnectionPath } from './connection-path';
import { initConnectionData, initialStoreState } from './initial-store';
import {
initConnectionData,
initialEdgeTypes,
initialNodeTypes,
initialStoreState
} from './initial-store';
import type { SvelteFlowStore } from './types';
export const key = Symbol();
@@ -32,20 +37,27 @@ type CreateStoreProps = {
id?: string;
};
export function createStore({
transform = [0, 0, 1],
nodeOrigin = [0, 0],
fitView: fitViewOnInit = false,
nodeTypes = {},
edgeTypes = {},
id = '1'
}: CreateStoreProps): SvelteFlowStore {
export function createStore({ fitView: fitViewOnInit = false }: CreateStoreProps): SvelteFlowStore {
const store = {
...initialStoreState
};
let fitViewOnInitDone = false;
function setNodeTypes(nodeTypes: NodeTypes) {
store.nodeTypes.set({
...initialNodeTypes,
...nodeTypes
});
}
function setEdgeTypes(edgeTypes: EdgeTypes) {
store.edgeTypes.set({
...initialEdgeTypes,
...edgeTypes
});
}
function setEdges(edges: Edge[]) {
store.edges.set(edges);
}
@@ -91,7 +103,7 @@ export function createStore({
}
function updateNodeDimensions(updates: NodeDimensionUpdate[]) {
const viewportNode = document?.querySelector('.react-flow__viewport');
const viewportNode = document?.querySelector('.svelte-flow__viewport');
if (!viewportNode) {
return;
@@ -153,6 +165,25 @@ export function createStore({
}
}
function setMinZoom(minZoom: number) {
const d3Zoom = get(store.d3).zoom;
if (d3Zoom) {
d3Zoom?.scaleExtent([minZoom, get(store.maxZoom)]);
store.minZoom.set(minZoom);
}
}
function setMaxZoom(maxZoom: number) {
const d3Zoom = get(store.d3).zoom;
if (d3Zoom) {
d3Zoom?.scaleExtent([get(store.minZoom), maxZoom]);
store.maxZoom.set(maxZoom);
}
}
function fitView() {
const { zoom: d3Zoom, selection: d3Selection } = get(store.d3);
@@ -309,12 +340,16 @@ export function createStore({
// actions
setNodes,
setEdges,
setNodeTypes,
setEdgeTypes,
addEdge,
updateNodePositions,
updateNodeDimensions,
zoomIn,
zoomOut,
fitView,
setMinZoom,
setMaxZoom,
resetSelectedElements,
addSelectedNodes,
panBy,

View File

@@ -13,10 +13,10 @@ import {
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
import InputNode from '$lib/components/nodes/InputNode.svelte';
import OutputNode from '$lib/components/nodes/OutputNode.svelte';
import type { Node, Edge, ConnectionData, NodeTypes, EdgeTypes, EdgeLayouted } from '$lib/types';
import BezierEdge from '$lib/components/edges/BezierEdge.svelte';
import StraightEdge from '$lib/components/edges/StraightEdge.svelte';
import SmoothStepEdge from '$lib/components/edges/SmoothStepEdge.svelte';
import type { Node, Edge, ConnectionData, NodeTypes, EdgeTypes, EdgeLayouted } from '$lib/types';
export const initConnectionData = {
nodeId: null,
@@ -26,18 +26,32 @@ export const initConnectionData = {
status: null
};
export const initialNodeTypes = {
input: InputNode,
output: OutputNode,
default: DefaultNode
};
export const initialEdgeTypes = {
straight: StraightEdge,
smoothstep: SmoothStepEdge,
default: BezierEdge
};
export const initialStoreState = {
id: writable<string | null>(null),
nodes: writable<Node[]>([]),
edges: writable<Edge[]>([]),
edgesLayouted: readable<EdgeLayouted[]>([]),
height: writable<number>(500),
width: writable<number>(500),
nodeOrigin: writable<NodeOrigin>([0.5, 0.5]),
minZoom: writable<number>(0.5),
maxZoom: writable<number>(2),
nodeOrigin: writable<NodeOrigin>([0, 0]),
d3: writable<{ zoom: D3ZoomInstance | null; selection: D3SelectionInstance | null }>({
zoom: null,
selection: null
}),
id: writable<string | null>(null),
dragging: writable<boolean>(false),
selectionRect: writable<SelectionRect | null>(null),
selectionKeyPressed: writable<boolean>(false),
@@ -45,16 +59,8 @@ export const initialStoreState = {
deleteKeyPressed: writable<boolean>(false),
selectionRectMode: writable<string | null>(null),
selectionMode: writable<SelectionMode>(SelectionMode.Partial),
nodeTypes: writable<NodeTypes>({
input: InputNode,
output: OutputNode,
default: DefaultNode
}),
edgeTypes: writable<EdgeTypes>({
straight: StraightEdge,
smoothstep: SmoothStepEdge,
default: BezierEdge
}),
nodeTypes: writable<NodeTypes>(initialNodeTypes),
edgeTypes: writable<EdgeTypes>(initialEdgeTypes),
transform: writable<Transform>([0, 0, 1]),
connectionMode: writable<ConnectionMode>(ConnectionMode.Strict),
domNode: writable<HTMLDivElement | null>(null),

View File

@@ -7,14 +7,18 @@ import type {
} from '@reactflow/system';
import type { initialStoreState } from './initial-store';
import type { Node, Edge, ConnectionData } from '$lib/types';
import type { Node, Edge, ConnectionData, NodeTypes, EdgeTypes } from '$lib/types';
export type SvelteFlowStoreActions = {
setNodes: (nodes: Node[]) => void;
setEdges: (edges: Edge[]) => void;
setNodeTypes: (nodeTypes: NodeTypes) => void;
setEdgeTypes: (edgeTypes: EdgeTypes) => void;
addEdge: (edge: Edge | Connection) => void;
zoomIn: (options?: ViewportHelperFunctionOptions) => void;
zoomOut: (options?: ViewportHelperFunctionOptions) => void;
setMinZoom: (minZoom: number) => void;
setMaxZoom: (maxZoom: number) => void;
fitView: (options?: ViewportHelperFunctionOptions) => boolean;
updateNodePositions: (
nodeDragItems: NodeDragItem[],

View File

@@ -3,6 +3,7 @@ import type { SvelteComponentTyped } from 'svelte';
import type {
BaseEdge,
BezierPathOptions,
DefaultEdgeOptionsBase,
Position,
SmoothStepPathOptions
} from '@reactflow/system';
@@ -58,3 +59,5 @@ export type EdgeProps = Pick<
>;
export type EdgeTypes = Record<string, typeof SvelteComponentTyped<EdgeProps>>;
export type DefaultEdgeOptions = DefaultEdgeOptionsBase<Edge>;

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import type { Node, Edge, NodeTypes } from '../lib/types';
import SvelteFlow, {
Controls,
Background,
BackgroundVariant,
Minimap,
Panel
Panel,
type Node, type Edge, type NodeTypes
} from '../lib/index';
import CustomNode from '../customnodes/Custom.svelte';
@@ -120,7 +120,24 @@
// }
</script>
<SvelteFlow bind:nodes bind:edges {nodeTypes} fitView>
<SvelteFlow
bind:nodes
bind:edges
{nodeTypes}
fitView
minZoom={0.1}
maxZoom={2.5}
initialViewport={{ x: 100, y: 100, zoom: 2 }}
on:node:click={(event) => console.log('on node click', event)}
on:node:mouseenter={(event) => console.log('on node enter', event)}
on:node:mouseleave={(event) => console.log('on node leave', event)}
on:edge:click={(event) => console.log('edge click', event)}
on:connect:start={(event) => console.log('on connect start', event)}
on:connect={(event) => console.log('on connect', event)}
on:connect:end={(event) => console.log('on connect end', event)}
on:pane:click={(event) => console.log('on pane click', event)}
on:pane:contextmenu={(event) => { console.log('on pane contextmenu', event); }}
>
<Controls />
<Background variant={BackgroundVariant.Dots} />
<Minimap />
@@ -134,7 +151,7 @@
--node-width: 50;
}
:global(.react-flow .custom-style) {
:global(.svelte-flow .custom-style) {
background: #ff5050;
color: white;
}

View File

@@ -10,6 +10,7 @@
"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