Merge branch 'main' into patch-1
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import { get } from 'svelte/store';
|
||||
import { XYDrag, type OnDrag } from '@xyflow/system';
|
||||
import { XYDrag, type NodeBase, type OnDrag, type XYDragParams } from '@xyflow/system';
|
||||
|
||||
import type { SvelteFlowStore } from '$lib/store/types';
|
||||
import type { Node, Edge, NodeTargetEventWithPointer } from '$lib/types';
|
||||
|
||||
export type UseDragParams = {
|
||||
store: SvelteFlowStore;
|
||||
export type UseDragParams<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
|
||||
store: SvelteFlowStore<NodeType, EdgeType>;
|
||||
disabled?: boolean;
|
||||
noDragClass?: string;
|
||||
handleSelector?: string;
|
||||
@@ -17,7 +17,10 @@ export type UseDragParams = {
|
||||
onNodeMouseDown?: (id: string) => void;
|
||||
};
|
||||
|
||||
export default function drag(domNode: Element, params: UseDragParams) {
|
||||
export default function drag<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
||||
domNode: Element,
|
||||
params: UseDragParams<NodeType, EdgeType>
|
||||
) {
|
||||
const { store, onDrag, onDragStart, onDragStop, onNodeMouseDown } = params;
|
||||
const dragInstance = XYDrag({
|
||||
onDrag,
|
||||
@@ -25,32 +28,34 @@ export default function drag(domNode: Element, params: UseDragParams) {
|
||||
onDragStop,
|
||||
onNodeMouseDown,
|
||||
getStoreItems: () => {
|
||||
const snapGrid = get(store.snapGrid);
|
||||
const vp = get(store.viewport);
|
||||
const { snapGrid, viewport } = store;
|
||||
|
||||
return {
|
||||
nodes: get(store.nodes),
|
||||
nodeLookup: get(store.nodeLookup),
|
||||
edges: get(store.edges),
|
||||
nodeExtent: get(store.nodeExtent),
|
||||
nodes: store.nodes satisfies NodeBase[],
|
||||
nodeLookup: store.nodeLookup,
|
||||
edges: store.edges,
|
||||
nodeExtent: store.nodeExtent,
|
||||
snapGrid: snapGrid ? snapGrid : [0, 0],
|
||||
snapToGrid: !!snapGrid,
|
||||
nodeOrigin: get(store.nodeOrigin),
|
||||
multiSelectionActive: get(store.multiselectionKeyPressed),
|
||||
domNode: get(store.domNode),
|
||||
transform: [vp.x, vp.y, vp.zoom],
|
||||
autoPanOnNodeDrag: get(store.autoPanOnNodeDrag),
|
||||
nodesDraggable: get(store.nodesDraggable),
|
||||
selectNodesOnDrag: get(store.selectNodesOnDrag),
|
||||
nodeDragThreshold: get(store.nodeDragThreshold),
|
||||
nodeOrigin: store.nodeOrigin,
|
||||
multiSelectionActive: store.multiselectionKeyPressed,
|
||||
domNode: store.domNode,
|
||||
transform: [viewport.x, viewport.y, viewport.zoom],
|
||||
autoPanOnNodeDrag: store.autoPanOnNodeDrag,
|
||||
nodesDraggable: store.nodesDraggable,
|
||||
selectNodesOnDrag: store.selectNodesOnDrag,
|
||||
nodeDragThreshold: store.nodeDragThreshold,
|
||||
unselectNodesAndEdges: store.unselectNodesAndEdges,
|
||||
updateNodePositions: store.updateNodePositions,
|
||||
onSelectionDrag: store.onselectiondrag,
|
||||
onSelectionDragStart: store.onselectiondragstart,
|
||||
onSelectionDragStop: store.onselectiondragstop,
|
||||
panBy: store.panBy
|
||||
};
|
||||
}
|
||||
});
|
||||
} as XYDragParams<NodeTargetEventWithPointer<MouseEvent | TouchEvent, NodeType>>);
|
||||
|
||||
function updateDrag(domNode: Element, params: UseDragParams) {
|
||||
function updateDrag(domNode: Element, params: UseDragParams<NodeType, EdgeType>) {
|
||||
if (params.disabled) {
|
||||
dragInstance.destroy();
|
||||
return;
|
||||
@@ -69,7 +74,7 @@ export default function drag(domNode: Element, params: UseDragParams) {
|
||||
updateDrag(domNode, params);
|
||||
|
||||
return {
|
||||
update(params: UseDragParams) {
|
||||
update(params: UseDragParams<NodeType, EdgeType>) {
|
||||
updateDrag(domNode, params);
|
||||
},
|
||||
destroy() {
|
||||
|
||||
@@ -1,31 +1 @@
|
||||
type PortalOptions = {
|
||||
target?: string;
|
||||
domNode: Element | null;
|
||||
};
|
||||
|
||||
function tryToMount(node: Element, domNode: Element | null, target: string | undefined) {
|
||||
if (!domNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetEl = target ? domNode.querySelector(target) : domNode;
|
||||
|
||||
if (targetEl) {
|
||||
targetEl.appendChild(node);
|
||||
}
|
||||
}
|
||||
|
||||
export default function (node: Element, { target, domNode }: PortalOptions) {
|
||||
tryToMount(node, domNode, target);
|
||||
|
||||
return {
|
||||
async update({ target, domNode }: PortalOptions) {
|
||||
tryToMount(node, domNode, target);
|
||||
},
|
||||
destroy() {
|
||||
if (node.parentNode) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
export { portal } from './portal.svelte';
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
type Portal = 'viewport-back' | 'viewport-front' | 'root' | 'edge-labels';
|
||||
|
||||
function tryToMount(node: Element, domNode: Element | null, target: Portal | undefined) {
|
||||
if (!target || !domNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetEl = target === 'root' ? domNode : domNode.querySelector(`.svelte-flow__${target}`);
|
||||
|
||||
if (targetEl) {
|
||||
targetEl.appendChild(node);
|
||||
}
|
||||
}
|
||||
|
||||
export function portal(node: Element, target: Portal | undefined) {
|
||||
// TODO: does this work if called outside of SvelteFlow
|
||||
const store = useStore();
|
||||
|
||||
let previousTarget: Portal | undefined = target;
|
||||
|
||||
tryToMount(node, store.domNode, target);
|
||||
|
||||
return {
|
||||
async update(target: Portal) {
|
||||
if (target !== previousTarget) {
|
||||
node.parentNode?.removeChild(node);
|
||||
previousTarget = target;
|
||||
}
|
||||
tryToMount(node, store.domNode, target);
|
||||
},
|
||||
destroy() {
|
||||
if (node.parentNode) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Writable } from 'svelte/store';
|
||||
import {
|
||||
PanOnScrollMode,
|
||||
XYPanZoom,
|
||||
@@ -10,17 +9,16 @@ import {
|
||||
} from '@xyflow/system';
|
||||
|
||||
type ZoomParams = {
|
||||
viewport: Writable<Viewport>;
|
||||
viewport: Viewport;
|
||||
initialViewport: Viewport;
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
dragging: Writable<boolean>;
|
||||
setPanZoomInstance: (panZoomInstance: PanZoomInstance) => void;
|
||||
onPanZoomStart?: OnPanZoom;
|
||||
onPanZoom?: OnPanZoom;
|
||||
onPanZoomEnd?: OnPanZoom;
|
||||
onPaneContextMenu?: (event: MouseEvent) => void;
|
||||
translateExtent: CoordinateExtent;
|
||||
panZoom: Writable<PanZoomInstance | null>;
|
||||
zoomOnScroll: boolean;
|
||||
zoomOnPinch: boolean;
|
||||
zoomOnDoubleClick: boolean;
|
||||
@@ -38,18 +36,19 @@ type ZoomParams = {
|
||||
lib: string;
|
||||
paneClickDistance: number;
|
||||
onTransformChange: (transform: Transform) => void;
|
||||
onDraggingChange: (dragging: boolean) => void;
|
||||
};
|
||||
|
||||
export default function zoom(domNode: Element, params: ZoomParams) {
|
||||
const {
|
||||
panZoom,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
initialViewport,
|
||||
viewport,
|
||||
dragging,
|
||||
translateExtent,
|
||||
paneClickDistance
|
||||
paneClickDistance,
|
||||
setPanZoomInstance,
|
||||
onDraggingChange,
|
||||
onTransformChange
|
||||
} = params;
|
||||
|
||||
const panZoomInstance = XYPanZoom({
|
||||
@@ -59,11 +58,19 @@ export default function zoom(domNode: Element, params: ZoomParams) {
|
||||
translateExtent,
|
||||
viewport: initialViewport,
|
||||
paneClickDistance,
|
||||
onDraggingChange: dragging.set
|
||||
onDraggingChange
|
||||
});
|
||||
const currentViewport = panZoomInstance.getViewport();
|
||||
viewport.set(currentViewport);
|
||||
panZoom.set(panZoomInstance);
|
||||
|
||||
const viewport = panZoomInstance.getViewport();
|
||||
if (
|
||||
initialViewport.x !== viewport.x ||
|
||||
initialViewport.y !== viewport.y ||
|
||||
initialViewport.zoom !== viewport.zoom
|
||||
) {
|
||||
onTransformChange([viewport.x, viewport.y, viewport.zoom]);
|
||||
}
|
||||
|
||||
setPanZoomInstance(panZoomInstance);
|
||||
|
||||
panZoomInstance.update(params);
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
|
||||
import type { SvelteFlowStore } from '$lib/store/types';
|
||||
import type { Node, Edge } from '$lib/types';
|
||||
import { ARIA_EDGE_DESC_KEY, ARIA_LIVE_MESSAGE, ARIA_NODE_DESC_KEY } from '.';
|
||||
|
||||
let { store }: { store: SvelteFlowStore<NodeType, EdgeType> } = $props();
|
||||
</script>
|
||||
|
||||
<div id={`${ARIA_NODE_DESC_KEY}-${store.flowId}`} style="display: none;">
|
||||
Press enter or space to select a node.
|
||||
{#if !store.disableKeyboardA11y}
|
||||
You can then use the arrow keys to move the node around.
|
||||
{/if}
|
||||
Press delete to remove it and escape to cancel.
|
||||
</div>
|
||||
<div id={`${ARIA_EDGE_DESC_KEY}-${store.flowId}`} style="display: none;">
|
||||
Press enter or space to select an edge. You can then press delete to remove it or escape to
|
||||
cancel.
|
||||
</div>
|
||||
|
||||
{#if !store.disableKeyboardA11y}
|
||||
<div
|
||||
id={`${ARIA_LIVE_MESSAGE}-${store.flowId}`}
|
||||
aria-live="assertive"
|
||||
aria-atomic="true"
|
||||
style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0; padding: 0; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); clip-path: inset(100%);"
|
||||
>
|
||||
{store.ariaLiveMessage}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,5 @@
|
||||
export { default as A11yDescriptions } from './A11yDescriptions.svelte';
|
||||
|
||||
export const ARIA_NODE_DESC_KEY = 'svelte-flow__node-desc';
|
||||
export const ARIA_EDGE_DESC_KEY = 'svelte-flow__edge-desc';
|
||||
export const ARIA_LIVE_MESSAGE = 'svelte-flow__aria-live';
|
||||
@@ -2,10 +2,7 @@
|
||||
import { Panel } from '$lib/container/Panel';
|
||||
import type { AttributionProps } from './types';
|
||||
|
||||
type $$Props = AttributionProps;
|
||||
|
||||
export let proOptions: $$Props['proOptions'] = undefined;
|
||||
export let position: $$Props['position'] = 'bottom-right';
|
||||
let { proOptions, position = 'bottom-right' }: AttributionProps = $props();
|
||||
</script>
|
||||
|
||||
{#if !proOptions?.hideAttribution}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<script lang="ts">
|
||||
import cc from 'classcat';
|
||||
import type { BaseEdgeProps } from './types';
|
||||
import EdgeLabel from '../EdgeLabel/EdgeLabel.svelte';
|
||||
|
||||
type $$Props = BaseEdgeProps;
|
||||
|
||||
export let id: $$Props['id'] = undefined;
|
||||
export let path: $$Props['path'];
|
||||
export let label: $$Props['label'] = undefined;
|
||||
export let labelX: $$Props['labelX'] = undefined;
|
||||
export let labelY: $$Props['labelY'] = undefined;
|
||||
export let labelStyle: $$Props['labelStyle'] = undefined;
|
||||
export let markerStart: $$Props['markerStart'] = undefined;
|
||||
export let markerEnd: $$Props['markerEnd'] = undefined;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let interactionWidth: $$Props['interactionWidth'] = 20;
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
|
||||
let interactionWidthValue = interactionWidth === undefined ? 20 : interactionWidth;
|
||||
</script>
|
||||
|
||||
<path
|
||||
{id}
|
||||
d={path}
|
||||
class={cc(['svelte-flow__edge-path', className])}
|
||||
marker-start={markerStart}
|
||||
marker-end={markerEnd}
|
||||
fill="none"
|
||||
{style}
|
||||
/>
|
||||
|
||||
{#if interactionWidthValue}
|
||||
<path
|
||||
d={path}
|
||||
stroke-opacity={0}
|
||||
stroke-width={interactionWidthValue}
|
||||
fill="none"
|
||||
class="svelte-flow__edge-interaction"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if label}
|
||||
<EdgeLabel x={labelX} y={labelY} style={labelStyle}>
|
||||
{label}
|
||||
</EdgeLabel>
|
||||
{/if}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as BaseEdge } from './BaseEdge.svelte';
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { EdgeProps } from '$lib/types';
|
||||
|
||||
export type BaseEdgeProps = Pick<
|
||||
EdgeProps,
|
||||
'interactionWidth' | 'label' | 'labelStyle' | 'style'
|
||||
> & {
|
||||
id?: string;
|
||||
/** SVG path of the edge */
|
||||
path: string;
|
||||
/** The x coordinate of the label */
|
||||
labelX?: number;
|
||||
/** The y coordinate of the label */
|
||||
labelY?: number;
|
||||
/** Marker at start of edge
|
||||
* @example 'url(#arrow)'
|
||||
*/
|
||||
markerStart?: string;
|
||||
/** Marker at end of edge
|
||||
* @example 'url(#arrow)'
|
||||
*/
|
||||
markerEnd?: string;
|
||||
class?: string;
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let _onMount: (() => void) | undefined = undefined;
|
||||
export { _onMount as onMount };
|
||||
|
||||
let _onDestroy: (() => void) | undefined = undefined;
|
||||
export { _onDestroy as onDestroy };
|
||||
|
||||
onMount(() => {
|
||||
_onMount?.();
|
||||
return _onDestroy;
|
||||
});
|
||||
</script>
|
||||
@@ -1 +0,0 @@
|
||||
export { default as CallOnMount } from './CallOnMount.svelte';
|
||||
@@ -1,7 +1,5 @@
|
||||
<script lang="ts">
|
||||
import cc from 'classcat';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
|
||||
import type { Component } from 'svelte';
|
||||
import {
|
||||
ConnectionLineType,
|
||||
getBezierPath,
|
||||
@@ -10,50 +8,69 @@
|
||||
getStraightPath
|
||||
} from '@xyflow/system';
|
||||
|
||||
export let containerStyle: string = '';
|
||||
export let style: string = '';
|
||||
export let isCustomComponent: boolean = false;
|
||||
import type { SvelteFlowStore } from '$lib/store/types';
|
||||
import type { Node, Edge } from '$lib/types';
|
||||
|
||||
const { width, height, connection, connectionLineType } = useStore();
|
||||
let {
|
||||
store = $bindable(),
|
||||
type,
|
||||
containerStyle,
|
||||
style,
|
||||
LineComponent
|
||||
}: {
|
||||
store: SvelteFlowStore<NodeType, EdgeType>;
|
||||
type: ConnectionLineType;
|
||||
containerStyle?: string;
|
||||
style?: string;
|
||||
LineComponent?: Component;
|
||||
} = $props();
|
||||
|
||||
let path: string | null = null;
|
||||
let path = $derived.by(() => {
|
||||
if (!store.connection.inProgress) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$: if ($connection.inProgress && !isCustomComponent) {
|
||||
const { from, to, fromPosition, toPosition } = $connection;
|
||||
const pathParams = {
|
||||
sourceX: from.x,
|
||||
sourceY: from.y,
|
||||
sourcePosition: fromPosition,
|
||||
targetX: to.x,
|
||||
targetY: to.y,
|
||||
targetPosition: toPosition
|
||||
sourceX: store.connection.from.x,
|
||||
sourceY: store.connection.from.y,
|
||||
sourcePosition: store.connection.fromPosition,
|
||||
targetX: store.connection.to.x,
|
||||
targetY: store.connection.to.y,
|
||||
targetPosition: store.connection.toPosition
|
||||
};
|
||||
|
||||
switch ($connectionLineType) {
|
||||
case ConnectionLineType.Bezier:
|
||||
[path] = getBezierPath(pathParams);
|
||||
break;
|
||||
switch (type) {
|
||||
case ConnectionLineType.Bezier: {
|
||||
const [path] = getBezierPath(pathParams);
|
||||
return path;
|
||||
}
|
||||
case ConnectionLineType.Straight: {
|
||||
const [path] = getStraightPath(pathParams);
|
||||
return path;
|
||||
}
|
||||
case ConnectionLineType.Step:
|
||||
[path] = getSmoothStepPath({
|
||||
case ConnectionLineType.SmoothStep: {
|
||||
const [path] = getSmoothStepPath({
|
||||
...pathParams,
|
||||
borderRadius: 0
|
||||
borderRadius: type === ConnectionLineType.Step ? 0 : undefined
|
||||
});
|
||||
break;
|
||||
case ConnectionLineType.SmoothStep:
|
||||
[path] = getSmoothStepPath(pathParams);
|
||||
break;
|
||||
default:
|
||||
[path] = getStraightPath(pathParams);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if $connection.inProgress}
|
||||
<svg width={$width} height={$height} class="svelte-flow__connectionline" style={containerStyle}>
|
||||
<g class={cc(['svelte-flow__connection', getConnectionStatus($connection.isValid)])}>
|
||||
<slot name="connectionLine" />
|
||||
<!-- slot fallbacks do not work if slots are forwarded in parent -->
|
||||
{#if !isCustomComponent}
|
||||
{#if store.connection.inProgress}
|
||||
<svg
|
||||
width={store.width}
|
||||
height={store.height}
|
||||
class="svelte-flow__connectionline"
|
||||
style={containerStyle}
|
||||
>
|
||||
<g class={['svelte-flow__connection', getConnectionStatus(store.connection.isValid)]}>
|
||||
{#if LineComponent}
|
||||
<LineComponent></LineComponent>
|
||||
{:else}
|
||||
<path d={path} {style} fill="none" class="svelte-flow__connection-path" />
|
||||
{/if}
|
||||
</g>
|
||||
|
||||
@@ -1,31 +1,53 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { portal } from '$lib/actions/portal';
|
||||
|
||||
import { EdgeLabelRenderer } from '$lib/components/EdgeLabelRenderer';
|
||||
import { useHandleEdgeSelect } from '$lib/hooks/useHandleEdgeSelect';
|
||||
import type { BaseEdgeProps } from '$lib/components/BaseEdge/types';
|
||||
import { useStore } from '$lib/store';
|
||||
import type { EdgeLabelProps } from './types';
|
||||
import { toPxString } from '$lib/utils';
|
||||
|
||||
export let style: BaseEdgeProps['labelStyle'] = undefined;
|
||||
export let x: BaseEdgeProps['labelX'] = undefined;
|
||||
export let y: BaseEdgeProps['labelY'] = undefined;
|
||||
let {
|
||||
x = 0,
|
||||
y = 0,
|
||||
width,
|
||||
height,
|
||||
selectEdgeOnClick = false,
|
||||
transparent = false,
|
||||
class: className,
|
||||
children,
|
||||
...rest
|
||||
}: EdgeLabelProps = $props();
|
||||
|
||||
const handleEdgeSelect = useHandleEdgeSelect();
|
||||
const store = useStore();
|
||||
|
||||
const id = getContext<string>('svelteflow__edge_id');
|
||||
|
||||
let z = $derived.by(() => {
|
||||
return store.visible.edges.get(id)?.zIndex;
|
||||
});
|
||||
</script>
|
||||
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
class="svelte-flow__edge-label"
|
||||
style:transform="translate(-50%, -50%) translate({x}px,{y}px)"
|
||||
style={'pointer-events: all;' + style}
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
on:keyup={() => {}}
|
||||
on:click={() => {
|
||||
if (id) handleEdgeSelect(id);
|
||||
}}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
<div
|
||||
use:portal={'edge-labels'}
|
||||
class={['svelte-flow__edge-label', { transparent }, className]}
|
||||
style:cursor={selectEdgeOnClick ? 'pointer' : undefined}
|
||||
style:transform="translate(-50%, -50%) translate({x}px,{y}px)"
|
||||
style:pointer-events="all"
|
||||
style:width={toPxString(width)}
|
||||
style:height={toPxString(height)}
|
||||
style:z-index={z}
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
onclick={() => {
|
||||
if (selectEdgeOnClick && id) store.handleEdgeSelection(id);
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.transparent {
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { default as EdgeLabel } from './EdgeLabel.svelte';
|
||||
export * from './types';
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
export type EdgeLabelProps = {
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
selectEdgeOnClick?: boolean;
|
||||
transparent?: boolean;
|
||||
children?: Snippet;
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
@@ -1,10 +0,0 @@
|
||||
<script lang="ts">
|
||||
import portal from '$lib/actions/portal';
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
const { domNode } = useStore();
|
||||
</script>
|
||||
|
||||
<div use:portal={{ target: '.svelte-flow__edgelabel-renderer', domNode: $domNode }}>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -1 +0,0 @@
|
||||
export { default as EdgeLabelRenderer } from './EdgeLabelRenderer.svelte';
|
||||
@@ -0,0 +1,123 @@
|
||||
<script lang="ts">
|
||||
import { useStore } from '$lib/store';
|
||||
import type { Edge } from '$lib/types';
|
||||
import { XYHandle, type HandleType } from '@xyflow/system';
|
||||
import { getContext } from 'svelte';
|
||||
import { EdgeLabel } from '../EdgeLabel';
|
||||
import type { EdgeReconnectAnchorProps } from './types';
|
||||
|
||||
let {
|
||||
type,
|
||||
reconnecting = $bindable(false),
|
||||
position,
|
||||
class: className,
|
||||
size = 25,
|
||||
children,
|
||||
...rest
|
||||
}: EdgeReconnectAnchorProps = $props();
|
||||
|
||||
const store = useStore();
|
||||
|
||||
let edgeId: string | undefined = getContext('svelteflow__edge_id');
|
||||
|
||||
if (!edgeId) {
|
||||
throw new Error('EdgeReconnectAnchor must be used within an Edge component');
|
||||
}
|
||||
|
||||
const onPointerDown = (event: PointerEvent) => {
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
autoPanOnConnect,
|
||||
domNode,
|
||||
isValidConnection,
|
||||
connectionMode,
|
||||
connectionRadius,
|
||||
onconnectstart,
|
||||
onconnectend,
|
||||
onreconnect,
|
||||
onreconnectstart,
|
||||
onreconnectend,
|
||||
onbeforereconnect,
|
||||
cancelConnection,
|
||||
nodeLookup,
|
||||
flowId,
|
||||
panBy,
|
||||
updateConnection,
|
||||
edgeLookup
|
||||
} = store;
|
||||
|
||||
let newEdge: Edge | undefined;
|
||||
let edge = edgeLookup.get(edgeId)!;
|
||||
|
||||
reconnecting = true;
|
||||
onreconnectstart?.(event, edge, type);
|
||||
|
||||
const opposite =
|
||||
type === 'target'
|
||||
? { nodeId: edge.source, handleId: edge.sourceHandle ?? null, type: 'source' as HandleType }
|
||||
: {
|
||||
nodeId: edge.target,
|
||||
handleId: edge.targetHandle ?? null,
|
||||
type: 'target' as HandleType
|
||||
};
|
||||
|
||||
XYHandle.onPointerDown(event, {
|
||||
autoPanOnConnect,
|
||||
connectionMode,
|
||||
connectionRadius,
|
||||
domNode,
|
||||
handleId: opposite.handleId,
|
||||
nodeId: opposite.nodeId,
|
||||
nodeLookup,
|
||||
isTarget: opposite.type === 'target',
|
||||
edgeUpdaterType: opposite.type,
|
||||
lib: 'svelte',
|
||||
flowId,
|
||||
cancelConnection,
|
||||
panBy,
|
||||
isValidConnection,
|
||||
onConnectStart: onconnectstart,
|
||||
onConnectEnd: onconnectend,
|
||||
onConnect: (connection) => {
|
||||
newEdge = { ...edge, ...connection };
|
||||
newEdge = onbeforereconnect ? (onbeforereconnect(newEdge, edge) ?? undefined) : newEdge;
|
||||
|
||||
if (newEdge) {
|
||||
store.edges = store.edges.map((e) => (e.id === edge.id ? (newEdge as Edge) : e));
|
||||
}
|
||||
|
||||
onreconnect?.(edge, connection);
|
||||
},
|
||||
onReconnectEnd: (event, connectionState) => {
|
||||
reconnecting = false;
|
||||
onreconnectend?.(event, edge, opposite.type, connectionState);
|
||||
},
|
||||
updateConnection,
|
||||
getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom],
|
||||
getFromHandle: () => store.connection.fromHandle
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<EdgeLabel
|
||||
x={position?.x}
|
||||
y={position?.y}
|
||||
width={size}
|
||||
height={size}
|
||||
class={[
|
||||
'svelte-flow__edgeupdater',
|
||||
`svelte-flow__edgeupdater-${type}`,
|
||||
store.noPanClass,
|
||||
className
|
||||
]}
|
||||
onpointerdown={onPointerDown}
|
||||
transparent
|
||||
{...rest}
|
||||
>
|
||||
{#if !reconnecting && children}
|
||||
{@render children()}
|
||||
{/if}
|
||||
</EdgeLabel>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as EdgeReconnectAnchor } from './EdgeReconnectAnchor.svelte';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { HandleType, XYPosition } from '@xyflow/system';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { ClassValue, HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
export type EdgeReconnectAnchorProps = {
|
||||
type: HandleType;
|
||||
reconnecting?: boolean;
|
||||
style?: string;
|
||||
class?: ClassValue;
|
||||
position?: XYPosition;
|
||||
size?: number;
|
||||
children?: Snippet;
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
@@ -1,115 +1,147 @@
|
||||
<svelte:options immutable />
|
||||
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
|
||||
import { setContext } from 'svelte';
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, setContext } from 'svelte';
|
||||
import cc from 'classcat';
|
||||
import { getMarkerId } from '@xyflow/system';
|
||||
import { elementSelectionKeys, getMarkerId } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import { BezierEdgeInternal } from '$lib/components/edges';
|
||||
import type { EdgeLayouted, Edge } from '$lib/types';
|
||||
import { useHandleEdgeSelect } from '$lib/hooks/useHandleEdgeSelect';
|
||||
|
||||
type $$Props = EdgeLayouted;
|
||||
import type { Node, EdgeLayouted, Edge, EdgeEvents } from '$lib/types';
|
||||
import type { SvelteFlowStore } from '$lib/store/types';
|
||||
import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions';
|
||||
|
||||
export let id: $$Props['id'];
|
||||
export let type: $$Props['type'] = 'default';
|
||||
export let source: $$Props['source'] = '';
|
||||
export let target: $$Props['target'] = '';
|
||||
export let data: $$Props['data'] = {};
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let zIndex: $$Props['zIndex'] = undefined;
|
||||
const {
|
||||
edge,
|
||||
store = $bindable(),
|
||||
onedgeclick,
|
||||
onedgecontextmenu,
|
||||
onedgepointerenter,
|
||||
onedgepointerleave
|
||||
}: {
|
||||
store: SvelteFlowStore<NodeType, EdgeType>;
|
||||
edge: EdgeLayouted<EdgeType>;
|
||||
} & EdgeEvents<EdgeType> = $props();
|
||||
|
||||
export let animated: $$Props['animated'] = false;
|
||||
export let selected: $$Props['selected'] = false;
|
||||
export let selectable: $$Props['selectable'] = undefined;
|
||||
export let deletable: $$Props['deletable'] = undefined;
|
||||
export let hidden: $$Props['hidden'] = false;
|
||||
export let label: $$Props['label'] = undefined;
|
||||
export let labelStyle: $$Props['labelStyle'] = undefined;
|
||||
export let markerStart: $$Props['markerStart'] = undefined;
|
||||
export let markerEnd: $$Props['markerEnd'] = undefined;
|
||||
export let sourceHandle: $$Props['sourceHandle'] = undefined;
|
||||
export let targetHandle: $$Props['targetHandle'] = undefined;
|
||||
export let sourceX: $$Props['sourceX'];
|
||||
export let sourceY: $$Props['sourceY'];
|
||||
export let targetX: $$Props['targetX'];
|
||||
export let targetY: $$Props['targetY'];
|
||||
export let sourcePosition: $$Props['sourcePosition'];
|
||||
export let targetPosition: $$Props['targetPosition'];
|
||||
export let ariaLabel: $$Props['ariaLabel'] = undefined;
|
||||
export let interactionWidth: $$Props['interactionWidth'] = undefined;
|
||||
let {
|
||||
source,
|
||||
target,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
animated = false,
|
||||
selected = false,
|
||||
label,
|
||||
labelStyle,
|
||||
data = {},
|
||||
style,
|
||||
interactionWidth,
|
||||
type = 'default',
|
||||
sourceHandle,
|
||||
targetHandle,
|
||||
markerStart,
|
||||
markerEnd,
|
||||
selectable: _selectable,
|
||||
focusable: _focusable,
|
||||
deletable = true,
|
||||
hidden,
|
||||
zIndex,
|
||||
class: className,
|
||||
ariaLabel
|
||||
} = $derived(edge);
|
||||
|
||||
// @ todo: support edge updates
|
||||
let className: string = '';
|
||||
export { className as class };
|
||||
// svelte-ignore non_reactive_update
|
||||
let edgeRef: SVGGElement | null = null;
|
||||
|
||||
const { id } = edge;
|
||||
setContext('svelteflow__edge_id', id);
|
||||
|
||||
const { edgeLookup, edgeTypes, flowId, elementsSelectable } = useStore();
|
||||
const dispatch = createEventDispatcher<{
|
||||
edgeclick: { edge: Edge; event: MouseEvent | TouchEvent };
|
||||
edgecontextmenu: { edge: Edge; event: MouseEvent };
|
||||
edgemouseenter: { edge: Edge; event: MouseEvent };
|
||||
edgemouseleave: { edge: Edge; event: MouseEvent };
|
||||
}>();
|
||||
let selectable = $derived(_selectable ?? store.elementsSelectable);
|
||||
let focusable = $derived(_focusable ?? store.edgesFocusable);
|
||||
|
||||
$: edgeType = type || 'default';
|
||||
$: edgeComponent = $edgeTypes[edgeType] || BezierEdgeInternal;
|
||||
$: markerStartUrl = markerStart ? `url('#${getMarkerId(markerStart, $flowId)}')` : undefined;
|
||||
$: markerEndUrl = markerEnd ? `url('#${getMarkerId(markerEnd, $flowId)}')` : undefined;
|
||||
$: isSelectable = selectable ?? $elementsSelectable;
|
||||
let EdgeComponent = $derived(store.edgeTypes[type] ?? BezierEdgeInternal);
|
||||
|
||||
const handleEdgeSelect = useHandleEdgeSelect();
|
||||
let markerStartUrl = $derived(
|
||||
markerStart ? `url('#${getMarkerId(markerStart, store.flowId)}')` : undefined
|
||||
);
|
||||
let markerEndUrl = $derived(
|
||||
markerEnd ? `url('#${getMarkerId(markerEnd, store.flowId)}')` : undefined
|
||||
);
|
||||
|
||||
function onClick(event: MouseEvent | TouchEvent) {
|
||||
const edge = $edgeLookup.get(id);
|
||||
function onclick(event: MouseEvent) {
|
||||
const edge = store.edgeLookup.get(id);
|
||||
|
||||
if (edge) {
|
||||
handleEdgeSelect(id);
|
||||
dispatch('edgeclick', { event, edge });
|
||||
if (selectable) store.handleEdgeSelection(id);
|
||||
onedgeclick?.({ event, edge });
|
||||
}
|
||||
}
|
||||
|
||||
type EdgeMouseEvent = 'edgecontextmenu' | 'edgemouseenter' | 'edgemouseleave';
|
||||
function onMouseEvent(event: MouseEvent, type: EdgeMouseEvent) {
|
||||
const edge = $edgeLookup.get(id);
|
||||
function onmouseevent<T = MouseEvent>(
|
||||
event: T,
|
||||
callback: ({ edge, event }: { edge: EdgeType; event: T }) => void
|
||||
) {
|
||||
const edge = store.edgeLookup.get(id);
|
||||
|
||||
if (edge) {
|
||||
dispatch(type, { event, edge });
|
||||
callback({ event, edge });
|
||||
}
|
||||
}
|
||||
|
||||
onkeydown = (event: KeyboardEvent) => {
|
||||
// TODO: Possible Svelte Bug? onkeydown is always firing for the last edge
|
||||
if (!store.disableKeyboardA11y && elementSelectionKeys.includes(event.key) && selectable) {
|
||||
const { unselectNodesAndEdges, addSelectedEdges } = store;
|
||||
const unselect = event.key === 'Escape';
|
||||
|
||||
if (unselect) {
|
||||
edgeRef?.blur();
|
||||
unselectNodesAndEdges({ edges: [edge] });
|
||||
} else {
|
||||
addSelectedEdges([id]);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
{#if !hidden}
|
||||
<svg style:z-index={zIndex}>
|
||||
<svg style:z-index={zIndex} class="svelte-flow__edge-wrapper">
|
||||
<g
|
||||
class={cc(['svelte-flow__edge', className])}
|
||||
bind:this={edgeRef}
|
||||
class={['svelte-flow__edge', className]}
|
||||
class:animated
|
||||
class:selected
|
||||
class:selectable={isSelectable}
|
||||
class:selectable
|
||||
data-id={id}
|
||||
on:click={onClick}
|
||||
on:contextmenu={(e) => {
|
||||
onMouseEvent(e, 'edgecontextmenu');
|
||||
}}
|
||||
on:mouseenter={(e) => {
|
||||
onMouseEvent(e, 'edgemouseenter');
|
||||
}}
|
||||
on:mouseleave={(e) => {
|
||||
onMouseEvent(e, 'edgemouseleave');
|
||||
}}
|
||||
{onclick}
|
||||
oncontextmenu={onedgecontextmenu
|
||||
? (e) => {
|
||||
onmouseevent(e, onedgecontextmenu);
|
||||
}
|
||||
: undefined}
|
||||
onpointerenter={onedgepointerenter
|
||||
? (e) => {
|
||||
onmouseevent(e, onedgepointerenter);
|
||||
}
|
||||
: undefined}
|
||||
onpointerleave={onedgepointerleave
|
||||
? (e) => {
|
||||
onmouseevent(e, onedgepointerleave);
|
||||
}
|
||||
: undefined}
|
||||
aria-label={ariaLabel === null
|
||||
? undefined
|
||||
: ariaLabel
|
||||
? ariaLabel
|
||||
: `Edge from ${source} to ${target}`}
|
||||
role="img"
|
||||
aria-describedby={focusable ? `${ARIA_EDGE_DESC_KEY}-${store.flowId}` : undefined}
|
||||
role={focusable ? 'button' : 'img'}
|
||||
onkeydown={focusable ? onkeydown : undefined}
|
||||
tabindex={focusable ? 0 : undefined}
|
||||
>
|
||||
<svelte:component
|
||||
this={edgeComponent}
|
||||
<EdgeComponent
|
||||
{id}
|
||||
{source}
|
||||
{target}
|
||||
@@ -126,9 +158,9 @@
|
||||
{data}
|
||||
{style}
|
||||
{interactionWidth}
|
||||
selectable={isSelectable}
|
||||
deletable={deletable ?? true}
|
||||
type={edgeType}
|
||||
{selectable}
|
||||
{deletable}
|
||||
{type}
|
||||
sourceHandleId={sourceHandle}
|
||||
targetHandleId={targetHandle}
|
||||
markerStart={markerStartUrl}
|
||||
|
||||
@@ -1,71 +1,113 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import cc from 'classcat';
|
||||
import {
|
||||
Position,
|
||||
XYHandle,
|
||||
isMouseEvent,
|
||||
type HandleConnection,
|
||||
areConnectionMapsEqual,
|
||||
handleConnectionChange,
|
||||
ConnectionMode
|
||||
ConnectionMode,
|
||||
getHostForElement,
|
||||
type HandleConnection,
|
||||
type Optional,
|
||||
type ConnectionState,
|
||||
type Connection
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import type { HandleProps } from '$lib/types';
|
||||
|
||||
type $$Props = HandleProps;
|
||||
import type { ConnectableContext } from '../NodeWrapper/types';
|
||||
import type { HandleProps } from './types';
|
||||
|
||||
export let id: $$Props['id'] = undefined;
|
||||
export let type: $$Props['type'] = 'source';
|
||||
export let position: $$Props['position'] = Position.Top;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let isValidConnection: $$Props['isValidConnection'] = undefined;
|
||||
export let onconnect: $$Props['onconnect'] = undefined;
|
||||
export let ondisconnect: $$Props['ondisconnect'] = undefined;
|
||||
// @todo implement connectablestart, connectableend
|
||||
// export let isConnectableStart: $$Props['isConnectableStart'] = undefined;
|
||||
// export let isConnectableEnd: $$Props['isConnectableEnd'] = undefined;
|
||||
let {
|
||||
id: handleId = null,
|
||||
type = 'source',
|
||||
position = Position.Top,
|
||||
style,
|
||||
class: className,
|
||||
isConnectable: isConnectableProp,
|
||||
isConnectableStart = true,
|
||||
isConnectableEnd = true,
|
||||
isValidConnection,
|
||||
onconnect,
|
||||
ondisconnect,
|
||||
children,
|
||||
...rest
|
||||
}: HandleProps = $props();
|
||||
|
||||
let isConnectableProp: $$Props['isConnectable'] = undefined;
|
||||
export { isConnectableProp as isConnectable };
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
|
||||
$: isTarget = type === 'target';
|
||||
const nodeId = getContext<string>('svelteflow__node_id');
|
||||
const connectable = getContext<Writable<boolean>>('svelteflow__node_connectable');
|
||||
$: isConnectable = isConnectableProp !== undefined ? isConnectableProp : $connectable;
|
||||
const isConnectableContext = getContext<ConnectableContext>('svelteflow__node_connectable');
|
||||
|
||||
$: handleId = id || null;
|
||||
let isTarget = $derived(type === 'target');
|
||||
let isConnectable = $derived(
|
||||
isConnectableProp !== undefined ? isConnectableProp : isConnectableContext.value
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
const {
|
||||
connectionMode,
|
||||
domNode,
|
||||
nodeLookup,
|
||||
connectionRadius,
|
||||
viewport,
|
||||
isValidConnection: isValidConnectionStore,
|
||||
lib,
|
||||
addEdge,
|
||||
onedgecreate,
|
||||
panBy,
|
||||
cancelConnection,
|
||||
updateConnection,
|
||||
autoPanOnConnect,
|
||||
edges,
|
||||
connectionLookup,
|
||||
onconnect: onConnectAction,
|
||||
onconnectstart: onConnectStartAction,
|
||||
onconnectend: onConnectEndAction,
|
||||
flowId,
|
||||
connection
|
||||
} = store;
|
||||
let store = useStore();
|
||||
|
||||
function onPointerDown(event: MouseEvent | TouchEvent) {
|
||||
let prevConnections: Map<string, HandleConnection> | null = null;
|
||||
$effect.pre(() => {
|
||||
if (onconnect || ondisconnect) {
|
||||
// connectionLookup is not reactive, so we use edges to get notified about updates
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
store.edges;
|
||||
let connections = store.connectionLookup.get(
|
||||
`${nodeId}-${type}${handleId ? `-${handleId}` : ''}`
|
||||
);
|
||||
|
||||
if (prevConnections && !areConnectionMapsEqual(connections, prevConnections)) {
|
||||
const _connections = connections ?? new Map();
|
||||
|
||||
handleConnectionChange(prevConnections, _connections, ondisconnect);
|
||||
handleConnectionChange(_connections, prevConnections, onconnect);
|
||||
}
|
||||
|
||||
prevConnections = new Map(connections);
|
||||
}
|
||||
});
|
||||
|
||||
let [connectionInProgress, connectingFrom, connectingTo, isPossibleTargetHandle, valid] =
|
||||
$derived.by(() => {
|
||||
if (!store.connection.inProgress) {
|
||||
return [false, false, false, false, null];
|
||||
}
|
||||
|
||||
const { fromHandle, toHandle, isValid } = store.connection;
|
||||
|
||||
const connectingFrom =
|
||||
fromHandle &&
|
||||
fromHandle.nodeId === nodeId &&
|
||||
fromHandle.type === type &&
|
||||
fromHandle.id === handleId;
|
||||
|
||||
const connectingTo =
|
||||
toHandle &&
|
||||
toHandle.nodeId === nodeId &&
|
||||
toHandle.type === type &&
|
||||
toHandle.id === handleId;
|
||||
|
||||
const isPossibleTargetHandle =
|
||||
store.connectionMode === ConnectionMode.Strict
|
||||
? fromHandle?.type !== type
|
||||
: nodeId !== fromHandle?.nodeId || handleId !== fromHandle?.id;
|
||||
|
||||
const valid = connectingTo && isValid;
|
||||
|
||||
return [true, connectingFrom, connectingTo, isPossibleTargetHandle, valid];
|
||||
});
|
||||
|
||||
function onConnectExtended(connection: Connection) {
|
||||
const edge = store.onbeforeconnect?.(connection) ?? connection;
|
||||
|
||||
if (!edge) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.addEdge(edge);
|
||||
store.onconnect?.(connection);
|
||||
}
|
||||
|
||||
function onpointerdown(event: MouseEvent | TouchEvent) {
|
||||
const isMouseTriggered = isMouseEvent(event);
|
||||
|
||||
if ((isMouseTriggered && event.button === 0) || !isMouseTriggered) {
|
||||
@@ -73,77 +115,82 @@
|
||||
handleId,
|
||||
nodeId,
|
||||
isTarget,
|
||||
connectionRadius: $connectionRadius,
|
||||
domNode: $domNode,
|
||||
nodeLookup: $nodeLookup,
|
||||
connectionMode: $connectionMode,
|
||||
lib: $lib,
|
||||
autoPanOnConnect: $autoPanOnConnect,
|
||||
flowId: $flowId,
|
||||
isValidConnection: isValidConnection ?? $isValidConnectionStore,
|
||||
updateConnection,
|
||||
cancelConnection,
|
||||
panBy,
|
||||
onConnect: (connection) => {
|
||||
const edge = $onedgecreate ? $onedgecreate(connection) : connection;
|
||||
|
||||
if (!edge) {
|
||||
return;
|
||||
}
|
||||
|
||||
addEdge(edge);
|
||||
$onConnectAction?.(connection);
|
||||
},
|
||||
connectionRadius: store.connectionRadius,
|
||||
domNode: store.domNode,
|
||||
nodeLookup: store.nodeLookup,
|
||||
connectionMode: store.connectionMode,
|
||||
lib: 'svelte',
|
||||
autoPanOnConnect: store.autoPanOnConnect,
|
||||
flowId: store.flowId,
|
||||
isValidConnection: isValidConnection ?? store.isValidConnection,
|
||||
updateConnection: store.updateConnection,
|
||||
cancelConnection: store.cancelConnection,
|
||||
panBy: store.panBy,
|
||||
onConnect: onConnectExtended,
|
||||
onConnectStart: (event, startParams) => {
|
||||
$onConnectStartAction?.(event, {
|
||||
store.onconnectstart?.(event, {
|
||||
nodeId: startParams.nodeId,
|
||||
handleId: startParams.handleId,
|
||||
handleType: startParams.handleType
|
||||
});
|
||||
},
|
||||
onConnectEnd: (event, connectionState) => {
|
||||
$onConnectEndAction?.(event, connectionState);
|
||||
store.onconnectend?.(event, connectionState);
|
||||
},
|
||||
getTransform: () => [$viewport.x, $viewport.y, $viewport.zoom],
|
||||
getFromHandle: () => $connection.fromHandle
|
||||
getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom],
|
||||
getFromHandle: () => store.connection.fromHandle
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let prevConnections: Map<string, HandleConnection> | null = null;
|
||||
let connections: Map<string, HandleConnection> | undefined;
|
||||
|
||||
$: if (onconnect || ondisconnect) {
|
||||
// connectionLookup is not reactive, so we use edges to get notified about updates
|
||||
$edges;
|
||||
connections = $connectionLookup.get(`${nodeId}-${type}-${id || null}`);
|
||||
}
|
||||
|
||||
$: {
|
||||
if (prevConnections && !areConnectionMapsEqual(connections, prevConnections)) {
|
||||
const _connections = connections ?? new Map();
|
||||
|
||||
handleConnectionChange(prevConnections, _connections, ondisconnect);
|
||||
handleConnectionChange(_connections, prevConnections, onconnect);
|
||||
function onclick(event: MouseEvent) {
|
||||
if (!nodeId || (!store.clickConnectStartHandle && !isConnectableStart)) {
|
||||
return;
|
||||
}
|
||||
|
||||
prevConnections = connections ?? new Map();
|
||||
}
|
||||
if (!store.clickConnectStartHandle) {
|
||||
store.onclickconnectstart?.(event, { nodeId, handleId, handleType: type });
|
||||
store.clickConnectStartHandle = { nodeId, type, id: handleId };
|
||||
return;
|
||||
}
|
||||
|
||||
$: connectionInProcess = !!$connection.fromHandle;
|
||||
$: connectingFrom =
|
||||
$connection.fromHandle?.nodeId === nodeId &&
|
||||
$connection.fromHandle?.type === type &&
|
||||
$connection.fromHandle?.id === handleId;
|
||||
$: connectingTo =
|
||||
$connection.toHandle?.nodeId === nodeId &&
|
||||
$connection.toHandle?.type === type &&
|
||||
$connection.toHandle?.id === handleId;
|
||||
$: isPossibleEndHandle =
|
||||
$connectionMode === ConnectionMode.Strict
|
||||
? $connection.fromHandle?.type !== type
|
||||
: nodeId !== $connection.fromHandle?.nodeId || handleId !== $connection.fromHandle?.id;
|
||||
$: valid = connectingTo && $connection.isValid;
|
||||
const doc = getHostForElement(event.target);
|
||||
const isValidConnectionHandler = isValidConnection ?? store.isValidConnection;
|
||||
|
||||
const { connectionMode, clickConnectStartHandle, flowId, nodeLookup } = store;
|
||||
const { connection, isValid } = XYHandle.isValid(event, {
|
||||
handle: {
|
||||
nodeId,
|
||||
id: handleId,
|
||||
type
|
||||
},
|
||||
connectionMode,
|
||||
fromNodeId: clickConnectStartHandle.nodeId,
|
||||
fromHandleId: clickConnectStartHandle.id ?? null,
|
||||
fromType: clickConnectStartHandle.type,
|
||||
isValidConnection: isValidConnectionHandler,
|
||||
flowId,
|
||||
doc,
|
||||
lib: 'svelte',
|
||||
nodeLookup
|
||||
});
|
||||
|
||||
if (isValid && connection) {
|
||||
onConnectExtended(connection);
|
||||
}
|
||||
|
||||
const connectionClone = structuredClone($state.snapshot(store.connection)) as Optional<
|
||||
ConnectionState,
|
||||
'inProgress'
|
||||
>;
|
||||
delete connectionClone.inProgress;
|
||||
connectionClone.toPosition = connectionClone.toHandle
|
||||
? connectionClone.toHandle.position
|
||||
: null;
|
||||
store.onclickconnectend?.(event, connectionClone);
|
||||
|
||||
store.clickConnectStartHandle = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<!--
|
||||
@@ -154,29 +201,34 @@ The Handle component is the part of a node that can be used to connect nodes.
|
||||
data-handleid={handleId}
|
||||
data-nodeid={nodeId}
|
||||
data-handlepos={position}
|
||||
data-id="{$flowId}-{nodeId}-{id || null}-{type}"
|
||||
class={cc([
|
||||
data-id="{store.flowId}-{nodeId}-{handleId ?? 'null'}-{type}"
|
||||
class={[
|
||||
'svelte-flow__handle',
|
||||
`svelte-flow__handle-${position}`,
|
||||
'nodrag',
|
||||
'nopan',
|
||||
store.noDragClass,
|
||||
store.noPanClass,
|
||||
position,
|
||||
className
|
||||
])}
|
||||
]}
|
||||
class:valid
|
||||
class:connectingto={connectingTo}
|
||||
class:connectingfrom={connectingFrom}
|
||||
class:source={!isTarget}
|
||||
class:target={isTarget}
|
||||
class:connectablestart={isConnectable}
|
||||
class:connectableend={isConnectable}
|
||||
class:connectablestart={isConnectableStart}
|
||||
class:connectableend={isConnectableEnd}
|
||||
class:connectable={isConnectable}
|
||||
class:connectionindicator={isConnectable && (!connectionInProcess || isPossibleEndHandle)}
|
||||
on:mousedown={onPointerDown}
|
||||
on:touchstart={onPointerDown}
|
||||
class:connectionindicator={isConnectable &&
|
||||
(!connectionInProgress || isPossibleTargetHandle) &&
|
||||
(connectionInProgress || store.clickConnectStartHandle ? isConnectableEnd : isConnectableStart)}
|
||||
onmousedown={onpointerdown}
|
||||
ontouchstart={onpointerdown}
|
||||
onclick={store.clickConnect ? onclick : undefined}
|
||||
onkeypress={() => {}}
|
||||
{style}
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
{...rest}
|
||||
>
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { default as Handle } from './Handle.svelte';
|
||||
export * from './types';
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Connection, HandleProps as HandlePropsSystem } from '@xyflow/system';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { ClassValue, HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
export type HandleProps = HandlePropsSystem & {
|
||||
class?: ClassValue;
|
||||
onconnect?: (connections: Connection[]) => void;
|
||||
ondisconnect?: (connections: Connection[]) => void;
|
||||
children?: Snippet;
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
@@ -1,4 +1,4 @@
|
||||
<script lang="ts">
|
||||
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
|
||||
import {
|
||||
shortcut,
|
||||
type ShortcutEventDetail,
|
||||
@@ -6,26 +6,20 @@
|
||||
} from '@svelte-put/shortcut';
|
||||
import { isInputDOMNode, isMacOs } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import type { KeyHandlerProps } from './types';
|
||||
import type { KeyDefinition, KeyDefinitionObject } from '$lib/types';
|
||||
import type { Node, Edge, KeyDefinition, KeyDefinitionObject } from '$lib/types';
|
||||
import { useSvelteFlow } from '$lib/hooks/useSvelteFlow.svelte';
|
||||
|
||||
type $$Props = KeyHandlerProps;
|
||||
let {
|
||||
store = $bindable(),
|
||||
selectionKey = 'Shift',
|
||||
multiSelectionKey = isMacOs() ? 'Meta' : 'Control',
|
||||
deleteKey = 'Backspace',
|
||||
panActivationKey = ' ',
|
||||
zoomActivationKey = isMacOs() ? 'Meta' : 'Control'
|
||||
}: KeyHandlerProps<NodeType, EdgeType> = $props();
|
||||
|
||||
export let selectionKey: $$Props['selectionKey'] = 'Shift';
|
||||
export let multiSelectionKey: $$Props['multiSelectionKey'] = isMacOs() ? 'Meta' : 'Control';
|
||||
export let deleteKey: $$Props['deleteKey'] = 'Backspace';
|
||||
export let panActivationKey: $$Props['panActivationKey'] = ' ';
|
||||
export let zoomActivationKey: $$Props['zoomActivationKey'] = isMacOs() ? 'Meta' : 'Control';
|
||||
|
||||
const {
|
||||
selectionKeyPressed,
|
||||
multiselectionKeyPressed,
|
||||
deleteKeyPressed,
|
||||
panActivationKeyPressed,
|
||||
zoomActivationKeyPressed,
|
||||
selectionRect
|
||||
} = useStore();
|
||||
let { deleteElements } = useSvelteFlow<NodeType, EdgeType>();
|
||||
|
||||
function isKeyObject(key?: KeyDefinition | null): key is KeyDefinitionObject {
|
||||
return key !== null && typeof key === 'object';
|
||||
@@ -62,12 +56,29 @@
|
||||
}
|
||||
|
||||
function resetKeysAndSelection() {
|
||||
selectionRect.set(null);
|
||||
selectionKeyPressed.set(false);
|
||||
multiselectionKeyPressed.set(false);
|
||||
deleteKeyPressed.set(false);
|
||||
panActivationKeyPressed.set(false);
|
||||
zoomActivationKeyPressed.set(false);
|
||||
store.selectionRect = null;
|
||||
store.selectionKeyPressed = false;
|
||||
store.multiselectionKeyPressed = false;
|
||||
store.deleteKeyPressed = false;
|
||||
store.panActivationKeyPressed = false;
|
||||
store.zoomActivationKeyPressed = false;
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
const selectedNodes = store.nodes.filter((node) => node.selected);
|
||||
const selectedEdges = store.edges.filter((edge) => edge.selected);
|
||||
|
||||
const { deletedNodes, deletedEdges } = await deleteElements({
|
||||
nodes: selectedNodes,
|
||||
edges: selectedEdges
|
||||
});
|
||||
|
||||
if (deletedNodes.length > 0 || deletedEdges.length > 0) {
|
||||
store.ondelete?.({
|
||||
nodes: deletedNodes,
|
||||
edges: deletedEdges
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -75,19 +86,21 @@
|
||||
on:blur={resetKeysAndSelection}
|
||||
on:contextmenu={resetKeysAndSelection}
|
||||
use:shortcut={{
|
||||
trigger: getShortcutTrigger(selectionKey, () => selectionKeyPressed.set(true)),
|
||||
trigger: getShortcutTrigger(selectionKey, () => (store.selectionKeyPressed = true)),
|
||||
type: 'keydown'
|
||||
}}
|
||||
use:shortcut={{
|
||||
trigger: getShortcutTrigger(selectionKey, () => selectionKeyPressed.set(false)),
|
||||
trigger: getShortcutTrigger(selectionKey, () => (store.selectionKeyPressed = false)),
|
||||
type: 'keyup'
|
||||
}}
|
||||
use:shortcut={{
|
||||
trigger: getShortcutTrigger(multiSelectionKey, () => multiselectionKeyPressed.set(true)),
|
||||
trigger: getShortcutTrigger(multiSelectionKey, () => {
|
||||
store.multiselectionKeyPressed = true;
|
||||
}),
|
||||
type: 'keydown'
|
||||
}}
|
||||
use:shortcut={{
|
||||
trigger: getShortcutTrigger(multiSelectionKey, () => multiselectionKeyPressed.set(false)),
|
||||
trigger: getShortcutTrigger(multiSelectionKey, () => (store.multiselectionKeyPressed = false)),
|
||||
type: 'keyup'
|
||||
}}
|
||||
use:shortcut={{
|
||||
@@ -97,29 +110,30 @@
|
||||
detail.originalEvent.metaKey ||
|
||||
detail.originalEvent.shiftKey;
|
||||
if (!isModifierKey && !isInputDOMNode(detail.originalEvent)) {
|
||||
deleteKeyPressed.set(true);
|
||||
store.deleteKeyPressed = true;
|
||||
handleDelete();
|
||||
}
|
||||
}),
|
||||
type: 'keydown'
|
||||
}}
|
||||
use:shortcut={{
|
||||
trigger: getShortcutTrigger(deleteKey, () => deleteKeyPressed.set(false)),
|
||||
trigger: getShortcutTrigger(deleteKey, () => (store.deleteKeyPressed = false)),
|
||||
type: 'keyup'
|
||||
}}
|
||||
use:shortcut={{
|
||||
trigger: getShortcutTrigger(panActivationKey, () => panActivationKeyPressed.set(true)),
|
||||
trigger: getShortcutTrigger(panActivationKey, () => (store.panActivationKeyPressed = true)),
|
||||
type: 'keydown'
|
||||
}}
|
||||
use:shortcut={{
|
||||
trigger: getShortcutTrigger(panActivationKey, () => panActivationKeyPressed.set(false)),
|
||||
trigger: getShortcutTrigger(panActivationKey, () => (store.panActivationKeyPressed = false)),
|
||||
type: 'keyup'
|
||||
}}
|
||||
use:shortcut={{
|
||||
trigger: getShortcutTrigger(zoomActivationKey, () => zoomActivationKeyPressed.set(true)),
|
||||
trigger: getShortcutTrigger(zoomActivationKey, () => (store.zoomActivationKeyPressed = true)),
|
||||
type: 'keydown'
|
||||
}}
|
||||
use:shortcut={{
|
||||
trigger: getShortcutTrigger(zoomActivationKey, () => zoomActivationKeyPressed.set(false)),
|
||||
trigger: getShortcutTrigger(zoomActivationKey, () => (store.zoomActivationKeyPressed = false)),
|
||||
type: 'keyup'
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { KeyDefinition } from '$lib/types';
|
||||
import type { SvelteFlowStore } from '$lib/store/types';
|
||||
import type { Node, Edge, KeyDefinition } from '$lib/types';
|
||||
|
||||
export type KeyHandlerProps = {
|
||||
export type KeyHandlerProps<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
|
||||
store: SvelteFlowStore<NodeType, EdgeType>;
|
||||
selectionKey?: KeyDefinition | KeyDefinition[] | null;
|
||||
multiSelectionKey?: KeyDefinition | KeyDefinition[] | null;
|
||||
deleteKey?: KeyDefinition | KeyDefinition[] | null;
|
||||
|
||||
@@ -1,73 +1,95 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
|
||||
import { getInternalNodesBounds, isNumeric, type Rect } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import { Selection } from '$lib/components/Selection';
|
||||
import drag from '$lib/actions/drag';
|
||||
import type { Node, NodeEventMap } from '$lib/types';
|
||||
|
||||
const store = useStore();
|
||||
const { selectionRectMode, nodes, nodeLookup } = store;
|
||||
import type { NodeSelectionProps } from './types';
|
||||
import { arrowKeyDiffs, toPxString } from '$lib/utils';
|
||||
import type { Node, Edge } from '$lib/types';
|
||||
|
||||
const dispatch = createEventDispatcher<
|
||||
NodeEventMap & {
|
||||
selectioncontextmenu: { nodes: Node[]; event: MouseEvent | TouchEvent };
|
||||
selectionclick: { nodes: Node[]; event: MouseEvent | TouchEvent };
|
||||
let {
|
||||
store = $bindable(),
|
||||
onnodedrag,
|
||||
onnodedragstart,
|
||||
onnodedragstop,
|
||||
onselectionclick,
|
||||
onselectioncontextmenu
|
||||
}: NodeSelectionProps<NodeType, EdgeType> = $props();
|
||||
|
||||
let ref = $state<HTMLDivElement>();
|
||||
|
||||
$effect(() => {
|
||||
if (!store.disableKeyboardA11y) {
|
||||
ref?.focus({
|
||||
preventScroll: true
|
||||
});
|
||||
}
|
||||
>();
|
||||
});
|
||||
|
||||
let bounds: Rect | null = null;
|
||||
let bounds: Rect | null = $derived.by(() => {
|
||||
if (store.selectionRectMode === 'nodes') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
store.nodes;
|
||||
return getInternalNodesBounds(store.nodeLookup, { filter: (node) => !!node.selected });
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
$: if ($selectionRectMode === 'nodes') {
|
||||
bounds = getInternalNodesBounds($nodeLookup, { filter: (node) => !!node.selected });
|
||||
$nodes;
|
||||
function oncontextmenu(event: MouseEvent) {
|
||||
const selectedNodes = store.nodes.filter((n) => n.selected);
|
||||
onselectioncontextmenu?.({ nodes: selectedNodes, event });
|
||||
}
|
||||
|
||||
function onContextMenu(event: MouseEvent | TouchEvent) {
|
||||
const selectedNodes = $nodes.filter((n) => n.selected);
|
||||
dispatch('selectioncontextmenu', { nodes: selectedNodes, event });
|
||||
function onclick(event: MouseEvent) {
|
||||
const selectedNodes = store.nodes.filter((n) => n.selected);
|
||||
onselectionclick?.({ nodes: selectedNodes, event });
|
||||
}
|
||||
|
||||
function onClick(event: MouseEvent | TouchEvent) {
|
||||
const selectedNodes = $nodes.filter((n) => n.selected);
|
||||
dispatch('selectionclick', { nodes: selectedNodes, event });
|
||||
function onkeydown(event: KeyboardEvent) {
|
||||
if (Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)) {
|
||||
event.preventDefault();
|
||||
store.moveSelectedNodes(arrowKeyDiffs[event.key], event.shiftKey ? 4 : 1);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $selectionRectMode === 'nodes' && bounds && isNumeric(bounds.x) && isNumeric(bounds.y)}
|
||||
{#if store.selectionRectMode === 'nodes' && bounds && isNumeric(bounds.x) && isNumeric(bounds.y)}
|
||||
<div
|
||||
class="selection-wrapper nopan"
|
||||
style="width: {bounds.width}px; height: {bounds.height}px; transform: translate({bounds.x}px, {bounds.y}px)"
|
||||
class={['svelte-flow__selection-wrapper', store.noPanClass]}
|
||||
style:width={toPxString(bounds.width)}
|
||||
style:height={toPxString(bounds.height)}
|
||||
style:transform="translate({bounds.x}px, {bounds.y}px)"
|
||||
use:drag={{
|
||||
disabled: false,
|
||||
store,
|
||||
onDrag: (event, _, __, nodes) => {
|
||||
dispatch('nodedrag', { event, targetNode: null, nodes });
|
||||
onnodedrag?.({ event, targetNode: null, nodes: nodes as NodeType[] });
|
||||
},
|
||||
onDragStart: (event, _, __, nodes) => {
|
||||
dispatch('nodedragstart', { event, targetNode: null, nodes });
|
||||
onnodedragstart?.({ event, targetNode: null, nodes: nodes as NodeType[] });
|
||||
},
|
||||
onDragStop: (event, _, __, nodes) => {
|
||||
dispatch('nodedragstop', { event, targetNode: null, nodes });
|
||||
onnodedragstop?.({ event, targetNode: null, nodes: nodes as NodeType[] });
|
||||
}
|
||||
}}
|
||||
on:contextmenu={onContextMenu}
|
||||
on:click={onClick}
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
on:keyup={() => {}}
|
||||
{oncontextmenu}
|
||||
{onclick}
|
||||
role={store.disableKeyboardA11y ? undefined : 'button'}
|
||||
tabIndex={store.disableKeyboardA11y ? undefined : -1}
|
||||
onkeydown={store.disableKeyboardA11y ? undefined : onkeydown}
|
||||
bind:this={ref}
|
||||
>
|
||||
<Selection width="100%" height="100%" x={0} y={0} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.selection-wrapper {
|
||||
.svelte-flow__selection-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 7;
|
||||
z-index: 2000;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { SvelteFlowStore } from '$lib/store/types';
|
||||
import type { Node, Edge, NodeEvents, NodeSelectionEvents } from '$lib/types';
|
||||
|
||||
export type NodeSelectionProps<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
|
||||
store: SvelteFlowStore<NodeType, EdgeType>;
|
||||
} & NodeSelectionEvents<NodeType> &
|
||||
Pick<NodeEvents<NodeType>, 'onnodedrag' | 'onnodedragstart' | 'onnodedragstop'>;
|
||||
@@ -1,134 +1,160 @@
|
||||
<svelte:options immutable />
|
||||
|
||||
<script lang="ts">
|
||||
import { setContext, onDestroy, createEventDispatcher } from 'svelte';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import cc from 'classcat';
|
||||
import { errorMessages, Position } from '@xyflow/system';
|
||||
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
|
||||
import { setContext, onDestroy } from 'svelte';
|
||||
import {
|
||||
elementSelectionKeys,
|
||||
errorMessages,
|
||||
isInputDOMNode,
|
||||
nodeHasDimensions,
|
||||
Position
|
||||
} from '@xyflow/system';
|
||||
|
||||
import drag from '$lib/actions/drag';
|
||||
import { useStore } from '$lib/store';
|
||||
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
|
||||
import type { NodeWrapperProps } from './types';
|
||||
import { getNodeInlineStyleDimensions } from './utils';
|
||||
import type { NodeEventMap } from '$lib/types';
|
||||
|
||||
interface $$Props extends NodeWrapperProps {}
|
||||
import type { ConnectableContext, NodeWrapperProps } from './types';
|
||||
import type { Node, Edge, NodeEvents } from '$lib/types';
|
||||
import { arrowKeyDiffs, toPxString } from '$lib/utils';
|
||||
import { ARIA_NODE_DESC_KEY } from '../A11yDescriptions';
|
||||
|
||||
export let node: $$Props['node'];
|
||||
export let id: $$Props['id'];
|
||||
export let data: $$Props['data'] = {};
|
||||
export let selected: $$Props['selected'] = false;
|
||||
export let draggable: $$Props['draggable'] = undefined;
|
||||
export let selectable: $$Props['selectable'] = undefined;
|
||||
export let connectable: $$Props['connectable'] = true;
|
||||
export let deletable: $$Props['deletable'] = true;
|
||||
export let hidden: $$Props['hidden'] = false;
|
||||
export let dragging: boolean = false;
|
||||
export let resizeObserver: $$Props['resizeObserver'] = null;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let type: $$Props['type'] = 'default';
|
||||
export let isParent: $$Props['isParent'] = false;
|
||||
export let positionX: $$Props['positionX'];
|
||||
export let positionY: $$Props['positionY'];
|
||||
export let sourcePosition: $$Props['sourcePosition'] = undefined;
|
||||
export let targetPosition: $$Props['targetPosition'] = undefined;
|
||||
export let zIndex: $$Props['zIndex'];
|
||||
export let measuredWidth: $$Props['measuredWidth'] = undefined;
|
||||
export let measuredHeight: $$Props['measuredHeight'] = undefined;
|
||||
export let initialWidth: $$Props['initialWidth'] = undefined;
|
||||
export let initialHeight: $$Props['initialHeight'] = undefined;
|
||||
export let width: $$Props['width'] = undefined;
|
||||
export let height: $$Props['height'] = undefined;
|
||||
export let dragHandle: $$Props['dragHandle'] = undefined;
|
||||
export let initialized: $$Props['initialized'] = false;
|
||||
export let parentId: $$Props['parentId'] = undefined;
|
||||
export let nodeClickDistance: $$Props['nodeClickDistance'] = undefined;
|
||||
let {
|
||||
store = $bindable(),
|
||||
node,
|
||||
resizeObserver,
|
||||
nodeClickDistance,
|
||||
onnodeclick,
|
||||
onnodedrag,
|
||||
onnodedragstart,
|
||||
onnodedragstop,
|
||||
onnodepointerenter,
|
||||
onnodepointerleave,
|
||||
onnodepointermove,
|
||||
onnodecontextmenu
|
||||
}: NodeWrapperProps<NodeType, EdgeType> & NodeEvents<NodeType> = $props();
|
||||
|
||||
let className: string = '';
|
||||
export { className as class };
|
||||
|
||||
const store = useStore();
|
||||
const {
|
||||
nodeTypes,
|
||||
nodeDragThreshold,
|
||||
selectNodesOnDrag,
|
||||
handleNodeSelection,
|
||||
updateNodeInternals
|
||||
} = store;
|
||||
|
||||
let nodeRef: HTMLDivElement;
|
||||
let prevNodeRef: HTMLDivElement | null = null;
|
||||
|
||||
const dispatchNodeEvent = createEventDispatcher<NodeEventMap>();
|
||||
const connectableStore = writable(connectable);
|
||||
let prevType: string | undefined = undefined;
|
||||
let prevSourcePosition: Position | undefined = undefined;
|
||||
let prevTargetPosition: Position | undefined = undefined;
|
||||
|
||||
$: nodeType = type || 'default';
|
||||
$: nodeTypeValid = !!$nodeTypes[nodeType];
|
||||
$: nodeComponent = $nodeTypes[nodeType] || DefaultNode;
|
||||
|
||||
$: {
|
||||
if (!nodeTypeValid) {
|
||||
console.warn('003', errorMessages['error003'](type!));
|
||||
}
|
||||
}
|
||||
|
||||
$: inlineStyleDimensions = getNodeInlineStyleDimensions({
|
||||
width,
|
||||
height,
|
||||
let {
|
||||
data = {},
|
||||
selected = false,
|
||||
draggable: _draggable,
|
||||
selectable: _selectable,
|
||||
deletable = true,
|
||||
connectable: _connectable,
|
||||
focusable: _focusable,
|
||||
hidden = false,
|
||||
dragging = false,
|
||||
style = '',
|
||||
class: className,
|
||||
type = 'default',
|
||||
parentId,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
measured: { width: measuredWidth, height: measuredHeight } = { width: 0, height: 0 },
|
||||
initialWidth,
|
||||
initialHeight,
|
||||
measuredWidth,
|
||||
measuredHeight
|
||||
});
|
||||
width,
|
||||
height,
|
||||
dragHandle,
|
||||
internals: {
|
||||
z: zIndex = 0,
|
||||
positionAbsolute: { x: positionX, y: positionY },
|
||||
userNode
|
||||
}
|
||||
} = $derived(node);
|
||||
|
||||
$: {
|
||||
connectableStore.set(!!connectable);
|
||||
let { id } = node;
|
||||
|
||||
let draggable = $derived(_draggable ?? store.nodesDraggable);
|
||||
let selectable = $derived(_selectable ?? store.elementsSelectable);
|
||||
let connectable = $derived(_connectable ?? store.nodesConnectable);
|
||||
let initialized = $derived(nodeHasDimensions(node) && !!node.internals.handleBounds);
|
||||
let focusable = $derived(_focusable ?? store.nodesFocusable);
|
||||
|
||||
function isInParentLookup(id: string) {
|
||||
return store.parentLookup.has(id);
|
||||
}
|
||||
|
||||
$: {
|
||||
let isParent = $derived(isInParentLookup(id));
|
||||
|
||||
let nodeRef: HTMLDivElement | null = $state(null);
|
||||
let prevNodeRef: HTMLDivElement | null = null;
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
let prevType: string | undefined = type;
|
||||
// svelte-ignore state_referenced_locally
|
||||
let prevSourcePosition: Position | undefined = sourcePosition;
|
||||
// svelte-ignore state_referenced_locally
|
||||
let prevTargetPosition: Position | undefined = targetPosition;
|
||||
|
||||
let NodeComponent = $derived(store.nodeTypes[type] ?? DefaultNode);
|
||||
|
||||
let connectableContext: ConnectableContext = {
|
||||
get value() {
|
||||
return connectable;
|
||||
}
|
||||
};
|
||||
setContext('svelteflow__node_connectable', connectableContext);
|
||||
setContext('svelteflow__node_id', id);
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
$effect(() => {
|
||||
const valid = !!store.nodeTypes[type];
|
||||
if (!valid) {
|
||||
console.warn('003', errorMessages['error003'](type!));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let nodeStyle = $derived.by(() => {
|
||||
const w = measuredWidth === undefined ? (width ?? initialWidth) : width;
|
||||
const h = measuredHeight === undefined ? (height ?? initialHeight) : height;
|
||||
|
||||
if (w === undefined && h === undefined && style === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return `${style};${w ? `width:${toPxString(w)};` : ''}${h ? `height:${toPxString(h)};` : ''}`;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
// if type, sourcePosition or targetPosition changes,
|
||||
// we need to re-calculate the handle positions
|
||||
const doUpdate =
|
||||
(prevType && nodeType !== prevType) ||
|
||||
(prevSourcePosition && sourcePosition !== prevSourcePosition) ||
|
||||
(prevTargetPosition && targetPosition !== prevTargetPosition);
|
||||
type !== prevType ||
|
||||
sourcePosition !== prevSourcePosition ||
|
||||
targetPosition !== prevTargetPosition;
|
||||
|
||||
if (doUpdate) {
|
||||
requestAnimationFrame(() =>
|
||||
updateNodeInternals(
|
||||
new Map([
|
||||
[
|
||||
id,
|
||||
{
|
||||
if (doUpdate && nodeRef !== null) {
|
||||
requestAnimationFrame(() => {
|
||||
if (nodeRef !== null) {
|
||||
store.updateNodeInternals(
|
||||
new Map([
|
||||
[
|
||||
id,
|
||||
nodeElement: nodeRef,
|
||||
force: true
|
||||
}
|
||||
]
|
||||
])
|
||||
)
|
||||
);
|
||||
{
|
||||
id,
|
||||
nodeElement: nodeRef,
|
||||
force: true
|
||||
}
|
||||
]
|
||||
])
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
prevType = nodeType;
|
||||
prevType = type;
|
||||
prevSourcePosition = sourcePosition;
|
||||
prevTargetPosition = targetPosition;
|
||||
}
|
||||
});
|
||||
|
||||
setContext('svelteflow__node_id', id);
|
||||
setContext('svelteflow__node_connectable', connectableStore);
|
||||
|
||||
$: {
|
||||
if (resizeObserver && (nodeRef !== prevNodeRef || !initialized)) {
|
||||
$effect(() => {
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
||||
if (resizeObserver && (!initialized || nodeRef !== prevNodeRef)) {
|
||||
prevNodeRef && resizeObserver.unobserve(prevNodeRef);
|
||||
nodeRef && resizeObserver.observe(nodeRef);
|
||||
prevNodeRef = nodeRef;
|
||||
}
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-unused-expressions */
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (prevNodeRef) {
|
||||
@@ -137,42 +163,69 @@
|
||||
});
|
||||
|
||||
function onSelectNodeHandler(event: MouseEvent | TouchEvent) {
|
||||
if (selectable && (!get(selectNodesOnDrag) || !draggable || get(nodeDragThreshold) > 0)) {
|
||||
if (selectable && (!store.selectNodesOnDrag || !draggable || store.nodeDragThreshold > 0)) {
|
||||
// this handler gets called by XYDrag on drag start when selectNodesOnDrag=true
|
||||
// here we only need to call it when selectNodesOnDrag=false
|
||||
handleNodeSelection(id);
|
||||
store.handleNodeSelection(id);
|
||||
}
|
||||
|
||||
dispatchNodeEvent('nodeclick', { node: node.internals.userNode, event });
|
||||
onnodeclick?.({ node: userNode, event });
|
||||
}
|
||||
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (isInputDOMNode(event) || store.disableKeyboardA11y) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (elementSelectionKeys.includes(event.key) && selectable) {
|
||||
const unselect = event.key === 'Escape';
|
||||
|
||||
store.handleNodeSelection(id, unselect, nodeRef);
|
||||
} else if (
|
||||
draggable &&
|
||||
node.selected &&
|
||||
Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)
|
||||
) {
|
||||
// prevent default scrolling behavior on arrow key press when node is moved
|
||||
event.preventDefault();
|
||||
|
||||
store.ariaLiveMessage = `Moved selected node ${event.key
|
||||
.replace('Arrow', '')
|
||||
.toLowerCase()}. New position, x: ${node.internals.positionAbsolute.x}, y: ${node.internals.positionAbsolute.y}`;
|
||||
|
||||
store.moveSelectedNodes(arrowKeyDiffs[event.key], event.shiftKey ? 4 : 1);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
{#if !hidden}
|
||||
<div
|
||||
use:drag={{
|
||||
nodeId: id,
|
||||
isSelectable: selectable,
|
||||
disabled: false,
|
||||
disabled: !draggable,
|
||||
handleSelector: dragHandle,
|
||||
noDragClass: 'nodrag',
|
||||
noDragClass: store.noDragClass,
|
||||
nodeClickDistance,
|
||||
onNodeMouseDown: handleNodeSelection,
|
||||
onNodeMouseDown: store.handleNodeSelection,
|
||||
onDrag: (event, _, targetNode, nodes) => {
|
||||
dispatchNodeEvent('nodedrag', { event, targetNode, nodes });
|
||||
onnodedrag?.({ event, targetNode: targetNode as NodeType, nodes: nodes as NodeType[] });
|
||||
},
|
||||
onDragStart: (event, _, targetNode, nodes) => {
|
||||
dispatchNodeEvent('nodedragstart', { event, targetNode, nodes });
|
||||
onnodedragstart?.({
|
||||
event,
|
||||
targetNode: targetNode as NodeType,
|
||||
nodes: nodes as NodeType[]
|
||||
});
|
||||
},
|
||||
onDragStop: (event, _, targetNode, nodes) => {
|
||||
dispatchNodeEvent('nodedragstop', { event, targetNode, nodes });
|
||||
onnodedragstop?.({ event, targetNode: targetNode as NodeType, nodes: nodes as NodeType[] });
|
||||
},
|
||||
store
|
||||
}}
|
||||
bind:this={nodeRef}
|
||||
data-id={id}
|
||||
class={cc(['svelte-flow__node', `svelte-flow__node-${nodeType}`, className])}
|
||||
class={['svelte-flow__node', `svelte-flow__node-${type}`, className]}
|
||||
class:dragging
|
||||
class:selected
|
||||
class:draggable
|
||||
@@ -183,15 +236,28 @@
|
||||
style:z-index={zIndex}
|
||||
style:transform="translate({positionX}px, {positionY}px)"
|
||||
style:visibility={initialized ? 'visible' : 'hidden'}
|
||||
style="{style ?? ''};{inlineStyleDimensions.width}{inlineStyleDimensions.height}"
|
||||
on:click={onSelectNodeHandler}
|
||||
on:mouseenter={(event) => dispatchNodeEvent('nodemouseenter', { node, event })}
|
||||
on:mouseleave={(event) => dispatchNodeEvent('nodemouseleave', { node, event })}
|
||||
on:mousemove={(event) => dispatchNodeEvent('nodemousemove', { node, event })}
|
||||
on:contextmenu={(event) => dispatchNodeEvent('nodecontextmenu', { node, event })}
|
||||
style={nodeStyle}
|
||||
onclick={onSelectNodeHandler}
|
||||
onpointerenter={onnodepointerenter
|
||||
? (event) => onnodepointerenter({ node: userNode, event })
|
||||
: undefined}
|
||||
onpointerleave={onnodepointerleave
|
||||
? (event) => onnodepointerleave({ node: userNode, event })
|
||||
: undefined}
|
||||
onpointermove={onnodepointermove
|
||||
? (event) => onnodepointermove({ node: userNode, event })
|
||||
: undefined}
|
||||
oncontextmenu={onnodecontextmenu
|
||||
? (event) => onnodecontextmenu({ node: userNode, event })
|
||||
: undefined}
|
||||
onkeydown={focusable ? onKeyDown : undefined}
|
||||
tabIndex={focusable ? 0 : undefined}
|
||||
role={focusable ? 'button' : undefined}
|
||||
aria-describedby={store.disableKeyboardA11y
|
||||
? undefined
|
||||
: `${ARIA_NODE_DESC_KEY}-${store.flowId}`}
|
||||
>
|
||||
<svelte:component
|
||||
this={nodeComponent}
|
||||
<NodeComponent
|
||||
{data}
|
||||
{id}
|
||||
{selected}
|
||||
@@ -204,8 +270,8 @@
|
||||
{draggable}
|
||||
{dragHandle}
|
||||
{parentId}
|
||||
type={nodeType}
|
||||
isConnectable={$connectableStore}
|
||||
{type}
|
||||
isConnectable={connectable}
|
||||
positionAbsoluteX={positionX}
|
||||
positionAbsoluteY={positionY}
|
||||
{width}
|
||||
|
||||
@@ -1,38 +1,13 @@
|
||||
import type { InternalNode, Node } from '$lib/types';
|
||||
import type { SvelteFlowStore } from '$lib/store/types';
|
||||
import type { Node, Edge, InternalNode } from '$lib/types';
|
||||
|
||||
export type NodeWrapperProps = Pick<
|
||||
Node,
|
||||
| 'id'
|
||||
| 'class'
|
||||
| 'connectable'
|
||||
| 'data'
|
||||
| 'draggable'
|
||||
| 'dragging'
|
||||
| 'selected'
|
||||
| 'selectable'
|
||||
| 'deletable'
|
||||
| 'style'
|
||||
| 'type'
|
||||
| 'sourcePosition'
|
||||
| 'targetPosition'
|
||||
| 'dragHandle'
|
||||
| 'hidden'
|
||||
| 'width'
|
||||
| 'height'
|
||||
| 'initialWidth'
|
||||
| 'initialHeight'
|
||||
| 'parentId'
|
||||
> & {
|
||||
measuredWidth?: number;
|
||||
measuredHeight?: number;
|
||||
type: string;
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
'on:nodeclick'?: (event: MouseEvent) => void;
|
||||
export type ConnectableContext = {
|
||||
value: boolean;
|
||||
};
|
||||
|
||||
export type NodeWrapperProps<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
|
||||
node: InternalNode<NodeType>;
|
||||
store: SvelteFlowStore<NodeType, EdgeType>;
|
||||
nodeClickDistance?: number;
|
||||
resizeObserver?: ResizeObserver | null;
|
||||
isParent?: boolean;
|
||||
zIndex: number;
|
||||
node: InternalNode;
|
||||
initialized: boolean;
|
||||
nodeClickDistance?: number;
|
||||
};
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
export function getNodeInlineStyleDimensions({
|
||||
width,
|
||||
height,
|
||||
initialWidth,
|
||||
initialHeight,
|
||||
measuredWidth,
|
||||
measuredHeight
|
||||
}: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
initialWidth?: number;
|
||||
initialHeight?: number;
|
||||
measuredWidth?: number;
|
||||
measuredHeight?: number;
|
||||
}): {
|
||||
width: string | undefined;
|
||||
height: string | undefined;
|
||||
} {
|
||||
if (measuredWidth === undefined && measuredHeight === undefined) {
|
||||
const styleWidth = width ?? initialWidth;
|
||||
const styleHeight = height ?? initialHeight;
|
||||
|
||||
return {
|
||||
width: styleWidth ? `width:${styleWidth}px;` : '',
|
||||
height: styleHeight ? `height:${styleHeight}px;` : ''
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
width: width ? `width:${width}px;` : '',
|
||||
height: height ? `height:${height}px;` : ''
|
||||
};
|
||||
}
|
||||
@@ -1,18 +1,28 @@
|
||||
<script lang="ts">
|
||||
export let x: number | null = 0;
|
||||
export let y: number | null = 0;
|
||||
export let width: number | string | null = 0;
|
||||
export let height: number | string | null = 0;
|
||||
export let isVisible: boolean = true;
|
||||
import { toPxString } from '$lib/utils';
|
||||
|
||||
let {
|
||||
x = 0,
|
||||
y = 0,
|
||||
width = 0,
|
||||
height = 0,
|
||||
isVisible = true
|
||||
}: {
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
isVisible?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
{#if isVisible}
|
||||
<div
|
||||
class="svelte-flow__selection"
|
||||
style:width={typeof width === 'string' ? width : `${width}px`}
|
||||
style:height={typeof height === 'string' ? height : `${height}px`}
|
||||
style:width={typeof width === 'string' ? width : toPxString(width)}
|
||||
style:height={typeof height === 'string' ? height : toPxString(height)}
|
||||
style:transform={`translate(${x}px, ${y}px)`}
|
||||
/>
|
||||
></div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
<script lang="ts">
|
||||
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
|
||||
import { onDestroy, setContext } from 'svelte';
|
||||
|
||||
import { createStore, key } from '$lib/store';
|
||||
import type { SvelteFlowProviderProps } from './types';
|
||||
import type { ProviderContext, SvelteFlowStore } from '$lib/store/types';
|
||||
import type { Node, Edge } from '$lib/types';
|
||||
|
||||
type $$Props = SvelteFlowProviderProps;
|
||||
let { children }: SvelteFlowProviderProps = $props();
|
||||
|
||||
export let initialNodes: $$Props['initialNodes'] = undefined;
|
||||
export let initialEdges: $$Props['initialEdges'] = undefined;
|
||||
export let initialWidth: $$Props['initialWidth'] = undefined;
|
||||
export let initialHeight: $$Props['initialHeight'] = undefined;
|
||||
export let fitView: $$Props['fitView'] = undefined;
|
||||
export let nodeOrigin: $$Props['nodeOrigin'] = undefined;
|
||||
|
||||
const store = createStore({
|
||||
nodes: initialNodes,
|
||||
edges: initialEdges,
|
||||
width: initialWidth,
|
||||
height: initialHeight,
|
||||
nodeOrigin,
|
||||
fitView
|
||||
});
|
||||
let store = $state.raw(
|
||||
createStore<NodeType, EdgeType>({
|
||||
props: {},
|
||||
nodes: [],
|
||||
edges: []
|
||||
})
|
||||
);
|
||||
|
||||
setContext(key, {
|
||||
getStore: () => store
|
||||
});
|
||||
provider: true,
|
||||
getStore() {
|
||||
return store;
|
||||
},
|
||||
setStore: (newStore: SvelteFlowStore<NodeType, EdgeType>) => {
|
||||
store = newStore;
|
||||
}
|
||||
} satisfies ProviderContext<NodeType, EdgeType>);
|
||||
|
||||
onDestroy(() => {
|
||||
store.reset();
|
||||
});
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import type { Edge, Node } from '$lib/types';
|
||||
import type { NodeOrigin } from '@xyflow/system';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
export type SvelteFlowProviderProps = {
|
||||
initialNodes?: Node[];
|
||||
initialEdges?: Edge[];
|
||||
initialWidth?: number;
|
||||
initialHeight?: number;
|
||||
fitView?: boolean;
|
||||
nodeOrigin?: NodeOrigin;
|
||||
children?: Snippet;
|
||||
};
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { useStore } from '$lib/store';
|
||||
import { Selection } from '$lib/components/Selection';
|
||||
|
||||
const { selectionRect, selectionRectMode } = useStore();
|
||||
</script>
|
||||
|
||||
<Selection
|
||||
isVisible={!!($selectionRect && $selectionRectMode === 'user')}
|
||||
width={$selectionRect?.width}
|
||||
height={$selectionRect?.height}
|
||||
x={$selectionRect?.x}
|
||||
y={$selectionRect?.y}
|
||||
/>
|
||||
@@ -1 +0,0 @@
|
||||
export { default as UserSelection } from './UserSelection.svelte';
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import portal from '$lib/actions/portal';
|
||||
import { useStore } from '$lib/store';
|
||||
import { portal } from '$lib/actions/portal';
|
||||
import type { ViewportPortalProps } from './types';
|
||||
|
||||
const { domNode } = useStore();
|
||||
let { target = 'front', children, ...rest }: ViewportPortalProps = $props();
|
||||
</script>
|
||||
|
||||
<div use:portal={{ target: '.svelte-flow__viewport-portal', domNode: $domNode }}>
|
||||
<slot />
|
||||
<div use:portal={`viewport-${target}`} {...rest}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { default as ViewportPortal } from './ViewportPortal.svelte';
|
||||
export * from './types';
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
export type ViewportPortalProps = {
|
||||
target: 'front' | 'back';
|
||||
children?: Snippet;
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import type { BaseEdgeProps } from '../../types';
|
||||
import EdgeLabel from '../EdgeLabel/EdgeLabel.svelte';
|
||||
|
||||
let {
|
||||
id,
|
||||
path,
|
||||
label,
|
||||
labelX,
|
||||
labelY,
|
||||
labelStyle,
|
||||
markerStart,
|
||||
markerEnd,
|
||||
style,
|
||||
interactionWidth = 20,
|
||||
class: className,
|
||||
...rest
|
||||
}: BaseEdgeProps = $props();
|
||||
</script>
|
||||
|
||||
<path
|
||||
{id}
|
||||
d={path}
|
||||
class={['svelte-flow__edge-path', className]}
|
||||
marker-start={markerStart}
|
||||
marker-end={markerEnd}
|
||||
fill="none"
|
||||
{style}
|
||||
/>
|
||||
|
||||
{#if interactionWidth > 0}
|
||||
<path
|
||||
d={path}
|
||||
stroke-opacity={0}
|
||||
stroke-width={interactionWidth}
|
||||
fill="none"
|
||||
class="svelte-flow__edge-interaction"
|
||||
{...rest}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if label}
|
||||
<EdgeLabel x={labelX} y={labelY} style={labelStyle} selectEdgeOnClick>
|
||||
{label}
|
||||
</EdgeLabel>
|
||||
{/if}
|
||||
@@ -1,37 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { getBezierPath } from '@xyflow/system';
|
||||
|
||||
import BaseEdge from './BaseEdge.svelte';
|
||||
import type { BezierEdgeProps } from '$lib/types';
|
||||
import { BaseEdge } from '$lib/components/BaseEdge';
|
||||
|
||||
type $$Props = BezierEdgeProps;
|
||||
|
||||
export let id: $$Props['id'] = undefined;
|
||||
export let label: $$Props['label'] = undefined;
|
||||
export let labelStyle: $$Props['labelStyle'] = undefined;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let markerStart: $$Props['markerStart'] = undefined;
|
||||
export let markerEnd: $$Props['markerEnd'] = undefined;
|
||||
export let pathOptions: $$Props['pathOptions'] = undefined;
|
||||
export let interactionWidth: $$Props['interactionWidth'] = undefined;
|
||||
|
||||
export let sourceX: $$Props['sourceX'];
|
||||
export let sourceY: $$Props['sourceY'];
|
||||
export let sourcePosition: $$Props['sourcePosition'];
|
||||
|
||||
export let targetX: $$Props['targetX'];
|
||||
export let targetY: $$Props['targetY'];
|
||||
export let targetPosition: $$Props['targetPosition'];
|
||||
|
||||
$: [path, labelX, labelY] = getBezierPath({
|
||||
let {
|
||||
id,
|
||||
interactionWidth,
|
||||
label,
|
||||
labelStyle,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
pathOptions,
|
||||
sourcePosition,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
style,
|
||||
targetPosition,
|
||||
curvature: pathOptions?.curvature
|
||||
});
|
||||
targetX,
|
||||
targetY
|
||||
}: BezierEdgeProps = $props();
|
||||
|
||||
let [path, labelX, labelY] = $derived(
|
||||
getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
curvature: pathOptions?.curvature
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<BaseEdge
|
||||
|
||||
@@ -1,37 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { getBezierPath } from '@xyflow/system';
|
||||
|
||||
import BaseEdge from './BaseEdge.svelte';
|
||||
import type { EdgeProps } from '$lib/types';
|
||||
import { BaseEdge } from '$lib/components/BaseEdge';
|
||||
|
||||
type $$Props = EdgeProps;
|
||||
|
||||
export let label: $$Props['label'] = undefined;
|
||||
export let labelStyle: $$Props['labelStyle'] = undefined;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let markerStart: $$Props['markerStart'] = undefined;
|
||||
export let markerEnd: $$Props['markerEnd'] = undefined;
|
||||
export let interactionWidth: $$Props['interactionWidth'] = undefined;
|
||||
|
||||
export let sourceX: $$Props['sourceX'];
|
||||
export let sourceY: $$Props['sourceY'];
|
||||
export let sourcePosition: $$Props['sourcePosition'];
|
||||
|
||||
export let targetX: $$Props['targetX'];
|
||||
export let targetY: $$Props['targetY'];
|
||||
export let targetPosition: $$Props['targetPosition'];
|
||||
|
||||
$: [path, labelX, labelY] = getBezierPath({
|
||||
let {
|
||||
interactionWidth,
|
||||
label,
|
||||
labelStyle,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
sourcePosition,
|
||||
sourceX,
|
||||
sourceY,
|
||||
style,
|
||||
targetPosition,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition
|
||||
});
|
||||
targetY
|
||||
}: EdgeProps = $props();
|
||||
|
||||
// this is a workaround for suppressing the warning about unused props
|
||||
$$restProps;
|
||||
let [path, labelX, labelY] = $derived(
|
||||
getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<BaseEdge
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { getSmoothStepPath } from '@xyflow/system';
|
||||
|
||||
import BaseEdge from './BaseEdge.svelte';
|
||||
import type { SmoothStepEdgeProps } from '$lib/types';
|
||||
import { BaseEdge } from '$lib/components/BaseEdge';
|
||||
|
||||
type $$Props = SmoothStepEdgeProps;
|
||||
|
||||
export let id: $$Props['id'] = undefined;
|
||||
export let label: $$Props['label'] = undefined;
|
||||
export let labelStyle: $$Props['labelStyle'] = undefined;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let markerStart: $$Props['markerStart'] = undefined;
|
||||
export let markerEnd: $$Props['markerEnd'] = undefined;
|
||||
export let pathOptions: $$Props['pathOptions'] = undefined;
|
||||
export let interactionWidth: $$Props['interactionWidth'] = undefined;
|
||||
|
||||
export let sourceX: $$Props['sourceX'];
|
||||
export let sourceY: $$Props['sourceY'];
|
||||
export let sourcePosition: $$Props['sourcePosition'];
|
||||
|
||||
export let targetX: $$Props['targetX'];
|
||||
export let targetY: $$Props['targetY'];
|
||||
export let targetPosition: $$Props['targetPosition'];
|
||||
|
||||
$: [path, labelX, labelY] = getSmoothStepPath({
|
||||
let {
|
||||
id,
|
||||
interactionWidth,
|
||||
label,
|
||||
labelStyle,
|
||||
style,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
pathOptions,
|
||||
sourcePosition,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
borderRadius: pathOptions?.borderRadius,
|
||||
offset: pathOptions?.offset
|
||||
});
|
||||
targetX,
|
||||
targetY
|
||||
}: SmoothStepEdgeProps = $props();
|
||||
|
||||
let [path, labelX, labelY] = $derived(
|
||||
getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
borderRadius: pathOptions?.borderRadius,
|
||||
offset: pathOptions?.offset
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<BaseEdge
|
||||
|
||||
@@ -1,37 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { getSmoothStepPath } from '@xyflow/system';
|
||||
|
||||
import BaseEdge from './BaseEdge.svelte';
|
||||
import type { EdgeProps } from '$lib/types';
|
||||
import { BaseEdge } from '$lib/components/BaseEdge';
|
||||
|
||||
type $$Props = EdgeProps;
|
||||
|
||||
export let label: $$Props['label'] = undefined;
|
||||
export let labelStyle: $$Props['labelStyle'] = undefined;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let markerStart: $$Props['markerStart'] = undefined;
|
||||
export let markerEnd: $$Props['markerEnd'] = undefined;
|
||||
export let interactionWidth: $$Props['interactionWidth'] = undefined;
|
||||
|
||||
export let sourceX: $$Props['sourceX'];
|
||||
export let sourceY: $$Props['sourceY'];
|
||||
export let sourcePosition: $$Props['sourcePosition'];
|
||||
|
||||
export let targetX: $$Props['targetX'];
|
||||
export let targetY: $$Props['targetY'];
|
||||
export let targetPosition: $$Props['targetPosition'];
|
||||
|
||||
$: [path, labelX, labelY] = getSmoothStepPath({
|
||||
let {
|
||||
interactionWidth,
|
||||
label,
|
||||
labelStyle,
|
||||
style,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
sourcePosition,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetPosition,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition
|
||||
});
|
||||
targetY
|
||||
}: EdgeProps = $props();
|
||||
|
||||
// this is a workaround for suppressing the warning about unused props
|
||||
$$restProps;
|
||||
let [path, labelX, labelY] = $derived(
|
||||
getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<BaseEdge
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { getSmoothStepPath } from '@xyflow/system';
|
||||
|
||||
import BaseEdge from './BaseEdge.svelte';
|
||||
import type { StepEdgeProps } from '$lib/types';
|
||||
import { BaseEdge } from '$lib/components/BaseEdge';
|
||||
|
||||
type $$Props = StepEdgeProps;
|
||||
|
||||
export let id: $$Props['id'] = undefined;
|
||||
export let label: $$Props['label'] = undefined;
|
||||
export let labelStyle: $$Props['labelStyle'] = undefined;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let markerStart: $$Props['markerStart'] = undefined;
|
||||
export let markerEnd: $$Props['markerEnd'] = undefined;
|
||||
export let pathOptions: $$Props['pathOptions'] = undefined;
|
||||
export let interactionWidth: $$Props['interactionWidth'] = undefined;
|
||||
|
||||
export let sourceX: $$Props['sourceX'];
|
||||
export let sourceY: $$Props['sourceY'];
|
||||
export let sourcePosition: $$Props['sourcePosition'];
|
||||
|
||||
export let targetX: $$Props['targetX'];
|
||||
export let targetY: $$Props['targetY'];
|
||||
export let targetPosition: $$Props['targetPosition'];
|
||||
|
||||
$: [path, labelX, labelY] = getSmoothStepPath({
|
||||
let {
|
||||
id,
|
||||
label,
|
||||
labelStyle,
|
||||
style,
|
||||
markerStart,
|
||||
markerEnd,
|
||||
pathOptions,
|
||||
interactionWidth,
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
borderRadius: 0,
|
||||
offset: pathOptions?.offset
|
||||
});
|
||||
targetPosition
|
||||
}: StepEdgeProps = $props();
|
||||
|
||||
let [path, labelX, labelY] = $derived(
|
||||
getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
borderRadius: 0,
|
||||
offset: pathOptions?.offset
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<BaseEdge
|
||||
|
||||
@@ -1,38 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { getSmoothStepPath } from '@xyflow/system';
|
||||
|
||||
import BaseEdge from './BaseEdge.svelte';
|
||||
import type { EdgeProps } from '$lib/types';
|
||||
import { BaseEdge } from '$lib/components/BaseEdge';
|
||||
|
||||
type $$Props = EdgeProps;
|
||||
|
||||
export let label: $$Props['label'] = undefined;
|
||||
export let labelStyle: $$Props['labelStyle'] = undefined;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let markerStart: $$Props['markerStart'] = undefined;
|
||||
export let markerEnd: $$Props['markerEnd'] = undefined;
|
||||
export let interactionWidth: $$Props['interactionWidth'] = undefined;
|
||||
|
||||
export let sourceX: $$Props['sourceX'];
|
||||
export let sourceY: $$Props['sourceY'];
|
||||
export let sourcePosition: $$Props['sourcePosition'];
|
||||
|
||||
export let targetX: $$Props['targetX'];
|
||||
export let targetY: $$Props['targetY'];
|
||||
export let targetPosition: $$Props['targetPosition'];
|
||||
|
||||
$: [path, labelX, labelY] = getSmoothStepPath({
|
||||
let {
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
borderRadius: 0
|
||||
});
|
||||
label,
|
||||
labelStyle,
|
||||
markerStart,
|
||||
markerEnd,
|
||||
interactionWidth,
|
||||
style
|
||||
}: EdgeProps = $props();
|
||||
|
||||
// this is a workaround for suppressing the warning about unused props
|
||||
$$restProps;
|
||||
let [path, labelX, labelY] = $derived(
|
||||
getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
borderRadius: 0
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<BaseEdge
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { getStraightPath } from '@xyflow/system';
|
||||
|
||||
import BaseEdge from './BaseEdge.svelte';
|
||||
import type { StraightEdgeProps } from '$lib/types';
|
||||
import { BaseEdge } from '$lib/components/BaseEdge';
|
||||
|
||||
type $$Props = StraightEdgeProps;
|
||||
|
||||
export let id: $$Props['id'] = undefined;
|
||||
export let label: $$Props['label'] = undefined;
|
||||
export let labelStyle: $$Props['labelStyle'] = undefined;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let markerStart: $$Props['markerStart'] = undefined;
|
||||
export let markerEnd: $$Props['markerEnd'] = undefined;
|
||||
export let interactionWidth: $$Props['interactionWidth'] = undefined;
|
||||
|
||||
export let sourceX: $$Props['sourceX'];
|
||||
export let sourceY: $$Props['sourceY'];
|
||||
|
||||
export let targetX: $$Props['targetX'];
|
||||
export let targetY: $$Props['targetY'];
|
||||
|
||||
$: [path, labelX, labelY] = getStraightPath({
|
||||
let {
|
||||
id,
|
||||
label,
|
||||
labelStyle,
|
||||
style,
|
||||
markerStart,
|
||||
markerEnd,
|
||||
interactionWidth,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY
|
||||
});
|
||||
}: StraightEdgeProps = $props();
|
||||
|
||||
let [path, labelX, labelY] = $derived(
|
||||
getStraightPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<BaseEdge
|
||||
|
||||
@@ -1,33 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { getStraightPath } from '@xyflow/system';
|
||||
|
||||
import BaseEdge from './BaseEdge.svelte';
|
||||
import type { EdgeProps } from '$lib/types';
|
||||
import { BaseEdge } from '$lib/components/BaseEdge';
|
||||
|
||||
type $$Props = EdgeProps;
|
||||
|
||||
export let label: $$Props['label'] = undefined;
|
||||
export let labelStyle: $$Props['labelStyle'] = undefined;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let markerStart: $$Props['markerStart'] = undefined;
|
||||
export let markerEnd: $$Props['markerEnd'] = undefined;
|
||||
export let interactionWidth: $$Props['interactionWidth'] = undefined;
|
||||
|
||||
export let sourceX: $$Props['sourceX'];
|
||||
export let sourceY: $$Props['sourceY'];
|
||||
|
||||
export let targetX: $$Props['targetX'];
|
||||
export let targetY: $$Props['targetY'];
|
||||
|
||||
$: [path, labelX, labelY] = getStraightPath({
|
||||
let {
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY
|
||||
});
|
||||
targetY,
|
||||
label,
|
||||
labelStyle,
|
||||
markerStart,
|
||||
markerEnd,
|
||||
interactionWidth,
|
||||
style
|
||||
}: EdgeProps = $props();
|
||||
|
||||
// this is a workaround for suppressing the warning about unused props
|
||||
$$restProps;
|
||||
let [path, labelX, labelY] = $derived(
|
||||
getStraightPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<BaseEdge
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// @todo: how can we prevent this duplication in ...Edge/ ...EdgeInternal?
|
||||
// both are quite similar, it's just about 1-2 props that are different
|
||||
export { default as BezierEdge } from './BezierEdge.svelte';
|
||||
export { default as BezierEdgeInternal } from './BezierEdgeInternal.svelte';
|
||||
export { default as BezierEdgeInternal } from './BezierEdge.svelte';
|
||||
|
||||
export { default as SmoothStepEdge } from './SmoothStepEdge.svelte';
|
||||
export { default as SmoothStepEdgeInternal } from './SmoothStepEdgeInternal.svelte';
|
||||
@@ -15,3 +15,5 @@ export { default as StraightEdgeInternal } from './StraightEdgeInternal.svelte';
|
||||
|
||||
export { default as StepEdge } from './StepEdge.svelte';
|
||||
export { default as StepEdgeInternal } from './StepEdgeInternal.svelte';
|
||||
|
||||
export { default as BaseEdge } from './BaseEdge.svelte';
|
||||
|
||||
@@ -4,16 +4,13 @@
|
||||
import { Handle } from '$lib/components/Handle';
|
||||
import type { NodeProps } from '$lib/types';
|
||||
|
||||
interface $$Props extends NodeProps {}
|
||||
|
||||
export let data: $$Props['data'] = { label: 'Node' };
|
||||
export let targetPosition: $$Props['targetPosition'] = undefined;
|
||||
export let sourcePosition: $$Props['sourcePosition'] = undefined;
|
||||
|
||||
// this is a workaround for suppressing the warning about unused props
|
||||
$$restProps;
|
||||
let {
|
||||
data,
|
||||
targetPosition = Position.Top,
|
||||
sourcePosition = Position.Bottom
|
||||
}: NodeProps = $props();
|
||||
</script>
|
||||
|
||||
<Handle type="target" position={targetPosition ?? Position.Top} />
|
||||
<Handle type="target" position={targetPosition} />
|
||||
{data?.label}
|
||||
<Handle type="source" position={sourcePosition ?? Position.Bottom} />
|
||||
<Handle type="source" position={sourcePosition} />
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { NodeProps } from '$lib/types';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface $$Props extends NodeProps {}
|
||||
|
||||
// this is a workaround for suppressing the warning about unused props
|
||||
$$restProps;
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
let {}: NodeProps = $props();
|
||||
</script>
|
||||
|
||||
@@ -4,14 +4,8 @@
|
||||
|
||||
import { Handle } from '$lib/components/Handle';
|
||||
|
||||
interface $$Props extends NodeProps {}
|
||||
|
||||
export let data: $$Props['data'] = { label: 'Node' };
|
||||
export let sourcePosition: $$Props['sourcePosition'] = undefined;
|
||||
|
||||
// this is a workaround for suppressing the warning about unused props
|
||||
$$restProps;
|
||||
let { data = { label: 'Node' }, sourcePosition = Position.Bottom }: NodeProps = $props();
|
||||
</script>
|
||||
|
||||
{data?.label}
|
||||
<Handle type="source" position={sourcePosition ?? Position.Bottom} />
|
||||
<Handle type="source" position={sourcePosition} />
|
||||
|
||||
@@ -4,14 +4,8 @@
|
||||
|
||||
import { Handle } from '$lib/components/Handle';
|
||||
|
||||
interface $$Props extends NodeProps {}
|
||||
|
||||
export let data: $$Props['data'] = { label: 'Node' };
|
||||
export let targetPosition: $$Props['targetPosition'] = undefined;
|
||||
|
||||
// this is a workaround for suppressing the warning about unused props
|
||||
$$restProps;
|
||||
let { data = { label: 'Node' }, targetPosition = Position.Top }: NodeProps = $props();
|
||||
</script>
|
||||
|
||||
{data?.label}
|
||||
<Handle type="target" position={targetPosition ?? Position.Top} />
|
||||
<Handle type="target" position={targetPosition} />
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
|
||||
import { EdgeWrapper } from '$lib/components/EdgeWrapper';
|
||||
import { CallOnMount } from '$lib/components/CallOnMount';
|
||||
import { MarkerDefinition } from '$lib/container/EdgeRenderer/MarkerDefinition';
|
||||
import { useStore } from '$lib/store';
|
||||
import type { DefaultEdgeOptions } from '$lib/types';
|
||||
import type { Node, Edge, EdgeEvents } from '$lib/types';
|
||||
import type { SvelteFlowStore } from '$lib/store/types';
|
||||
|
||||
export let defaultEdgeOptions: DefaultEdgeOptions | undefined;
|
||||
|
||||
const {
|
||||
visibleEdges,
|
||||
edgesInitialized,
|
||||
edges: { setDefaultOptions },
|
||||
elementsSelectable
|
||||
} = useStore();
|
||||
|
||||
onMount(() => {
|
||||
if (defaultEdgeOptions) setDefaultOptions(defaultEdgeOptions);
|
||||
});
|
||||
let {
|
||||
store = $bindable(),
|
||||
onedgeclick,
|
||||
onedgecontextmenu,
|
||||
onedgepointerenter,
|
||||
onedgepointerleave
|
||||
}: { store: SvelteFlowStore<NodeType, EdgeType> } & EdgeEvents<EdgeType> = $props();
|
||||
</script>
|
||||
|
||||
<div class="svelte-flow__edges">
|
||||
@@ -25,50 +18,14 @@
|
||||
<MarkerDefinition />
|
||||
</svg>
|
||||
|
||||
{#each $visibleEdges as edge (edge.id)}
|
||||
{#each store.visible.edges.values() as edge (edge.id)}
|
||||
<EdgeWrapper
|
||||
id={edge.id}
|
||||
source={edge.source}
|
||||
target={edge.target}
|
||||
data={edge.data}
|
||||
style={edge.style}
|
||||
animated={edge.animated}
|
||||
selected={edge.selected}
|
||||
selectable={edge.selectable ?? $elementsSelectable}
|
||||
deletable={edge.deletable}
|
||||
hidden={edge.hidden}
|
||||
label={edge.label}
|
||||
labelStyle={edge.labelStyle}
|
||||
markerStart={edge.markerStart}
|
||||
markerEnd={edge.markerEnd}
|
||||
sourceHandle={edge.sourceHandle}
|
||||
targetHandle={edge.targetHandle}
|
||||
sourceX={edge.sourceX}
|
||||
sourceY={edge.sourceY}
|
||||
targetX={edge.targetX}
|
||||
targetY={edge.targetY}
|
||||
sourcePosition={edge.sourcePosition}
|
||||
targetPosition={edge.targetPosition}
|
||||
ariaLabel={edge.ariaLabel}
|
||||
interactionWidth={edge.interactionWidth}
|
||||
class={edge.class}
|
||||
type={edge.type || 'default'}
|
||||
zIndex={edge.zIndex}
|
||||
on:edgeclick
|
||||
on:edgecontextmenu
|
||||
on:edgemouseenter
|
||||
on:edgemouseleave
|
||||
bind:store
|
||||
{edge}
|
||||
{onedgeclick}
|
||||
{onedgecontextmenu}
|
||||
{onedgepointerenter}
|
||||
{onedgepointerleave}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
{#if $visibleEdges.length > 0}
|
||||
<CallOnMount
|
||||
onMount={() => {
|
||||
$edgesInitialized = true;
|
||||
}}
|
||||
onDestroy={() => {
|
||||
$edgesInitialized = false;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { type MarkerProps, MarkerType } from '@xyflow/system';
|
||||
|
||||
type $$Props = MarkerProps;
|
||||
|
||||
export let id: $$Props['id'];
|
||||
export let type: $$Props['type'];
|
||||
export let width: $$Props['width'] = 12.5;
|
||||
export let height: $$Props['height'] = 12.5;
|
||||
export let markerUnits: $$Props['markerUnits'] = 'strokeWidth';
|
||||
export let orient: $$Props['orient'] = 'auto-start-reverse';
|
||||
export let color: $$Props['color'] = undefined;
|
||||
export let strokeWidth: $$Props['strokeWidth'] = undefined;
|
||||
let {
|
||||
id,
|
||||
type,
|
||||
width = 12.5,
|
||||
height = 12.5,
|
||||
markerUnits = 'strokeWidth',
|
||||
orient = 'auto-start-reverse',
|
||||
color,
|
||||
strokeWidth
|
||||
}: MarkerProps = $props();
|
||||
</script>
|
||||
|
||||
<marker
|
||||
|
||||
+2
-2
@@ -2,11 +2,11 @@
|
||||
import { useStore } from '$lib/store';
|
||||
import { Marker } from '$lib/container/EdgeRenderer/MarkerDefinition';
|
||||
|
||||
const { markers } = useStore();
|
||||
const store = useStore();
|
||||
</script>
|
||||
|
||||
<defs>
|
||||
{#each $markers as marker (marker.id)}
|
||||
{#each store.markers as marker (marker.id)}
|
||||
<Marker {...marker} />
|
||||
{/each}
|
||||
</defs>
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
<script lang="ts">
|
||||
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
|
||||
import { onDestroy } from 'svelte';
|
||||
import { nodeHasDimensions } from '@xyflow/system';
|
||||
|
||||
import { NodeWrapper } from '$lib/components/NodeWrapper';
|
||||
import { useStore } from '$lib/store';
|
||||
import type { NodeRendererProps } from './types';
|
||||
|
||||
type $$Props = NodeRendererProps;
|
||||
import type { Node, Edge, NodeEvents } from '$lib/types';
|
||||
import type { SvelteFlowStore } from '$lib/store/types';
|
||||
|
||||
export let nodeClickDistance: $$Props['nodeClickDistance'] = 0;
|
||||
|
||||
const {
|
||||
visibleNodes,
|
||||
nodesDraggable,
|
||||
nodesConnectable,
|
||||
elementsSelectable,
|
||||
updateNodeInternals,
|
||||
parentLookup
|
||||
} = useStore();
|
||||
let {
|
||||
store = $bindable(),
|
||||
nodeClickDistance,
|
||||
onnodeclick,
|
||||
onnodecontextmenu,
|
||||
onnodepointerenter,
|
||||
onnodepointermove,
|
||||
onnodepointerleave,
|
||||
onnodedrag,
|
||||
onnodedragstart,
|
||||
onnodedragstop
|
||||
}: {
|
||||
store: SvelteFlowStore<NodeType, EdgeType>;
|
||||
nodeClickDistance?: number;
|
||||
} & NodeEvents<NodeType> = $props();
|
||||
|
||||
const resizeObserver: ResizeObserver | null =
|
||||
typeof ResizeObserver === 'undefined'
|
||||
@@ -35,7 +38,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
updateNodeInternals(updates);
|
||||
store.updateNodeInternals(updates);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -44,62 +47,20 @@
|
||||
</script>
|
||||
|
||||
<div class="svelte-flow__nodes">
|
||||
{#each $visibleNodes as node (node.id)}
|
||||
{#each store.visible.nodes.values() as node (node.id)}
|
||||
<NodeWrapper
|
||||
bind:store
|
||||
{node}
|
||||
id={node.id}
|
||||
data={node.data}
|
||||
selected={!!node.selected}
|
||||
hidden={!!node.hidden}
|
||||
draggable={!!(node.draggable || ($nodesDraggable && typeof node.draggable === 'undefined'))}
|
||||
selectable={!!(
|
||||
node.selectable ||
|
||||
($elementsSelectable && typeof node.selectable === 'undefined')
|
||||
)}
|
||||
connectable={!!(
|
||||
node.connectable ||
|
||||
($nodesConnectable && typeof node.connectable === 'undefined')
|
||||
)}
|
||||
deletable={node.deletable ?? true}
|
||||
positionX={node.internals.positionAbsolute.x}
|
||||
positionY={node.internals.positionAbsolute.y}
|
||||
isParent={$parentLookup.has(node.id)}
|
||||
style={node.style}
|
||||
class={node.class}
|
||||
type={node.type ?? 'default'}
|
||||
sourcePosition={node.sourcePosition}
|
||||
targetPosition={node.targetPosition}
|
||||
dragging={node.dragging}
|
||||
zIndex={node.internals.z ?? 0}
|
||||
dragHandle={node.dragHandle}
|
||||
initialized={nodeHasDimensions(node)}
|
||||
width={node.width}
|
||||
height={node.height}
|
||||
initialWidth={node.initialWidth}
|
||||
initialHeight={node.initialHeight}
|
||||
measuredWidth={node.measured.width}
|
||||
measuredHeight={node.measured.height}
|
||||
parentId={node.parentId}
|
||||
{resizeObserver}
|
||||
{nodeClickDistance}
|
||||
on:nodeclick
|
||||
on:nodemouseenter
|
||||
on:nodemousemove
|
||||
on:nodemouseleave
|
||||
on:nodedrag
|
||||
on:nodedragstart
|
||||
on:nodedragstop
|
||||
on:nodecontextmenu
|
||||
{onnodeclick}
|
||||
{onnodepointerenter}
|
||||
{onnodepointermove}
|
||||
{onnodepointerleave}
|
||||
{onnodedrag}
|
||||
{onnodedragstart}
|
||||
{onnodedragstop}
|
||||
{onnodecontextmenu}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.svelte-flow__nodes {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export type NodeRendererProps = {
|
||||
nodeClickDistance?: number;
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
<script lang="ts" context="module">
|
||||
<script lang="ts" module>
|
||||
export function wrapHandler(
|
||||
handler: (evt: MouseEvent) => void,
|
||||
container: HTMLDivElement
|
||||
@@ -7,94 +7,93 @@
|
||||
if (event.target !== container) {
|
||||
return;
|
||||
}
|
||||
|
||||
handler?.(event);
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleSelected<Item extends Node | Edge>(ids: string[]) {
|
||||
export function toggleSelected<Item extends Node | Edge>(ids: Set<string>) {
|
||||
return (item: Item) => {
|
||||
const isSelected = ids.includes(item.id);
|
||||
const isSelected = ids.has(item.id);
|
||||
|
||||
if (item.selected !== isSelected) {
|
||||
item.selected = isSelected;
|
||||
if (!!item.selected !== isSelected) {
|
||||
return { ...item, selected: isSelected };
|
||||
}
|
||||
|
||||
return item;
|
||||
};
|
||||
}
|
||||
|
||||
function isSetEqual(a: Set<string>, b: Set<string>) {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const item of a) {
|
||||
if (!b.has(item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import {
|
||||
SelectionMode,
|
||||
getEventPosition,
|
||||
getNodesInside,
|
||||
getConnectedEdges
|
||||
} from '@xyflow/system';
|
||||
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
|
||||
import { SelectionMode, getEventPosition, getNodesInside } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import type { Node, Edge, InternalNode } from '$lib/types';
|
||||
import type { Node, Edge } from '$lib/types';
|
||||
import type { PaneProps } from './types';
|
||||
|
||||
type $$Props = PaneProps;
|
||||
|
||||
export let panOnDrag: $$Props['panOnDrag'] = undefined;
|
||||
export let selectionOnDrag: $$Props['selectionOnDrag'] = undefined;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
paneclick: {
|
||||
event: MouseEvent | TouchEvent;
|
||||
};
|
||||
panecontextmenu: {
|
||||
event: MouseEvent;
|
||||
};
|
||||
}>();
|
||||
const {
|
||||
nodes,
|
||||
nodeLookup,
|
||||
edges,
|
||||
viewport,
|
||||
dragging,
|
||||
elementsSelectable,
|
||||
selectionRect,
|
||||
selectionRectMode,
|
||||
selectionKeyPressed,
|
||||
selectionMode,
|
||||
panActivationKeyPressed,
|
||||
unselectNodesAndEdges
|
||||
} = useStore();
|
||||
let {
|
||||
store = $bindable(),
|
||||
panOnDrag = true,
|
||||
selectionOnDrag,
|
||||
onpaneclick,
|
||||
onpanecontextmenu,
|
||||
onselectionstart,
|
||||
onselectionend,
|
||||
children
|
||||
}: PaneProps<NodeType, EdgeType> = $props();
|
||||
|
||||
// svelte-ignore non_reactive_update
|
||||
let container: HTMLDivElement;
|
||||
let containerBounds: DOMRect | null = null;
|
||||
let selectedNodes: InternalNode[] = [];
|
||||
|
||||
$: _panOnDrag = $panActivationKeyPressed || panOnDrag;
|
||||
$: isSelecting =
|
||||
$selectionKeyPressed || $selectionRect || (selectionOnDrag && _panOnDrag !== true);
|
||||
$: hasActiveSelection = $elementsSelectable && (isSelecting || $selectionRectMode === 'user');
|
||||
let selectedNodeIds: Set<string> = new Set();
|
||||
let selectedEdgeIds: Set<string> = new Set();
|
||||
|
||||
let panOnDragActive = $derived(store.panActivationKeyPressed || panOnDrag);
|
||||
let isSelecting = $derived(
|
||||
store.selectionKeyPressed ||
|
||||
store.selectionRect ||
|
||||
(selectionOnDrag && panOnDragActive !== true)
|
||||
);
|
||||
let hasActiveSelection = $derived(
|
||||
store.elementsSelectable && (isSelecting || store.selectionRectMode === 'user')
|
||||
);
|
||||
|
||||
// Used to prevent click events when the user lets go of the selectionKey during a selection
|
||||
let selectionInProgress = false;
|
||||
|
||||
function onClick(event: MouseEvent | TouchEvent) {
|
||||
function onClick(event: MouseEvent) {
|
||||
// We prevent click events when the user let go of the selectionKey during a selection
|
||||
if (selectionInProgress) {
|
||||
// We also prevent click events when a connection is in progress
|
||||
if (selectionInProgress || store.connection.inProgress) {
|
||||
selectionInProgress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch('paneclick', { event });
|
||||
unselectNodesAndEdges();
|
||||
selectionRectMode.set(null);
|
||||
onpaneclick?.({ event });
|
||||
store.unselectNodesAndEdges();
|
||||
store.selectionRectMode = null;
|
||||
}
|
||||
|
||||
// We start the selection process when the user clicks down on the pane
|
||||
function onPointerDown(event: PointerEvent) {
|
||||
containerBounds = container.getBoundingClientRect();
|
||||
containerBounds = container?.getBoundingClientRect();
|
||||
|
||||
if (
|
||||
!elementsSelectable ||
|
||||
!store.elementsSelectable ||
|
||||
!isSelecting ||
|
||||
event.button !== 0 ||
|
||||
event.target !== container ||
|
||||
@@ -103,71 +102,81 @@
|
||||
return;
|
||||
}
|
||||
|
||||
(event.target as Element)?.setPointerCapture?.(event.pointerId);
|
||||
(event.target as Partial<Element> | null)?.setPointerCapture?.(event.pointerId);
|
||||
|
||||
const { x, y } = getEventPosition(event, containerBounds);
|
||||
|
||||
unselectNodesAndEdges();
|
||||
store.unselectNodesAndEdges();
|
||||
|
||||
selectionRect.set({
|
||||
store.selectionRect = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
startX: x,
|
||||
startY: y,
|
||||
x,
|
||||
y
|
||||
});
|
||||
};
|
||||
|
||||
// onSelectionStart?.(event);
|
||||
onselectionstart?.(event);
|
||||
}
|
||||
|
||||
function onPointerMove(event: PointerEvent) {
|
||||
if (!isSelecting || !containerBounds || !$selectionRect) {
|
||||
if (!isSelecting || !containerBounds || !store.selectionRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectionInProgress = true;
|
||||
|
||||
const mousePos = getEventPosition(event, containerBounds);
|
||||
const startX = $selectionRect.startX ?? 0;
|
||||
const startY = $selectionRect.startY ?? 0;
|
||||
const { startX = 0, startY = 0 } = store.selectionRect;
|
||||
|
||||
const nextUserSelectRect = {
|
||||
...$selectionRect,
|
||||
...store.selectionRect,
|
||||
x: mousePos.x < startX ? mousePos.x : startX,
|
||||
y: mousePos.y < startY ? mousePos.y : startY,
|
||||
width: Math.abs(mousePos.x - startX),
|
||||
height: Math.abs(mousePos.y - startY)
|
||||
};
|
||||
const prevSelectedNodeIds = selectedNodes.map((n) => n.id);
|
||||
const prevSelectedEdgeIds = getConnectedEdges(selectedNodes, $edges).map((e) => e.id);
|
||||
|
||||
selectedNodes = getNodesInside(
|
||||
$nodeLookup,
|
||||
nextUserSelectRect,
|
||||
[$viewport.x, $viewport.y, $viewport.zoom],
|
||||
$selectionMode === SelectionMode.Partial,
|
||||
true
|
||||
const prevSelectedNodeIds = selectedNodeIds;
|
||||
const prevSelectedEdgeIds = selectedEdgeIds;
|
||||
|
||||
selectedNodeIds = new Set(
|
||||
getNodesInside(
|
||||
store.nodeLookup,
|
||||
nextUserSelectRect,
|
||||
[store.viewport.x, store.viewport.y, store.viewport.zoom],
|
||||
store.selectionMode === SelectionMode.Partial,
|
||||
true
|
||||
).map((n) => n.id)
|
||||
);
|
||||
const selectedEdgeIds = getConnectedEdges(selectedNodes, $edges).map((e) => e.id);
|
||||
const selectedNodeIds = selectedNodes.map((n) => n.id);
|
||||
|
||||
const edgesSelectable = store.defaultEdgeOptions.selectable ?? true;
|
||||
selectedEdgeIds = new Set();
|
||||
|
||||
// We look for all edges connected to the selected nodes
|
||||
for (const nodeId of selectedNodeIds) {
|
||||
const connections = store.connectionLookup.get(nodeId);
|
||||
if (!connections) continue;
|
||||
for (const { edgeId } of connections.values()) {
|
||||
const edge = store.edgeLookup.get(edgeId);
|
||||
if (edge && (edge.selectable ?? edgesSelectable)) {
|
||||
selectedEdgeIds.add(edgeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this prevents unnecessary updates while updating the selection rectangle
|
||||
if (
|
||||
prevSelectedNodeIds.length !== selectedNodeIds.length ||
|
||||
selectedNodeIds.some((id) => !prevSelectedNodeIds.includes(id))
|
||||
) {
|
||||
nodes.update((nodes) => nodes.map(toggleSelected(selectedNodeIds)));
|
||||
if (!isSetEqual(prevSelectedNodeIds, selectedNodeIds)) {
|
||||
store.nodes = store.nodes.map(toggleSelected(selectedNodeIds));
|
||||
}
|
||||
|
||||
if (
|
||||
prevSelectedEdgeIds.length !== selectedEdgeIds.length ||
|
||||
selectedEdgeIds.some((id) => !prevSelectedEdgeIds.includes(id))
|
||||
) {
|
||||
edges.update((edges) => edges.map(toggleSelected(selectedEdgeIds)));
|
||||
if (!isSetEqual(prevSelectedEdgeIds, selectedEdgeIds)) {
|
||||
store.edges = store.edges.map(toggleSelected(selectedEdgeIds));
|
||||
}
|
||||
|
||||
selectionRectMode.set('user');
|
||||
selectionRect.set(nextUserSelectRect);
|
||||
store.selectionRectMode = 'user';
|
||||
store.selectionRect = nextUserSelectRect;
|
||||
}
|
||||
|
||||
function onPointerUp(event: PointerEvent) {
|
||||
@@ -175,61 +184,51 @@
|
||||
return;
|
||||
}
|
||||
|
||||
(event.target as Element)?.releasePointerCapture?.(event.pointerId);
|
||||
(event.target as Partial<Element> | null)?.releasePointerCapture?.(event.pointerId);
|
||||
|
||||
// We only want to trigger click functions when in selection mode if
|
||||
// the user did not move the mouse.
|
||||
if (!isSelecting && $selectionRectMode === 'user' && event.target === container) {
|
||||
if (!isSelecting && store.selectionRectMode === 'user' && event.target === container) {
|
||||
onClick?.(event);
|
||||
}
|
||||
selectionRect.set(null);
|
||||
store.selectionRect = null;
|
||||
|
||||
if (selectedNodes.length > 0) {
|
||||
$selectionRectMode = 'nodes';
|
||||
if (selectedNodeIds.size > 0) {
|
||||
store.selectionRectMode = 'nodes';
|
||||
}
|
||||
|
||||
// If the user kept holding the selectionKey during the selection,
|
||||
// we need to reset the selectionInProgress, so the next click event is not prevented
|
||||
if ($selectionKeyPressed) {
|
||||
if (store.selectionKeyPressed) {
|
||||
selectionInProgress = false;
|
||||
}
|
||||
|
||||
// onSelectionEnd?.(event);
|
||||
onselectionend?.(event);
|
||||
}
|
||||
|
||||
const onContextMenu = (event: MouseEvent) => {
|
||||
if (Array.isArray(_panOnDrag) && _panOnDrag?.includes(2)) {
|
||||
if (Array.isArray(panOnDragActive) && panOnDragActive.includes(2)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch('panecontextmenu', { event });
|
||||
onpanecontextmenu?.({ event });
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
bind:this={container}
|
||||
class="svelte-flow__pane"
|
||||
class="svelte-flow__pane svelte-flow__container"
|
||||
class:draggable={panOnDrag === true || (Array.isArray(panOnDrag) && panOnDrag.includes(0))}
|
||||
class:dragging={$dragging}
|
||||
class:dragging={store.dragging}
|
||||
class:selection={isSelecting}
|
||||
on:click={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
|
||||
on:pointerdown={hasActiveSelection ? onPointerDown : undefined}
|
||||
on:pointermove={hasActiveSelection ? onPointerMove : undefined}
|
||||
on:pointerup={hasActiveSelection ? onPointerUp : undefined}
|
||||
on:contextmenu={wrapHandler(onContextMenu, container)}
|
||||
onclick={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
|
||||
onpointerdown={hasActiveSelection ? onPointerDown : undefined}
|
||||
onpointermove={hasActiveSelection ? onPointerMove : undefined}
|
||||
onpointerup={hasActiveSelection ? onPointerUp : undefined}
|
||||
oncontextmenu={wrapHandler(onContextMenu, container)}
|
||||
>
|
||||
<slot />
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.svelte-flow__pane {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { default as Pane } from './Pane.svelte';
|
||||
export * from './types';
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
export type PaneProps = {
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { Node, Edge, PaneEvents } from '$lib/types';
|
||||
import type { SvelteFlowStore } from '$lib/store/types';
|
||||
|
||||
export type PaneProps<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
|
||||
store: SvelteFlowStore<NodeType, EdgeType>;
|
||||
panOnDrag?: boolean | number[];
|
||||
selectionOnDrag?: boolean;
|
||||
};
|
||||
onselectionstart?: (event: PointerEvent) => void;
|
||||
onselectionend?: (event: PointerEvent) => void;
|
||||
children: Snippet;
|
||||
} & PaneEvents;
|
||||
|
||||
@@ -1,26 +1,19 @@
|
||||
<script lang="ts">
|
||||
import cc from 'classcat';
|
||||
import type { PanelProps } from './types';
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
type $$Props = PanelProps;
|
||||
let { position = 'top-right', style, class: className, children, ...rest }: PanelProps = $props();
|
||||
|
||||
export let position: $$Props['position'] = 'top-right';
|
||||
export let style: $$Props['style'] = undefined;
|
||||
let store = $derived(useStore());
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
|
||||
const { selectionRectMode } = useStore();
|
||||
|
||||
$: positionClasses = `${position}`.split('-');
|
||||
let positionClasses = $derived(`${position}`.split('-'));
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cc(['svelte-flow__panel', className, ...positionClasses])}
|
||||
class={['svelte-flow__panel', className, ...positionClasses]}
|
||||
{style}
|
||||
style:pointer-events={$selectionRectMode ? 'none' : ''}
|
||||
{...$$restProps}
|
||||
style:pointer-events={store.selectionRectMode ? 'none' : ''}
|
||||
{...rest}
|
||||
>
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { default as Panel } from './Panel.svelte';
|
||||
export type { PanelProps } from './types';
|
||||
export * from './types';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PanelPosition } from '@xyflow/system';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import type { ClassValue, HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
export type PanelProps = HTMLAttributes<HTMLDivElement> & {
|
||||
'data-testid'?: string;
|
||||
@@ -9,5 +9,5 @@ export type PanelProps = HTMLAttributes<HTMLDivElement> & {
|
||||
*/
|
||||
position?: PanelPosition;
|
||||
style?: string;
|
||||
class?: string;
|
||||
class?: ClassValue;
|
||||
};
|
||||
|
||||
@@ -1,218 +1,143 @@
|
||||
<script lang="ts">
|
||||
import { onMount, hasContext } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import cc from 'classcat';
|
||||
import { ConnectionMode, PanOnScrollMode } from '@xyflow/system';
|
||||
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
|
||||
import type { Edge, Node } from '$lib/types';
|
||||
import { getContext, setContext, onDestroy, untrack } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { ConnectionLineType, PanOnScrollMode } from '@xyflow/system';
|
||||
|
||||
import { key, createStore } from '$lib/store';
|
||||
import { Zoom } from '$lib/container/Zoom';
|
||||
import { Pane } from '$lib/container/Pane';
|
||||
import { Viewport as ViewportComponent } from '$lib/container/Viewport';
|
||||
import { NodeRenderer } from '$lib/container/NodeRenderer';
|
||||
import { EdgeRenderer } from '$lib/container/EdgeRenderer';
|
||||
import { UserSelection } from '$lib/components/UserSelection';
|
||||
import { NodeSelection } from '$lib/components/NodeSelection';
|
||||
import { Selection } from '$lib/components/Selection';
|
||||
import { KeyHandler } from '$lib/components/KeyHandler';
|
||||
import { ConnectionLine } from '$lib/components/ConnectionLine';
|
||||
import { Attribution } from '$lib/components/Attribution';
|
||||
import { key, useStore, createStoreContext } from '$lib/store';
|
||||
import type { SvelteFlowProps } from './types';
|
||||
import { updateStore, updateStoreByKeys, type UpdatableStoreProps } from './utils';
|
||||
import { useColorModeClass } from '$lib/hooks/useColorModeClass';
|
||||
import { type ProviderContext, type StoreContext } from '$lib/store/types';
|
||||
import Wrapper from './Wrapper.svelte';
|
||||
import { A11yDescriptions } from '$lib/components/A11yDescriptions';
|
||||
|
||||
type $$Props = SvelteFlowProps;
|
||||
let {
|
||||
width,
|
||||
height,
|
||||
proOptions,
|
||||
selectionKey,
|
||||
deleteKey,
|
||||
panActivationKey,
|
||||
multiSelectionKey,
|
||||
zoomActivationKey,
|
||||
paneClickDistance = 1,
|
||||
nodeClickDistance = 1,
|
||||
onmovestart,
|
||||
onmoveend,
|
||||
onmove,
|
||||
oninit,
|
||||
onnodeclick,
|
||||
onnodecontextmenu,
|
||||
onnodedrag,
|
||||
onnodedragstart,
|
||||
onnodedragstop,
|
||||
onnodepointerenter,
|
||||
onnodepointermove,
|
||||
onnodepointerleave,
|
||||
onselectionclick,
|
||||
onselectioncontextmenu,
|
||||
onselectionstart,
|
||||
onselectionend,
|
||||
onedgeclick,
|
||||
onedgecontextmenu,
|
||||
onedgepointerenter,
|
||||
onedgepointerleave,
|
||||
onpaneclick,
|
||||
onpanecontextmenu,
|
||||
panOnScrollMode = PanOnScrollMode.Free,
|
||||
preventScrolling = true,
|
||||
zoomOnScroll = true,
|
||||
zoomOnDoubleClick = true,
|
||||
zoomOnPinch = true,
|
||||
panOnScroll = false,
|
||||
panOnDrag = true,
|
||||
selectionOnDrag = true,
|
||||
connectionLineComponent,
|
||||
connectionLineStyle,
|
||||
connectionLineContainerStyle,
|
||||
connectionLineType = ConnectionLineType.Bezier,
|
||||
attributionPosition,
|
||||
children,
|
||||
nodes = $bindable([]),
|
||||
edges = $bindable([]),
|
||||
viewport = $bindable(undefined),
|
||||
...props
|
||||
}: SvelteFlowProps<NodeType, EdgeType> &
|
||||
Omit<HTMLAttributes<HTMLDivElement>, 'onselectionchange'> = $props();
|
||||
|
||||
export let id: $$Props['id'] = '1';
|
||||
export let nodes: $$Props['nodes'];
|
||||
export let edges: $$Props['edges'];
|
||||
export let fitView: $$Props['fitView'] = undefined;
|
||||
export let fitViewOptions: $$Props['fitViewOptions'] = undefined;
|
||||
export let minZoom: $$Props['minZoom'] = undefined;
|
||||
export let maxZoom: $$Props['maxZoom'] = undefined;
|
||||
export let initialViewport: $$Props['initialViewport'] = undefined;
|
||||
export let viewport: $$Props['viewport'] = undefined;
|
||||
export let nodeTypes: $$Props['nodeTypes'] = undefined;
|
||||
export let edgeTypes: $$Props['edgeTypes'] = undefined;
|
||||
export let selectionKey: $$Props['selectionKey'] = undefined;
|
||||
export let selectionMode: $$Props['selectionMode'] = undefined;
|
||||
export let panActivationKey: $$Props['panActivationKey'] = undefined;
|
||||
export let multiSelectionKey: $$Props['multiSelectionKey'] = undefined;
|
||||
export let zoomActivationKey: $$Props['zoomActivationKey'] = undefined;
|
||||
export let nodesDraggable: $$Props['nodesDraggable'] = undefined;
|
||||
export let nodesConnectable: $$Props['nodesConnectable'] = undefined;
|
||||
export let nodeDragThreshold: $$Props['nodeDragThreshold'] = undefined;
|
||||
export let elementsSelectable: $$Props['elementsSelectable'] = undefined;
|
||||
export let snapGrid: $$Props['snapGrid'] = undefined;
|
||||
export let deleteKey: $$Props['deleteKey'] = undefined;
|
||||
export let connectionRadius: $$Props['connectionRadius'] = undefined;
|
||||
export let connectionLineType: $$Props['connectionLineType'] = undefined;
|
||||
export let connectionMode: $$Props['connectionMode'] = ConnectionMode.Strict;
|
||||
export let connectionLineStyle: $$Props['connectionLineStyle'] = '';
|
||||
export let connectionLineContainerStyle: $$Props['connectionLineContainerStyle'] = '';
|
||||
export let onMoveStart: $$Props['onMoveStart'] = undefined;
|
||||
export let onMove: $$Props['onMove'] = undefined;
|
||||
export let onMoveEnd: $$Props['onMoveEnd'] = undefined;
|
||||
export let isValidConnection: $$Props['isValidConnection'] = undefined;
|
||||
export let translateExtent: $$Props['translateExtent'] = undefined;
|
||||
export let nodeExtent: $$Props['nodeExtent'] = undefined;
|
||||
export let onlyRenderVisibleElements: $$Props['onlyRenderVisibleElements'] = undefined;
|
||||
export let panOnScrollMode: $$Props['panOnScrollMode'] = PanOnScrollMode.Free;
|
||||
export let preventScrolling: $$Props['preventScrolling'] = true;
|
||||
export let zoomOnScroll: $$Props['zoomOnScroll'] = true;
|
||||
export let zoomOnDoubleClick: $$Props['zoomOnDoubleClick'] = true;
|
||||
export let zoomOnPinch: $$Props['zoomOnPinch'] = true;
|
||||
export let panOnScroll: $$Props['panOnScroll'] = false;
|
||||
export let panOnDrag: $$Props['panOnDrag'] = true;
|
||||
export let selectionOnDrag: $$Props['selectionOnDrag'] = undefined;
|
||||
export let autoPanOnConnect: $$Props['autoPanOnConnect'] = true;
|
||||
export let autoPanOnNodeDrag: $$Props['autoPanOnNodeDrag'] = true;
|
||||
export let onerror: $$Props['onerror'] = undefined;
|
||||
export let ondelete: $$Props['ondelete'] = undefined;
|
||||
export let onedgecreate: $$Props['onedgecreate'] = undefined;
|
||||
export let attributionPosition: $$Props['attributionPosition'] = undefined;
|
||||
export let proOptions: $$Props['proOptions'] = undefined;
|
||||
export let defaultEdgeOptions: $$Props['defaultEdgeOptions'] = undefined;
|
||||
export let width: $$Props['width'] = undefined;
|
||||
export let height: $$Props['height'] = undefined;
|
||||
export let colorMode: $$Props['colorMode'] = 'light';
|
||||
export let onconnect: $$Props['onconnect'] = undefined;
|
||||
export let onconnectstart: $$Props['onconnectstart'] = undefined;
|
||||
export let onconnectend: $$Props['onconnectend'] = undefined;
|
||||
export let onbeforedelete: $$Props['onbeforedelete'] = undefined;
|
||||
export let oninit: $$Props['oninit'] = undefined;
|
||||
export let nodeOrigin: $$Props['nodeOrigin'] = undefined;
|
||||
export let paneClickDistance: $$Props['paneClickDistance'] = 0;
|
||||
export let nodeClickDistance: $$Props['nodeClickDistance'] = 0;
|
||||
|
||||
export let defaultMarkerColor = '#b1b1b7';
|
||||
|
||||
export let style: $$Props['style'] = undefined;
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
|
||||
let domNode: HTMLDivElement;
|
||||
let clientWidth: number;
|
||||
let clientHeight: number;
|
||||
|
||||
const initViewport = $viewport || initialViewport;
|
||||
|
||||
const store = hasContext(key)
|
||||
? useStore()
|
||||
: createStoreContext({
|
||||
nodes: get(nodes),
|
||||
edges: get(edges),
|
||||
width,
|
||||
height,
|
||||
fitView,
|
||||
nodeOrigin,
|
||||
nodeExtent
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
store.width.set(clientWidth);
|
||||
store.height.set(clientHeight);
|
||||
store.domNode.set(domNode);
|
||||
|
||||
store.syncNodeStores(nodes);
|
||||
store.syncEdgeStores(edges);
|
||||
store.syncViewport(viewport);
|
||||
|
||||
if (fitView !== undefined) {
|
||||
store.fitViewOnInit.set(fitView);
|
||||
// svelte-ignore non_reactive_update
|
||||
let store = createStore<NodeType, EdgeType>({
|
||||
props,
|
||||
width,
|
||||
height,
|
||||
get nodes() {
|
||||
return nodes;
|
||||
},
|
||||
set nodes(newNodes) {
|
||||
nodes = newNodes;
|
||||
},
|
||||
get edges() {
|
||||
return edges;
|
||||
},
|
||||
set edges(newEdges) {
|
||||
edges = newEdges;
|
||||
},
|
||||
get viewport() {
|
||||
return viewport;
|
||||
},
|
||||
set viewport(newViewport) {
|
||||
viewport = newViewport;
|
||||
}
|
||||
|
||||
if (fitViewOptions) {
|
||||
store.fitViewOptions.set(fitViewOptions);
|
||||
}
|
||||
|
||||
updateStore(store, {
|
||||
nodeTypes,
|
||||
edgeTypes,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
translateExtent,
|
||||
paneClickDistance
|
||||
});
|
||||
|
||||
return () => {
|
||||
store.reset();
|
||||
};
|
||||
});
|
||||
|
||||
// Update width & height on resize
|
||||
$: {
|
||||
if (clientWidth !== undefined && clientHeight !== undefined) {
|
||||
store.width.set(clientWidth);
|
||||
store.height.set(clientHeight);
|
||||
// Set store for provider context
|
||||
const providerContext = getContext<ProviderContext<NodeType, EdgeType>>(key);
|
||||
if (providerContext && providerContext.setStore) {
|
||||
providerContext.setStore(store);
|
||||
}
|
||||
|
||||
// Overwrite store context to give children direct access
|
||||
setContext(key, {
|
||||
provider: false,
|
||||
getStore() {
|
||||
return store;
|
||||
}
|
||||
}
|
||||
} satisfies StoreContext<NodeType, EdgeType>);
|
||||
|
||||
// Call oninit once when flow is intialized
|
||||
const { initialized } = store;
|
||||
let onInitCalled = false;
|
||||
$: {
|
||||
if (!onInitCalled && $initialized) {
|
||||
oninit?.();
|
||||
onInitCalled = true;
|
||||
// handle selection change
|
||||
$effect(() => {
|
||||
const params = { nodes: store.selectedNodes, edges: store.selectedEdges };
|
||||
untrack(() => props.onselectionchange)?.(params);
|
||||
for (const handler of store.selectionChangeHandlers.values()) {
|
||||
handler(params);
|
||||
}
|
||||
}
|
||||
|
||||
// this updates the store for simple changes
|
||||
// where the prop names equals the store name
|
||||
$: {
|
||||
const updatableProps: UpdatableStoreProps = {
|
||||
flowId: id,
|
||||
connectionLineType,
|
||||
connectionRadius,
|
||||
selectionMode,
|
||||
snapGrid,
|
||||
defaultMarkerColor,
|
||||
nodesDraggable,
|
||||
nodesConnectable,
|
||||
elementsSelectable,
|
||||
onlyRenderVisibleElements,
|
||||
isValidConnection,
|
||||
autoPanOnConnect,
|
||||
autoPanOnNodeDrag,
|
||||
onerror,
|
||||
ondelete,
|
||||
onedgecreate,
|
||||
connectionMode,
|
||||
nodeDragThreshold,
|
||||
onconnect,
|
||||
onconnectstart,
|
||||
onconnectend,
|
||||
onbeforedelete,
|
||||
nodeOrigin
|
||||
};
|
||||
|
||||
updateStoreByKeys(store, updatableProps);
|
||||
}
|
||||
|
||||
$: updateStore(store, {
|
||||
nodeTypes,
|
||||
edgeTypes,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
translateExtent,
|
||||
paneClickDistance
|
||||
});
|
||||
|
||||
$: colorModeClass = useColorModeClass(colorMode);
|
||||
onDestroy(() => {
|
||||
store.reset();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={domNode}
|
||||
bind:clientWidth
|
||||
bind:clientHeight
|
||||
{style}
|
||||
class={cc(['svelte-flow', className, $colorModeClass])}
|
||||
data-testid="svelte-flow__wrapper"
|
||||
on:dragover
|
||||
on:drop
|
||||
{...$$restProps}
|
||||
role="application"
|
||||
<Wrapper
|
||||
bind:domNode={store.domNode}
|
||||
bind:clientWidth={store.width}
|
||||
bind:clientHeight={store.height}
|
||||
colorMode={store.colorMode}
|
||||
{width}
|
||||
{height}
|
||||
rest={props}
|
||||
>
|
||||
<KeyHandler
|
||||
bind:store
|
||||
{selectionKey}
|
||||
{deleteKey}
|
||||
{panActivationKey}
|
||||
@@ -220,91 +145,78 @@
|
||||
{zoomActivationKey}
|
||||
/>
|
||||
<Zoom
|
||||
initialViewport={initViewport}
|
||||
{onMoveStart}
|
||||
{onMove}
|
||||
{onMoveEnd}
|
||||
panOnScrollMode={panOnScrollMode === undefined ? PanOnScrollMode.Free : panOnScrollMode}
|
||||
preventScrolling={preventScrolling === undefined ? true : preventScrolling}
|
||||
zoomOnScroll={zoomOnScroll === undefined ? true : zoomOnScroll}
|
||||
zoomOnDoubleClick={zoomOnDoubleClick === undefined ? true : zoomOnDoubleClick}
|
||||
zoomOnPinch={zoomOnPinch === undefined ? true : zoomOnPinch}
|
||||
panOnScroll={panOnScroll === undefined ? false : panOnScroll}
|
||||
panOnDrag={panOnDrag === undefined ? true : panOnDrag}
|
||||
paneClickDistance={paneClickDistance === undefined ? 0 : paneClickDistance}
|
||||
bind:store
|
||||
{panOnScrollMode}
|
||||
{preventScrolling}
|
||||
{zoomOnScroll}
|
||||
{zoomOnDoubleClick}
|
||||
{zoomOnPinch}
|
||||
{panOnScroll}
|
||||
{panOnDrag}
|
||||
{paneClickDistance}
|
||||
{onmovestart}
|
||||
{onmove}
|
||||
{onmoveend}
|
||||
{oninit}
|
||||
>
|
||||
<Pane
|
||||
on:paneclick
|
||||
on:panecontextmenu
|
||||
panOnDrag={panOnDrag === undefined ? true : panOnDrag}
|
||||
bind:store
|
||||
{onpaneclick}
|
||||
{onpanecontextmenu}
|
||||
{onselectionstart}
|
||||
{onselectionend}
|
||||
{panOnDrag}
|
||||
{selectionOnDrag}
|
||||
>
|
||||
<ViewportComponent>
|
||||
<ViewportComponent bind:store>
|
||||
<div class="svelte-flow__viewport-back svelte-flow__container"></div>
|
||||
<EdgeRenderer
|
||||
on:edgeclick
|
||||
on:edgecontextmenu
|
||||
on:edgemouseenter
|
||||
on:edgemouseleave
|
||||
{defaultEdgeOptions}
|
||||
bind:store
|
||||
{onedgeclick}
|
||||
{onedgecontextmenu}
|
||||
{onedgepointerenter}
|
||||
{onedgepointerleave}
|
||||
/>
|
||||
<div class="svelte-flow__edge-labels svelte-flow__container"></div>
|
||||
<ConnectionLine
|
||||
bind:store
|
||||
type={connectionLineType}
|
||||
LineComponent={connectionLineComponent}
|
||||
containerStyle={connectionLineContainerStyle}
|
||||
style={connectionLineStyle}
|
||||
isCustomComponent={$$slots.connectionLine}
|
||||
>
|
||||
<slot name="connectionLine" slot="connectionLine" />
|
||||
</ConnectionLine>
|
||||
<div class="svelte-flow__edgelabel-renderer" />
|
||||
<div class="svelte-flow__viewport-portal" />
|
||||
/>
|
||||
<NodeRenderer
|
||||
bind:store
|
||||
{nodeClickDistance}
|
||||
on:nodeclick
|
||||
on:nodemouseenter
|
||||
on:nodemousemove
|
||||
on:nodemouseleave
|
||||
on:nodedragstart
|
||||
on:nodedrag
|
||||
on:nodedragstop
|
||||
on:nodecontextmenu
|
||||
{onnodeclick}
|
||||
{onnodecontextmenu}
|
||||
{onnodepointerenter}
|
||||
{onnodepointermove}
|
||||
{onnodepointerleave}
|
||||
{onnodedrag}
|
||||
{onnodedragstart}
|
||||
{onnodedragstop}
|
||||
/>
|
||||
<NodeSelection
|
||||
on:selectionclick
|
||||
on:selectioncontextmenu
|
||||
on:nodedragstart
|
||||
on:nodedrag
|
||||
on:nodedragstop
|
||||
bind:store
|
||||
{onselectionclick}
|
||||
{onselectioncontextmenu}
|
||||
{onnodedrag}
|
||||
{onnodedragstart}
|
||||
{onnodedragstop}
|
||||
/>
|
||||
<div class="svelte-flow__viewport-front svelte-flow__container"></div>
|
||||
</ViewportComponent>
|
||||
<UserSelection />
|
||||
<Selection
|
||||
isVisible={!!(store.selectionRect && store.selectionRectMode === 'user')}
|
||||
width={store.selectionRect?.width}
|
||||
height={store.selectionRect?.height}
|
||||
x={store.selectionRect?.x}
|
||||
y={store.selectionRect?.y}
|
||||
/>
|
||||
</Pane>
|
||||
</Zoom>
|
||||
<Attribution {proOptions} position={attributionPosition} />
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.svelte-flow {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
|
||||
background-color: var(--background-color, var(--background-color-default));
|
||||
}
|
||||
|
||||
:root {
|
||||
--background-color-default: #fff;
|
||||
--background-pattern-color-default: #ddd;
|
||||
|
||||
--minimap-mask-color-default: rgb(240, 240, 240, 0.6);
|
||||
--minimap-mask-stroke-color-default: none;
|
||||
--minimap-mask-stroke-width-default: 1;
|
||||
|
||||
--controls-button-background-color-default: #fefefe;
|
||||
--controls-button-background-color-hover-default: #f4f4f4;
|
||||
--controls-button-color-default: inherit;
|
||||
--controls-button-color-hover-default: inherit;
|
||||
--controls-button-border-color-default: #eee;
|
||||
}
|
||||
</style>
|
||||
<A11yDescriptions {store} />
|
||||
{@render children?.()}
|
||||
</Wrapper>
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { type SvelteFlowRestProps } from '$lib/store/types';
|
||||
import { toPxString } from '$lib/utils';
|
||||
import type { Node, Edge } from '$lib/types';
|
||||
|
||||
let {
|
||||
width,
|
||||
height,
|
||||
colorMode,
|
||||
domNode = $bindable(),
|
||||
clientWidth = $bindable(),
|
||||
clientHeight = $bindable(),
|
||||
children,
|
||||
rest
|
||||
}: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
colorMode?: string;
|
||||
domNode: HTMLDivElement | null;
|
||||
clientWidth?: number;
|
||||
clientHeight?: number;
|
||||
children?: Snippet;
|
||||
rest: SvelteFlowRestProps<NodeType, EdgeType> &
|
||||
Omit<HTMLAttributes<HTMLDivElement>, 'onselectionchange'>;
|
||||
} = $props();
|
||||
|
||||
// Unfortunately we have to destructure the props here this way,
|
||||
// so we don't pass all the props as attributes to the div element
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
let {
|
||||
id,
|
||||
class: className,
|
||||
nodeTypes,
|
||||
edgeTypes,
|
||||
colorMode: _colorMode,
|
||||
isValidConnection,
|
||||
onmove,
|
||||
onmovestart,
|
||||
onmoveend,
|
||||
onflowerror,
|
||||
ondelete,
|
||||
onbeforedelete,
|
||||
onbeforeconnect,
|
||||
onconnect,
|
||||
onconnectstart,
|
||||
onconnectend,
|
||||
onbeforereconnect,
|
||||
onreconnect,
|
||||
onreconnectstart,
|
||||
onreconnectend,
|
||||
onclickconnectstart,
|
||||
onclickconnectend,
|
||||
oninit,
|
||||
onselectionchange,
|
||||
onselectiondragstart,
|
||||
onselectiondrag,
|
||||
onselectiondragstop,
|
||||
onselectionstart,
|
||||
onselectionend,
|
||||
clickConnect,
|
||||
fitView,
|
||||
fitViewOptions,
|
||||
nodeOrigin,
|
||||
nodeDragThreshold,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
initialViewport,
|
||||
connectionRadius,
|
||||
connectionMode,
|
||||
selectionMode,
|
||||
selectNodesOnDrag,
|
||||
snapGrid,
|
||||
defaultMarkerColor,
|
||||
translateExtent,
|
||||
nodeExtent,
|
||||
onlyRenderVisibleElements,
|
||||
autoPanOnConnect,
|
||||
autoPanOnNodeDrag,
|
||||
colorModeSSR,
|
||||
style,
|
||||
defaultEdgeOptions,
|
||||
elevateNodesOnSelect,
|
||||
elevateEdgesOnSelect,
|
||||
nodesDraggable,
|
||||
nodesConnectable,
|
||||
elementsSelectable,
|
||||
nodesFocusable,
|
||||
edgesFocusable,
|
||||
disableKeyboardA11y,
|
||||
noDragClass,
|
||||
noPanClass,
|
||||
noWheelClass,
|
||||
...divAttributes
|
||||
} = $derived(rest);
|
||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||
|
||||
type OnlyDivAttributes<T> = {
|
||||
[K in keyof T]: K extends keyof HTMLAttributes<HTMLDivElement> ? T[K] : never;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={domNode}
|
||||
bind:clientHeight
|
||||
bind:clientWidth
|
||||
style:width={toPxString(width)}
|
||||
style:height={toPxString(height)}
|
||||
class={['svelte-flow', 'svelte-flow__container', className, colorMode]}
|
||||
data-testid="svelte-flow__wrapper"
|
||||
role="application"
|
||||
{...divAttributes satisfies OnlyDivAttributes<typeof divAttributes>}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.svelte-flow {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
|
||||
background-color: var(--background-color, var(--background-color-default));
|
||||
}
|
||||
|
||||
:root {
|
||||
--background-color-default: #fff;
|
||||
--background-pattern-color-default: #ddd;
|
||||
|
||||
--minimap-mask-color-default: rgb(240, 240, 240, 0.6);
|
||||
--minimap-mask-stroke-color-default: none;
|
||||
--minimap-mask-stroke-width-default: 1;
|
||||
|
||||
--controls-button-background-color-default: #fefefe;
|
||||
--controls-button-background-color-hover-default: #f4f4f4;
|
||||
--controls-button-color-default: inherit;
|
||||
--controls-button-color-hover-default: inherit;
|
||||
--controls-button-border-color-default: #eee;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DOMAttributes } from 'svelte/elements';
|
||||
import type { ClassValue } from 'svelte/elements';
|
||||
import type {
|
||||
ConnectionLineType,
|
||||
NodeOrigin,
|
||||
@@ -17,7 +17,10 @@ import type {
|
||||
ColorMode,
|
||||
OnConnect,
|
||||
OnConnectStart,
|
||||
OnConnectEnd
|
||||
OnConnectEnd,
|
||||
OnReconnect,
|
||||
OnReconnectStart,
|
||||
OnReconnectEnd
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type {
|
||||
@@ -29,324 +32,443 @@ import type {
|
||||
DefaultEdgeOptions,
|
||||
FitViewOptions,
|
||||
OnDelete,
|
||||
OnEdgeCreate,
|
||||
OnBeforeConnect,
|
||||
OnBeforeDelete,
|
||||
IsValidConnection
|
||||
IsValidConnection,
|
||||
OnBeforeReconnect,
|
||||
OnSelectionChange
|
||||
} from '$lib/types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export type SvelteFlowProps = DOMAttributes<HTMLDivElement> & {
|
||||
/** The id of the flow
|
||||
*
|
||||
* This is necessary if you want to render multiple flows.
|
||||
* @optional
|
||||
*/
|
||||
id?: string;
|
||||
/** An array of nodes to render in a controlled flow.
|
||||
* @example
|
||||
* const nodes = writable([
|
||||
* {
|
||||
* id: 'node-1',
|
||||
* type: 'input',
|
||||
* data: { label: 'Node 1' },
|
||||
* position: { x: 250, y: 50 }
|
||||
* }
|
||||
* ]);
|
||||
*/
|
||||
nodes: Writable<Node[]>;
|
||||
/** An array of edges to render in a controlled flow.
|
||||
* @example
|
||||
* const edges = writable([
|
||||
* {
|
||||
* id: 'edge-1-2',
|
||||
* source: 'node-1',
|
||||
* target: 'node-2',
|
||||
* }
|
||||
* ]);
|
||||
*/
|
||||
edges: Writable<Edge[]>;
|
||||
/** Custom node types to be available in a flow.
|
||||
*
|
||||
* Svelte Flow matches a node's type to a component in the nodeTypes object.
|
||||
* @example
|
||||
* import CustomNode from './CustomNode.svelte';
|
||||
*
|
||||
* const nodeTypes = { nameOfNodeType: CustomNode };
|
||||
*/
|
||||
nodeTypes?: NodeTypes;
|
||||
/** Custom edge types to be available in a flow.
|
||||
*
|
||||
* Svelte Flow matches an edge's type to a component in the edgeTypes object.
|
||||
* @example
|
||||
* import CustomEdge from './CustomEdge.svelte';
|
||||
*
|
||||
* const edgeTypes = { nameOfEdgeType: CustomEdge };
|
||||
*/
|
||||
edgeTypes?: EdgeTypes;
|
||||
/** Pressing down this key you can select multiple elements with a selection box.
|
||||
* @default 'Shift'
|
||||
*/
|
||||
selectionKey?: KeyDefinition | KeyDefinition[] | null;
|
||||
/** If a key is set, you can pan the viewport while that key is held down even if panOnScroll is set to false.
|
||||
*
|
||||
* By setting this prop to null you can disable this functionality.
|
||||
* @default 'Space'
|
||||
*/
|
||||
panActivationKey?: KeyDefinition | KeyDefinition[] | null;
|
||||
/** Pressing down this key deletes all selected nodes & edges.
|
||||
* @default 'Backspace'
|
||||
*/
|
||||
deleteKey?: KeyDefinition | KeyDefinition[] | null;
|
||||
/** Pressing down this key you can select multiple elements by clicking.
|
||||
* @default 'Meta' for macOS, "Ctrl" for other systems
|
||||
*/
|
||||
multiSelectionKey?: KeyDefinition | KeyDefinition[] | null;
|
||||
/** If a key is set, you can zoom the viewport while that key is held down even if panOnScroll is set to false.
|
||||
*
|
||||
* By setting this prop to null you can disable this functionality.
|
||||
* @default 'Meta' for macOS, "Ctrl" for other systems
|
||||
* */
|
||||
zoomActivationKey?: KeyDefinition | KeyDefinition[] | null;
|
||||
/** If set, initial viewport will show all nodes & edges */
|
||||
fitView?: boolean;
|
||||
/** Options to be used in combination with fitView
|
||||
* @example
|
||||
* const fitViewOptions = {
|
||||
* padding: 0.1,
|
||||
* includeHiddenNodes: false,
|
||||
* minZoom: 0.1,
|
||||
* maxZoom: 1,
|
||||
* duration: 200,
|
||||
* nodes: [{id: 'node-1'}, {id: 'node-2'}], // nodes to fit
|
||||
* };
|
||||
*/
|
||||
fitViewOptions?: FitViewOptions;
|
||||
/** Defines nodes relative position to its coordinates
|
||||
* @example
|
||||
* [0, 0] // default, top left
|
||||
* [0.5, 0.5] // center
|
||||
* [1, 1] // bottom right
|
||||
*/
|
||||
nodeOrigin?: NodeOrigin;
|
||||
/** With a threshold greater than zero you can control the distinction between node drag and click events.
|
||||
*
|
||||
* If threshold equals 1, you need to drag the node 1 pixel before a drag event is fired.
|
||||
* @default 1
|
||||
*/
|
||||
nodeDragThreshold?: number;
|
||||
/** Distance that the mouse can move between mousedown/up that will trigger a click
|
||||
* @default 0
|
||||
*/
|
||||
paneClickDistance?: number;
|
||||
/** Distance that the mouse can move between mousedown/up that will trigger a click
|
||||
* @default 0
|
||||
*/
|
||||
nodeClickDistance?: number;
|
||||
/** Minimum zoom level
|
||||
* @default 0.5
|
||||
*/
|
||||
minZoom?: number;
|
||||
/** Maximum zoom level
|
||||
* @default 2
|
||||
*/
|
||||
maxZoom?: number;
|
||||
/** Sets the initial position and zoom of the viewport.
|
||||
*
|
||||
* If a default viewport is provided but fitView is enabled, the default viewport will be ignored.
|
||||
* @example
|
||||
* const initialViewport = {
|
||||
* zoom: 0.5,
|
||||
* position: { x: 0, y: 0 }
|
||||
* };
|
||||
*/
|
||||
initialViewport?: Viewport;
|
||||
/** Custom viewport writable to be used instead of internal one */
|
||||
viewport?: Writable<Viewport>;
|
||||
/** The radius around a handle where you drop a connection line to create a new edge.
|
||||
* @default 20
|
||||
*/
|
||||
connectionRadius?: number;
|
||||
/** 'strict' connection mode will only allow you to connect source handles to target handles.
|
||||
*
|
||||
* 'loose' connection mode will allow you to connect handles of any type to one another.
|
||||
* @default 'strict'
|
||||
*/
|
||||
connectionMode?: ConnectionMode;
|
||||
/** Styles to be applied to the connection line */
|
||||
connectionLineStyle?: string;
|
||||
/** Styles to be applied to the container of the connection line */
|
||||
connectionLineContainerStyle?: string;
|
||||
/** When set to "partial", when the user creates a selection box by click and dragging nodes that are only partially in the box are still selected.
|
||||
* @default 'full'
|
||||
*/
|
||||
selectionMode?: SelectionMode;
|
||||
/** Grid all nodes will snap to
|
||||
* @example [20, 20]
|
||||
*/
|
||||
snapGrid?: SnapGrid;
|
||||
/** Color of edge markers
|
||||
* @example "#b1b1b7"
|
||||
*/
|
||||
defaultMarkerColor?: string;
|
||||
/** Controls if all nodes should be draggable
|
||||
* @default true
|
||||
*/
|
||||
nodesDraggable?: boolean;
|
||||
/** Controls if all nodes should be connectable to each other
|
||||
* @default true
|
||||
*/
|
||||
nodesConnectable?: boolean;
|
||||
/** Controls if all elements should (nodes & edges) be selectable
|
||||
* @default true
|
||||
*/
|
||||
elementsSelectable?: boolean;
|
||||
/** By default the viewport extends infinitely. You can use this prop to set a boundary.
|
||||
*
|
||||
* The first pair of coordinates is the top left boundary and the second pair is the bottom right.
|
||||
* @example [[-1000, -10000], [1000, 1000]]
|
||||
*/
|
||||
translateExtent?: CoordinateExtent;
|
||||
/** By default the nodes can be placed anywhere. You can use this prop to set a boundary.
|
||||
*
|
||||
* The first pair of coordinates is the top left boundary and the second pair is the bottom right.
|
||||
* @example [[-1000, -10000], [1000, 1000]]
|
||||
*/
|
||||
nodeExtent?: CoordinateExtent;
|
||||
/** Disabling this prop will allow the user to scroll the page even when their pointer is over the flow.
|
||||
* @default true
|
||||
*/
|
||||
preventScrolling?: boolean;
|
||||
/** Controls if the viewport should zoom by scrolling inside the container */
|
||||
zoomOnScroll?: boolean;
|
||||
/** Controls if the viewport should zoom by double clicking somewhere on the flow */
|
||||
zoomOnDoubleClick?: boolean;
|
||||
/** Controls if the viewport should zoom by pinching on a touch screen */
|
||||
zoomOnPinch?: boolean;
|
||||
/** Controls if the viewport should pan by scrolling inside the container
|
||||
*
|
||||
* Can be limited to a specific direction with panOnScrollMode
|
||||
*/
|
||||
panOnScroll?: boolean;
|
||||
/** This prop is used to limit the direction of panning when panOnScroll is enabled.
|
||||
*
|
||||
* The "free" option allows panning in any direction.
|
||||
* @default "free"
|
||||
* @example "horizontal" | "vertical"
|
||||
*/
|
||||
panOnScrollMode?: PanOnScrollMode;
|
||||
/** Enableing this prop allows users to pan the viewport by clicking and dragging.
|
||||
*
|
||||
* You can also set this prop to an array of numbers to limit which mouse buttons can activate panning.
|
||||
* @example [0, 2] // allows panning with the left and right mouse buttons
|
||||
* [0, 1, 2, 3, 4] // allows panning with all mouse buttons
|
||||
*/
|
||||
panOnDrag?: boolean | number[];
|
||||
/** Select multiple elements with a selection box, without pressing down selectionKey */
|
||||
selectionOnDrag?: boolean;
|
||||
/** You can enable this optimisation to instruct Svelte Flow to only render nodes and edges that would be visible in the viewport.
|
||||
*
|
||||
* This might improve performance when you have a large number of nodes and edges but also adds an overhead.
|
||||
* @default false
|
||||
*/
|
||||
onlyRenderVisibleElements?: boolean;
|
||||
/** You can enable this prop to automatically pan the viewport while making a new connection.
|
||||
* @default true
|
||||
*/
|
||||
autoPanOnConnect?: boolean;
|
||||
/** You can enable this prop to automatically pan the viewport while dragging a node.
|
||||
* @default true
|
||||
*/
|
||||
autoPanOnNodeDrag?: boolean;
|
||||
/** Set position of the attribution
|
||||
* @default 'bottom-right'
|
||||
* @example 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
|
||||
*/
|
||||
attributionPosition?: PanelPosition;
|
||||
/** By default, we render a small attribution in the corner of your flows that links back to the project.
|
||||
*
|
||||
* Anyone is free to remove this attribution whether they're a Pro subscriber or not
|
||||
* but we ask that you take a quick look at our {@link https://reactflow.dev/learn/troubleshooting/remove-attribution | removing attribution guide}
|
||||
* before doing so.
|
||||
*/
|
||||
proOptions?: ProOptions;
|
||||
/** Defaults to be applied to all new edges that are added to the flow.
|
||||
*
|
||||
* Properties on a new edge will override these defaults if they exist.
|
||||
* @example
|
||||
* const defaultEdgeOptions = {
|
||||
* type: 'customEdgeType',
|
||||
* animated: true,
|
||||
* interactionWidth: 10,
|
||||
* data: { label: 'custom label' },
|
||||
* hidden: false,
|
||||
* deletable: true,
|
||||
* selected: false,
|
||||
* focusable: true,
|
||||
* markerStart: EdgeMarker.ArrowClosed,
|
||||
* markerEnd: EdgeMarker.ArrowClosed,
|
||||
* zIndex: 12,
|
||||
* ariaLabel: 'custom aria label'
|
||||
* }
|
||||
*/
|
||||
defaultEdgeOptions?: DefaultEdgeOptions;
|
||||
/** Sets a fixed width for the flow */
|
||||
width?: number;
|
||||
/** Sets a fixed height for the flow */
|
||||
height?: number;
|
||||
/** Controls color scheme used for styling the flow
|
||||
* @default 'system'
|
||||
* @example 'system' | 'light' | 'dark'
|
||||
*/
|
||||
colorMode?: ColorMode;
|
||||
/** Class to be applied to the flow container */
|
||||
class?: string;
|
||||
/** Styles to be applied to the flow container */
|
||||
style?: string;
|
||||
/** Choose from the built-in edge types to be used for connections
|
||||
* @default 'default' | ConnectionLineType.Bezier
|
||||
* @example 'straight' | 'default' | 'step' | 'smoothstep' | 'bezier'
|
||||
* @example ConnectionLineType.Straight | ConnectionLineType.Default | ConnectionLineType.Step | ConnectionLineType.SmoothStep | ConnectionLineType.Bezier
|
||||
*/
|
||||
connectionLineType?: ConnectionLineType;
|
||||
/** This callback can be used to validate a new connection
|
||||
*
|
||||
* If you return false, the edge will not be added to your flow.
|
||||
* If you have custom connection logic its preferred to use this callback over the isValidConnection prop on the handle component for performance reasons.
|
||||
* @default (connection: Connection) => true
|
||||
*/
|
||||
isValidConnection?: IsValidConnection;
|
||||
/** This event handler is called when the user begins to pan or zoom the viewport */
|
||||
onMoveStart?: OnMoveStart;
|
||||
/** This event handler is called when the user pans or zooms the viewport */
|
||||
onMove?: OnMove;
|
||||
/** This event handler is called when the user stops panning or zooming the viewport */
|
||||
onMoveEnd?: OnMoveEnd;
|
||||
/** Ocassionally something may happen that causes Svelte Flow to throw an error.
|
||||
*
|
||||
* Instead of exploding your application, we log a message to the console and then call this event handler.
|
||||
* You might use it for additional logging or to show a message to the user.
|
||||
*/
|
||||
onerror?: OnError;
|
||||
/** This handler gets called when the user deletes nodes or edges.
|
||||
* @example
|
||||
* onDelete={({nodes, edges}) => {
|
||||
* console.log('deleted nodes:', nodes);
|
||||
* console.log('deleted edges:', edges);
|
||||
* }}
|
||||
*/
|
||||
ondelete?: OnDelete;
|
||||
/** This handler gets called before the user deletes nodes or edges and provides a way to abort the deletion by returning false. */
|
||||
onbeforedelete?: OnBeforeDelete;
|
||||
import type { Component } from 'svelte';
|
||||
import type {
|
||||
EdgeEvents,
|
||||
NodeEvents,
|
||||
NodeSelectionEvents,
|
||||
OnSelectionDrag,
|
||||
PaneEvents
|
||||
} from '$lib/types/events';
|
||||
|
||||
/** This handler gets called when a new edge is created. You can use it to modify the newly created edge. */
|
||||
onedgecreate?: OnEdgeCreate;
|
||||
|
||||
/** This event gets fired when a connection successfully completes and an edge is created. */
|
||||
onconnect?: OnConnect;
|
||||
/** When a user starts to drag a connection line, this event gets fired. */
|
||||
onconnectstart?: OnConnectStart;
|
||||
/** When a user stops dragging a connection line, this event gets fired. */
|
||||
onconnectend?: OnConnectEnd;
|
||||
/** This handler gets called when the flow is finished initializing */
|
||||
oninit?: () => void;
|
||||
};
|
||||
export type SvelteFlowProps<
|
||||
NodeType extends Node = Node,
|
||||
EdgeType extends Edge = Edge
|
||||
> = NodeEvents<NodeType> &
|
||||
NodeSelectionEvents<NodeType> &
|
||||
EdgeEvents<EdgeType> &
|
||||
PaneEvents & {
|
||||
/**
|
||||
* The id of the flow. This is necessary if you want to render multiple flows.
|
||||
*/
|
||||
id?: string;
|
||||
/** Sets a fixed width for the flow */
|
||||
width?: number;
|
||||
/** Sets a fixed height for the flow */
|
||||
height?: number;
|
||||
/**
|
||||
* An array of nodes to render in a flow.
|
||||
* @example
|
||||
* const nodes = $state.raw([
|
||||
* {
|
||||
* id: 'node-1',
|
||||
* type: 'input',
|
||||
* data: { label: 'Node 1' },
|
||||
* position: { x: 250, y: 50 }
|
||||
* }
|
||||
* ]);
|
||||
*/
|
||||
nodes?: NodeType[];
|
||||
/**
|
||||
* An array of edges to render in a flow.
|
||||
* @example
|
||||
* const edges = $state.raw([
|
||||
* {
|
||||
* id: 'edge-1-2',
|
||||
* source: 'node-1',
|
||||
* target: 'node-2',
|
||||
* }
|
||||
* ]);
|
||||
*/
|
||||
edges?: EdgeType[];
|
||||
/**
|
||||
* Custom node types to be available in a flow.
|
||||
* Svelte Flow matches a node's type to a component in the nodeTypes object.
|
||||
* @example
|
||||
* import CustomNode from './CustomNode.svelte';
|
||||
*
|
||||
* const nodeTypes = { nameOfNodeType: CustomNode };
|
||||
*/
|
||||
nodeTypes?: NodeTypes;
|
||||
/**
|
||||
* Custom edge types to be available in a flow.
|
||||
* Svelte Flow matches an edge's type to a component in the edgeTypes object.
|
||||
* @example
|
||||
* import CustomEdge from './CustomEdge.svelte';
|
||||
*
|
||||
* const edgeTypes = { nameOfEdgeType: CustomEdge };
|
||||
*/
|
||||
edgeTypes?: EdgeTypes;
|
||||
/** Pressing down this key you can select multiple elements with a selection box.
|
||||
* @default 'Shift'
|
||||
*/
|
||||
selectionKey?: KeyDefinition | KeyDefinition[] | null;
|
||||
/** If a key is set, you can pan the viewport while that key is held down even if panOnScroll is set to false.
|
||||
*
|
||||
* By setting this prop to null you can disable this functionality.
|
||||
* @default 'Space'
|
||||
*/
|
||||
panActivationKey?: KeyDefinition | KeyDefinition[] | null;
|
||||
/** Pressing down this key deletes all selected nodes & edges.
|
||||
* @default 'Backspace'
|
||||
*/
|
||||
deleteKey?: KeyDefinition | KeyDefinition[] | null;
|
||||
/** Pressing down this key you can select multiple elements by clicking.
|
||||
* @default 'Meta' for macOS, "Ctrl" for other systems
|
||||
*/
|
||||
multiSelectionKey?: KeyDefinition | KeyDefinition[] | null;
|
||||
/** If a key is set, you can zoom the viewport while that key is held down even if panOnScroll is set to false.
|
||||
*
|
||||
* By setting this prop to null you can disable this functionality.
|
||||
* @default 'Meta' for macOS, "Ctrl" for other systems
|
||||
* */
|
||||
zoomActivationKey?: KeyDefinition | KeyDefinition[] | null;
|
||||
/** If set, initial viewport will show all nodes & edges */
|
||||
fitView?: boolean;
|
||||
/**
|
||||
* Options to be used in combination with fitView
|
||||
* @example
|
||||
* const fitViewOptions = {
|
||||
* padding: 0.1,
|
||||
* includeHiddenNodes: false,
|
||||
* minZoom: 0.1,
|
||||
* maxZoom: 1,
|
||||
* duration: 200,
|
||||
* nodes: [{id: 'node-1'}, {id: 'node-2'}], // nodes to fit
|
||||
* };
|
||||
*/
|
||||
fitViewOptions?: FitViewOptions<NodeType>;
|
||||
/**
|
||||
* Defines nodes relative position to its coordinates
|
||||
* @default [0, 0]
|
||||
* @example
|
||||
* [0, 0] // default, top left
|
||||
* [0.5, 0.5] // center
|
||||
* [1, 1] // bottom right
|
||||
*/
|
||||
nodeOrigin?: NodeOrigin;
|
||||
/**
|
||||
* With a threshold greater than zero you can control the distinction between node drag and click events.
|
||||
* If threshold equals 1, you need to drag the node 1 pixel before a drag event is fired.
|
||||
* @default 1
|
||||
*/
|
||||
nodeDragThreshold?: number;
|
||||
/**
|
||||
* Distance that the mouse can move between mousedown/up that will trigger a click
|
||||
* @default 0
|
||||
*/
|
||||
paneClickDistance?: number;
|
||||
/** Distance that the mouse can move between mousedown/up that will trigger a click
|
||||
* @default 0
|
||||
*/
|
||||
nodeClickDistance?: number;
|
||||
/** Minimum zoom level
|
||||
* @default 0.5
|
||||
*/
|
||||
minZoom?: number;
|
||||
/** Maximum zoom level
|
||||
* @default 2
|
||||
*/
|
||||
maxZoom?: number;
|
||||
/**
|
||||
* Sets the initial position and zoom of the viewport.
|
||||
* If a default viewport is provided but fitView is enabled, the default viewport will be ignored.
|
||||
* @default { zoom: 1, position: { x: 0, y: 0 } }
|
||||
* @example
|
||||
* const initialViewport = {
|
||||
* zoom: 0.5,
|
||||
* position: { x: 0, y: 0 }
|
||||
* };
|
||||
*/
|
||||
initialViewport?: Viewport;
|
||||
/** Custom viewport to be used instead of internal one */
|
||||
viewport?: Viewport;
|
||||
/**
|
||||
* The radius around a handle where you drop a connection line to create a new edge.
|
||||
* @default 20
|
||||
*/
|
||||
connectionRadius?: number;
|
||||
/**
|
||||
* 'strict' connection mode will only allow you to connect source handles to target handles.
|
||||
* 'loose' connection mode will allow you to connect handles of any type to one another.
|
||||
* @default 'strict'
|
||||
*/
|
||||
connectionMode?: ConnectionMode;
|
||||
/** Provide a custom snippet to be used insted of the default connection line */
|
||||
connectionLineComponent?: Component;
|
||||
/** Styles to be applied to the connection line */
|
||||
connectionLineStyle?: string;
|
||||
/** Styles to be applied to the container of the connection line */
|
||||
connectionLineContainerStyle?: string;
|
||||
/**
|
||||
* When set to "partial", when the user creates a selection box by click and dragging
|
||||
* nodes that are only partially in the box are still selected.
|
||||
* @default 'full'
|
||||
*/
|
||||
selectionMode?: SelectionMode;
|
||||
/**
|
||||
* Controls if nodes should be automatically selected when being dragged
|
||||
*/
|
||||
selectNodesOnDrag?: boolean;
|
||||
/**
|
||||
* Grid all nodes will snap to
|
||||
* @example [20, 20]
|
||||
*/
|
||||
snapGrid?: SnapGrid;
|
||||
/** Color of edge markers
|
||||
* @example "#b1b1b7"
|
||||
*/
|
||||
defaultMarkerColor?: string;
|
||||
/**
|
||||
* Controls if all nodes should be draggable
|
||||
* @default true
|
||||
*/
|
||||
nodesDraggable?: boolean;
|
||||
/**
|
||||
* Controls if all nodes should be connectable to each other
|
||||
* @default true
|
||||
*/
|
||||
nodesConnectable?: boolean;
|
||||
/** Controls if all elements should (nodes & edges) be selectable
|
||||
* @default true
|
||||
*/
|
||||
elementsSelectable?: boolean;
|
||||
/**
|
||||
* When `true`, focus between nodes can be cycled with the `Tab` key and selected with the `Enter`
|
||||
* key. This option can be overridden by individual nodes by setting their `focusable` prop.
|
||||
* @default true
|
||||
*/
|
||||
nodesFocusable?: boolean;
|
||||
/**
|
||||
* When `true`, focus between edges can be cycled with the `Tab` key and selected with the `Enter`
|
||||
* key. This option can be overridden by individual edges by setting their `focusable` prop.
|
||||
* @default true
|
||||
*/
|
||||
edgesFocusable?: boolean;
|
||||
/**
|
||||
* By default the viewport extends infinitely. You can use this prop to set a boundary.
|
||||
* The first pair of coordinates is the top left boundary and the second pair is the bottom right.
|
||||
* @default @default [[-∞, -∞], [+∞, +∞]]
|
||||
* @example [[-1000, -10000], [1000, 1000]]
|
||||
*/
|
||||
translateExtent?: CoordinateExtent;
|
||||
/**
|
||||
* By default the nodes can be placed anywhere. You can use this prop to set a boundary.
|
||||
* The first pair of coordinates is the top left boundary and the second pair is the bottom right.
|
||||
* @default [[-∞, -∞], [+∞, +∞]]
|
||||
* @example [[-1000, -10000], [1000, 1000]]
|
||||
*/
|
||||
nodeExtent?: CoordinateExtent;
|
||||
/**
|
||||
* Disabling this prop will allow the user to scroll the page even when their pointer is over the flow.
|
||||
* @default true
|
||||
*/
|
||||
preventScrolling?: boolean;
|
||||
/**
|
||||
* Controls if the viewport should zoom by scrolling inside the container.
|
||||
* @default true
|
||||
*/
|
||||
zoomOnScroll?: boolean;
|
||||
/**
|
||||
* Controls if the viewport should zoom by double clicking somewhere on the flow
|
||||
* @default true
|
||||
*/
|
||||
zoomOnDoubleClick?: boolean;
|
||||
/**
|
||||
* Controls if the viewport should zoom by pinching on a touch screen
|
||||
* @default true
|
||||
*/
|
||||
zoomOnPinch?: boolean;
|
||||
/**
|
||||
* Controls if the viewport should pan by scrolling inside the container
|
||||
* Can be limited to a specific direction with panOnScrollMode
|
||||
* @default false
|
||||
*/
|
||||
panOnScroll?: boolean;
|
||||
/**
|
||||
* This prop is used to limit the direction of panning when panOnScroll is enabled.
|
||||
* The "free" option allows panning in any direction.
|
||||
* @default "free"
|
||||
* @example "horizontal" | "vertical"
|
||||
*/
|
||||
panOnScrollMode?: PanOnScrollMode;
|
||||
/**
|
||||
* Enableing this prop allows users to pan the viewport by clicking and dragging.
|
||||
* You can also set this prop to an array of numbers to limit which mouse buttons can activate panning.
|
||||
* @default true
|
||||
* @example [0, 2] // allows panning with the left and right mouse buttons
|
||||
* [0, 1, 2, 3, 4] // allows panning with all mouse buttons
|
||||
*/
|
||||
panOnDrag?: boolean | number[];
|
||||
/**
|
||||
* Select multiple elements with a selection box, without pressing down selectionKey.
|
||||
* @default false
|
||||
*/
|
||||
selectionOnDrag?: boolean;
|
||||
/**
|
||||
* You can enable this optimisation to instruct Svelte Flow to only render nodes and edges that would be visible in the viewport.
|
||||
* This might improve performance when you have a large number of nodes and edges but also adds an overhead.
|
||||
* @default false
|
||||
*/
|
||||
onlyRenderVisibleElements?: boolean;
|
||||
/**
|
||||
* You can enable this prop to automatically pan the viewport while making a new connection.
|
||||
* @default true
|
||||
*/
|
||||
autoPanOnConnect?: boolean;
|
||||
/**
|
||||
* You can enable this prop to automatically pan the viewport while dragging a node.
|
||||
* @default true
|
||||
*/
|
||||
autoPanOnNodeDrag?: boolean;
|
||||
/**
|
||||
* Defaults to be applied to all new edges that are added to the flow.
|
||||
* Properties on a new edge will override these defaults if they exist.
|
||||
* @example
|
||||
* const defaultEdgeOptions = {
|
||||
* type: 'customEdgeType',
|
||||
* animated: true
|
||||
* }
|
||||
*/
|
||||
defaultEdgeOptions?: DefaultEdgeOptions;
|
||||
/**
|
||||
* Controls color scheme used for styling the flow
|
||||
* @default 'system'
|
||||
* @example 'system' | 'light' | 'dark'
|
||||
*/
|
||||
colorMode?: ColorMode;
|
||||
/** Fallback color mode for SSR if colorMode is set to 'system' */
|
||||
colorModeSSR?: Omit<ColorMode, 'system'>;
|
||||
/** Class to be applied to the flow container */
|
||||
class?: ClassValue;
|
||||
/** Styles to be applied to the flow container */
|
||||
style?: string;
|
||||
/** Choose from the built-in edge types to be used for connections
|
||||
* @default 'default' | ConnectionLineType.Bezier
|
||||
* @example 'straight' | 'default' | 'step' | 'smoothstep' | 'bezier'
|
||||
* @example ConnectionLineType.Straight | ConnectionLineType.Default | ConnectionLineType.Step | ConnectionLineType.SmoothStep | ConnectionLineType.Bezier
|
||||
*/
|
||||
connectionLineType?: ConnectionLineType;
|
||||
/** Enabling this option will raise the z-index of nodes when they are selected.
|
||||
* @default true
|
||||
*/
|
||||
elevateNodesOnSelect?: boolean;
|
||||
/**
|
||||
* Enabling this option will raise the z-index of edges when they are selected,
|
||||
* or when the connected nodes are selected.
|
||||
* @default true
|
||||
*/
|
||||
elevateEdgesOnSelect?: boolean;
|
||||
/**
|
||||
* You can use this prop to disable keyboard accessibility features such as selecting nodes or
|
||||
* moving selected nodes with the arrow keys.
|
||||
* @default false
|
||||
*/
|
||||
disableKeyboardA11y?: boolean;
|
||||
/**
|
||||
* If a node is draggable, clicking and dragging that node will move it around the canvas. Adding
|
||||
* the `"nodrag"` class prevents this behavior and this prop allows you to change the name of that
|
||||
* class.
|
||||
* @default "nodrag"
|
||||
*/
|
||||
noDragClass?: string;
|
||||
/**
|
||||
* Typically, scrolling the mouse wheel when the mouse is over the canvas will zoom the viewport.
|
||||
* Adding the `"nowheel"` class to an element n the canvas will prevent this behavior and this prop
|
||||
* allows you to change the name of that class.
|
||||
* @default "nowheel"
|
||||
*/
|
||||
noWheelClass?: string;
|
||||
/**
|
||||
* If an element in the canvas does not stop mouse events from propagating, clicking and dragging
|
||||
* that element will pan the viewport. Adding the `"nopan"` class prevents this behavior and this
|
||||
* prop allows you to change the name of that class.
|
||||
* @default "nopan"
|
||||
*/
|
||||
noPanClass?: string;
|
||||
/** Toggles ability to make connections via clicking the handles */
|
||||
clickConnect?: boolean;
|
||||
/**
|
||||
* This callback can be used to validate a new connection.
|
||||
* If you return `false`, the edge will not be added to your flow.
|
||||
* If you have custom connection logic its preferred to use this callback over the
|
||||
* `isValidConnection` prop on the handle component for performance reasons.
|
||||
*/
|
||||
/**
|
||||
* Set position of the attribution
|
||||
* @default 'bottom-right'
|
||||
* @example 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
|
||||
*/
|
||||
attributionPosition?: PanelPosition;
|
||||
/**
|
||||
* By default, we render a small attribution in the corner of your flows that links back to the project.
|
||||
* You are free to remove this attribution but we ask that you take a quick look at our
|
||||
* {@link https://svelteflow.dev/learn/troubleshooting/remove-attribution | removing attribution guide}
|
||||
* before doing so.
|
||||
*/
|
||||
proOptions?: ProOptions;
|
||||
isValidConnection?: IsValidConnection;
|
||||
/** This event handler is called when the user begins to pan or zoom the viewport */
|
||||
onmovestart?: OnMoveStart;
|
||||
/** This event handler is called when the user pans or zooms the viewport */
|
||||
onmove?: OnMove;
|
||||
/** This event handler is called when the user stops panning or zooming the viewport */
|
||||
onmoveend?: OnMoveEnd;
|
||||
/**
|
||||
* Ocassionally something may happen that causes Svelte Flow to throw an error.
|
||||
* Instead of exploding your application, we log a message to the console and then call this event handler.
|
||||
* You might use it for additional logging or to show a message to the user.
|
||||
*/
|
||||
onflowerror?: OnError;
|
||||
/** This handler gets called when the user deletes nodes or edges.
|
||||
* @example
|
||||
* onDelete={({nodes, edges}) => {
|
||||
* console.log('deleted nodes:', nodes);
|
||||
* console.log('deleted edges:', edges);
|
||||
* }}
|
||||
*/
|
||||
ondelete?: OnDelete<NodeType, EdgeType>;
|
||||
/** This handler gets called before the user deletes nodes or edges and provides a way to abort the deletion by returning false. */
|
||||
onbeforedelete?: OnBeforeDelete<NodeType, EdgeType>;
|
||||
/** This handler gets called when a new edge is created. You can use it to modify the newly created edge. */
|
||||
onbeforeconnect?: OnBeforeConnect<EdgeType>;
|
||||
/** This event gets fired when a connection successfully completes and an edge is created. */
|
||||
onconnect?: OnConnect;
|
||||
/** When a user starts to drag a connection line, this event gets fired. */
|
||||
onconnectstart?: OnConnectStart;
|
||||
/** When a user stops dragging a connection line, this event gets fired. */
|
||||
onconnectend?: OnConnectEnd;
|
||||
/** This event gets fired when after an edge was reconnected*/
|
||||
onreconnect?: OnReconnect<EdgeType>;
|
||||
/** This event gets fired when a user starts to reconnect an edge */
|
||||
onreconnectstart?: OnReconnectStart<EdgeType>;
|
||||
/** This event gets fired when a user stops reconnecting an edge */
|
||||
onreconnectend?: OnReconnectEnd<EdgeType>;
|
||||
/** This handler gets called when an edge is reconnected. You can use it to modify the edge before the update is applied. */
|
||||
onbeforereconnect?: OnBeforeReconnect<EdgeType>;
|
||||
/** A connection is started by clicking on a handle */
|
||||
onclickconnectstart?: OnConnectStart;
|
||||
/** A connection is finished by clicking on a handle */
|
||||
onclickconnectend?: OnConnectEnd;
|
||||
/** This handler gets called when the flow is finished initializing */
|
||||
oninit?: () => void;
|
||||
/** This event handler gets called when the selected nodes & edges change */
|
||||
onselectionchange?: OnSelectionChange<NodeType, EdgeType>;
|
||||
/** This event handler gets called when a user starts to drag a selection box. */
|
||||
onselectiondragstart?: OnSelectionDrag<NodeType>;
|
||||
/** This event handler gets called when a user drags a selection box. */
|
||||
onselectiondrag?: OnSelectionDrag<NodeType>;
|
||||
/** This event handler gets called when a user stops dragging a selection box. */
|
||||
onselectiondragstop?: OnSelectionDrag<NodeType>;
|
||||
/** This event handler gets called when the user starts to drag a selection box */
|
||||
onselectionstart?: (event: PointerEvent) => void;
|
||||
/** This event handler gets called when the user finishes dragging a selection box */
|
||||
onselectionend?: (event: PointerEvent) => void;
|
||||
};
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { CoordinateExtent } from '@xyflow/system';
|
||||
|
||||
import type { SvelteFlowStore } from '$lib/store/types';
|
||||
import type { EdgeTypes, NodeTypes } from '$lib/types';
|
||||
|
||||
// this is helper function for updating the store
|
||||
// for props where we need to call a specific store action
|
||||
export function updateStore(
|
||||
store: SvelteFlowStore,
|
||||
{
|
||||
nodeTypes,
|
||||
edgeTypes,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
translateExtent,
|
||||
paneClickDistance
|
||||
}: {
|
||||
nodeTypes?: NodeTypes;
|
||||
edgeTypes?: EdgeTypes;
|
||||
minZoom?: number;
|
||||
maxZoom?: number;
|
||||
translateExtent?: CoordinateExtent;
|
||||
paneClickDistance?: number;
|
||||
}
|
||||
) {
|
||||
if (nodeTypes !== undefined) {
|
||||
store.setNodeTypes(nodeTypes);
|
||||
}
|
||||
|
||||
if (edgeTypes !== undefined) {
|
||||
store.setEdgeTypes(edgeTypes);
|
||||
}
|
||||
|
||||
if (minZoom !== undefined) {
|
||||
store.setMinZoom(minZoom);
|
||||
}
|
||||
|
||||
if (maxZoom !== undefined) {
|
||||
store.setMaxZoom(maxZoom);
|
||||
}
|
||||
|
||||
if (translateExtent !== undefined) {
|
||||
store.setTranslateExtent(translateExtent);
|
||||
}
|
||||
|
||||
if (paneClickDistance !== undefined) {
|
||||
store.setPaneClickDistance(paneClickDistance);
|
||||
}
|
||||
}
|
||||
|
||||
const getKeys = <T extends object>(obj: T) => Object.keys(obj) as Array<keyof T>;
|
||||
|
||||
type UnwrapWritable<T> = T extends Writable<infer U> ? U : T;
|
||||
|
||||
// @todo there must be a better way to define the types here..
|
||||
export type UpdatableStoreProps = {
|
||||
flowId?: UnwrapWritable<SvelteFlowStore['flowId']>;
|
||||
connectionLineType?: UnwrapWritable<SvelteFlowStore['connectionLineType']>;
|
||||
connectionRadius?: UnwrapWritable<SvelteFlowStore['connectionRadius']>;
|
||||
selectionMode?: UnwrapWritable<SvelteFlowStore['selectionMode']>;
|
||||
snapGrid?: UnwrapWritable<SvelteFlowStore['snapGrid']>;
|
||||
defaultMarkerColor?: UnwrapWritable<SvelteFlowStore['defaultMarkerColor']>;
|
||||
nodesDraggable?: UnwrapWritable<SvelteFlowStore['nodesDraggable']>;
|
||||
nodesConnectable?: UnwrapWritable<SvelteFlowStore['nodesConnectable']>;
|
||||
elementsSelectable?: UnwrapWritable<SvelteFlowStore['elementsSelectable']>;
|
||||
onlyRenderVisibleElements?: UnwrapWritable<SvelteFlowStore['onlyRenderVisibleElements']>;
|
||||
isValidConnection?: UnwrapWritable<SvelteFlowStore['isValidConnection']>;
|
||||
autoPanOnConnect?: UnwrapWritable<SvelteFlowStore['autoPanOnConnect']>;
|
||||
autoPanOnNodeDrag?: UnwrapWritable<SvelteFlowStore['autoPanOnNodeDrag']>;
|
||||
connectionMode?: UnwrapWritable<SvelteFlowStore['connectionMode']>;
|
||||
onerror?: UnwrapWritable<SvelteFlowStore['onerror']>;
|
||||
ondelete?: UnwrapWritable<SvelteFlowStore['ondelete']>;
|
||||
onedgecreate?: UnwrapWritable<SvelteFlowStore['onedgecreate']>;
|
||||
nodeDragThreshold?: UnwrapWritable<SvelteFlowStore['nodeDragThreshold']>;
|
||||
onconnect?: UnwrapWritable<SvelteFlowStore['onconnect']>;
|
||||
onconnectstart?: UnwrapWritable<SvelteFlowStore['onconnectstart']>;
|
||||
onconnectend?: UnwrapWritable<SvelteFlowStore['onconnectend']>;
|
||||
onbeforedelete?: UnwrapWritable<SvelteFlowStore['onbeforedelete']>;
|
||||
nodeOrigin?: UnwrapWritable<SvelteFlowStore['nodeOrigin']>;
|
||||
};
|
||||
|
||||
export function updateStoreByKeys(store: SvelteFlowStore, keys: UpdatableStoreProps) {
|
||||
getKeys(keys).forEach((prop) => {
|
||||
const update = keys[prop];
|
||||
if (update !== undefined) {
|
||||
// @ts-expect-error @todo: how to fix this TS error?
|
||||
store[prop].set(update);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,22 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { useStore } from '$lib/store';
|
||||
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
|
||||
import type { SvelteFlowStore } from '$lib/store/types';
|
||||
import type { Node, Edge } from '$lib/types';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
const { viewport } = useStore();
|
||||
let {
|
||||
store = $bindable(),
|
||||
children
|
||||
}: { store: SvelteFlowStore<NodeType, EdgeType>; children: Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="svelte-flow__viewport xyflow__viewport"
|
||||
style="transform: translate({$viewport.x}px, {$viewport.y}px) scale({$viewport.zoom})"
|
||||
class="svelte-flow__viewport xyflow__viewport svelte-flow__container"
|
||||
style:transform="translate({store.viewport.x}px, {store.viewport.y}px) scale({store.viewport
|
||||
.zoom})"
|
||||
>
|
||||
<slot />
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.svelte-flow__viewport {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,92 +1,77 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { PanOnScrollMode, type Transform } from '@xyflow/system';
|
||||
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
|
||||
import { PanOnScrollMode, type PanZoomInstance, type Transform } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import zoom from '$lib/actions/zoom';
|
||||
import type { ZoomProps } from './types';
|
||||
import type { Node, Edge } from '$lib/types';
|
||||
|
||||
type $$Props = ZoomProps;
|
||||
let {
|
||||
store = $bindable(),
|
||||
panOnScrollMode = PanOnScrollMode.Free,
|
||||
preventScrolling = true,
|
||||
zoomOnScroll = true,
|
||||
zoomOnDoubleClick = true,
|
||||
zoomOnPinch = true,
|
||||
panOnDrag = true,
|
||||
panOnScroll = false,
|
||||
paneClickDistance = 1,
|
||||
onmovestart,
|
||||
onmove,
|
||||
onmoveend,
|
||||
oninit,
|
||||
children
|
||||
}: ZoomProps<NodeType, EdgeType> = $props();
|
||||
|
||||
export let initialViewport: $$Props['initialViewport'] = undefined;
|
||||
export let onMoveStart: $$Props['onMoveStart'] = undefined;
|
||||
export let onMove: $$Props['onMove'] = undefined;
|
||||
export let onMoveEnd: $$Props['onMoveEnd'] = undefined;
|
||||
export let panOnScrollMode: $$Props['panOnScrollMode'];
|
||||
export let preventScrolling: $$Props['preventScrolling'];
|
||||
export let zoomOnScroll: $$Props['zoomOnScroll'];
|
||||
export let zoomOnDoubleClick: $$Props['zoomOnDoubleClick'];
|
||||
export let zoomOnPinch: $$Props['zoomOnPinch'];
|
||||
export let panOnDrag: $$Props['panOnDrag'];
|
||||
export let panOnScroll: $$Props['panOnScroll'];
|
||||
export let paneClickDistance: $$Props['paneClickDistance'];
|
||||
let panOnDragActive = $derived(store.panActivationKeyPressed || panOnDrag);
|
||||
let panOnScrollActive = $derived(store.panActivationKeyPressed || panOnScroll);
|
||||
|
||||
const {
|
||||
viewport,
|
||||
panZoom,
|
||||
selectionRect,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
dragging,
|
||||
translateExtent,
|
||||
lib,
|
||||
panActivationKeyPressed,
|
||||
zoomActivationKeyPressed,
|
||||
viewportInitialized
|
||||
} = useStore();
|
||||
// We extract the initial value by destructuring
|
||||
const { viewport: initialViewport } = store;
|
||||
|
||||
$: viewPort = initialViewport || { x: 0, y: 0, zoom: 1 };
|
||||
$: _panOnDrag = $panActivationKeyPressed || panOnDrag;
|
||||
$: _panOnScroll = $panActivationKeyPressed || panOnScroll;
|
||||
|
||||
const onTransformChange = (transform: Transform) =>
|
||||
viewport.set({ x: transform[0], y: transform[1], zoom: transform[2] });
|
||||
|
||||
onMount(() => {
|
||||
$viewportInitialized = true;
|
||||
let onInitCalled = false;
|
||||
$effect(() => {
|
||||
if (!onInitCalled && store.viewportInitialized) {
|
||||
oninit?.();
|
||||
onInitCalled = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="svelte-flow__zoom"
|
||||
class="svelte-flow__zoom svelte-flow__container"
|
||||
use:zoom={{
|
||||
viewport,
|
||||
minZoom: $minZoom,
|
||||
maxZoom: $maxZoom,
|
||||
initialViewport: viewPort,
|
||||
dragging,
|
||||
panZoom,
|
||||
onPanZoomStart: onMoveStart,
|
||||
onPanZoom: onMove,
|
||||
onPanZoomEnd: onMoveEnd,
|
||||
viewport: store.viewport,
|
||||
minZoom: store.minZoom,
|
||||
maxZoom: store.maxZoom,
|
||||
initialViewport,
|
||||
onDraggingChange: (dragging: boolean) => {
|
||||
store.dragging = dragging;
|
||||
},
|
||||
setPanZoomInstance: (instance: PanZoomInstance) => {
|
||||
store.panZoom = instance;
|
||||
},
|
||||
onPanZoomStart: onmovestart,
|
||||
onPanZoom: onmove,
|
||||
onPanZoomEnd: onmoveend,
|
||||
zoomOnScroll,
|
||||
zoomOnDoubleClick,
|
||||
zoomOnPinch,
|
||||
panOnScroll: _panOnScroll,
|
||||
panOnDrag: _panOnDrag,
|
||||
panOnScroll: panOnScrollActive,
|
||||
panOnDrag: panOnDragActive,
|
||||
panOnScrollSpeed: 0.5,
|
||||
panOnScrollMode: panOnScrollMode || PanOnScrollMode.Free,
|
||||
zoomActivationKeyPressed: $zoomActivationKeyPressed,
|
||||
zoomActivationKeyPressed: store.zoomActivationKeyPressed,
|
||||
preventScrolling: typeof preventScrolling === 'boolean' ? preventScrolling : true,
|
||||
noPanClassName: 'nopan',
|
||||
noWheelClassName: 'nowheel',
|
||||
userSelectionActive: !!$selectionRect,
|
||||
translateExtent: $translateExtent,
|
||||
lib: $lib,
|
||||
noPanClassName: store.noPanClass,
|
||||
noWheelClassName: store.noWheelClass,
|
||||
userSelectionActive: !!store.selectionRect,
|
||||
translateExtent: store.translateExtent,
|
||||
lib: 'svelte',
|
||||
paneClickDistance,
|
||||
onTransformChange
|
||||
onTransformChange: (transform: Transform) => {
|
||||
store.viewport = { x: transform[0], y: transform[1], zoom: transform[2] };
|
||||
}
|
||||
}}
|
||||
>
|
||||
<slot />
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.svelte-flow__zoom {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 4;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { OnMoveStart, OnMove, OnMoveEnd, PanOnScrollMode, Viewport } from '@xyflow/system';
|
||||
import type { SvelteFlowStore } from '$lib/store/types';
|
||||
import type { Edge, Node } from '$lib/types';
|
||||
import type { OnMoveStart, OnMove, OnMoveEnd, PanOnScrollMode } from '@xyflow/system';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
export type ZoomProps = {
|
||||
initialViewport?: Viewport;
|
||||
export type ZoomProps<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
|
||||
store: SvelteFlowStore<NodeType, EdgeType>;
|
||||
panOnScrollMode: PanOnScrollMode;
|
||||
onMove?: OnMove;
|
||||
onMoveStart?: OnMoveStart;
|
||||
onMoveEnd?: OnMoveEnd;
|
||||
preventScrolling: boolean;
|
||||
zoomOnScroll: boolean;
|
||||
zoomOnDoubleClick: boolean;
|
||||
@@ -13,4 +13,9 @@ export type ZoomProps = {
|
||||
panOnScroll: boolean;
|
||||
panOnDrag: boolean | number[];
|
||||
paneClickDistance: number;
|
||||
onmove?: OnMove;
|
||||
onmovestart?: OnMoveStart;
|
||||
onmoveend?: OnMoveEnd;
|
||||
oninit?: () => void;
|
||||
children: Snippet;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { key } from '$lib/store';
|
||||
import type { StoreContext } from '$lib/store/types';
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
/**
|
||||
* Warns the user that they should use $derived() when calling a hook.
|
||||
* This is not neccessarry when the hook is called inside a child of <SvelteFlowFlow />,
|
||||
* however exceptions can be made if you don't want to return a closure.
|
||||
* @param functionName - The name of the function that is being called
|
||||
* @param force - If true, the warning will be shown regardless if child of <SvelteFlowFlow />
|
||||
*/
|
||||
export function derivedWarning(functionName: string) {
|
||||
const storeContext = getContext<StoreContext>(key);
|
||||
|
||||
if (!storeContext) {
|
||||
throw new Error(
|
||||
`In order to use ${functionName}() you need to wrap your component in a <SvelteFlowProvider />`
|
||||
);
|
||||
}
|
||||
|
||||
if (storeContext.provider && typeof window === 'object' && !$effect.tracking()) {
|
||||
throw new Error(`Use $derived(${functionName}()) to receive updates when values change.`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { ColorModeClass } from '@xyflow/system';
|
||||
import { useStore } from './useStore';
|
||||
|
||||
/**
|
||||
* Hook for receiving the current color mode class 'dark' or 'light'.
|
||||
*
|
||||
*/
|
||||
export function useColorMode(): { current: ColorModeClass } {
|
||||
const { colorMode } = $derived(useStore());
|
||||
|
||||
return {
|
||||
current: colorMode
|
||||
};
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { ColorMode, ColorModeClass } from '@xyflow/system';
|
||||
import { readable, type Readable } from 'svelte/store';
|
||||
|
||||
function getMediaQuery() {
|
||||
if (typeof window === 'undefined' || !window.matchMedia) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return window.matchMedia('(prefers-color-scheme: dark)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for receiving the current color mode class 'dark' or 'light'.
|
||||
*
|
||||
* @internal
|
||||
* @param colorMode - The color mode to use ('dark', 'light' or 'system')
|
||||
*/
|
||||
export function useColorModeClass(colorMode: ColorMode = 'light'): Readable<ColorModeClass> {
|
||||
const colorModeClass = readable<ColorModeClass>('light', (set) => {
|
||||
if (colorMode !== 'system') {
|
||||
set(colorMode);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaQuery = getMediaQuery();
|
||||
const updateColorModeClass = () => set(mediaQuery?.matches ? 'dark' : 'light');
|
||||
|
||||
set(mediaQuery?.matches ? 'dark' : 'light');
|
||||
mediaQuery?.addEventListener('change', updateColorModeClass);
|
||||
|
||||
return () => {
|
||||
mediaQuery?.removeEventListener('change', updateColorModeClass);
|
||||
};
|
||||
});
|
||||
|
||||
return colorModeClass;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
import type { ConnectionState } from '@xyflow/system';
|
||||
|
||||
/**
|
||||
* Hook for receiving the current connection.
|
||||
*
|
||||
* @public
|
||||
* @returns Current connection as a signal
|
||||
*/
|
||||
export function useConnection(): { current: ConnectionState } {
|
||||
const { connection } = $derived(useStore());
|
||||
|
||||
return {
|
||||
get current() {
|
||||
return connection;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { Readable } from 'svelte/store';
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
import type { ConnectionState } from '@xyflow/system';
|
||||
|
||||
/**
|
||||
* Hook for receiving the current connection.
|
||||
*
|
||||
* @public
|
||||
* @returns current connection as a readable store
|
||||
*/
|
||||
export function useConnection(): Readable<ConnectionState> {
|
||||
const { connection } = useStore();
|
||||
|
||||
return connection;
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { derived } from 'svelte/store';
|
||||
import { areConnectionMapsEqual, type HandleConnection, type HandleType } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
export type useHandleConnectionsParams = {
|
||||
type: HandleType;
|
||||
nodeId?: string;
|
||||
id?: string | null;
|
||||
};
|
||||
|
||||
const initialConnections: HandleConnection[] = [];
|
||||
|
||||
/**
|
||||
* Hook to check if a <Handle /> is connected to another <Handle /> and get the connections.
|
||||
*
|
||||
* @public
|
||||
* @param param.nodeId
|
||||
* @param param.type - handle type 'source' or 'target'
|
||||
* @param param.id - the handle id (this is only needed if the node has multiple handles of the same type)
|
||||
* @returns an array with connections
|
||||
*/
|
||||
export function useHandleConnections({ type, nodeId, id = null }: useHandleConnectionsParams) {
|
||||
const { edges, connectionLookup } = useStore();
|
||||
|
||||
const _nodeId = getContext<string>('svelteflow__node_id');
|
||||
const currentNodeId = nodeId ?? _nodeId;
|
||||
|
||||
let prevConnections: Map<string, HandleConnection> | undefined = undefined;
|
||||
|
||||
return derived(
|
||||
[edges, connectionLookup],
|
||||
([, connectionLookup], set) => {
|
||||
const nextConnections = connectionLookup.get(`${currentNodeId}-${type}-${id || null}`);
|
||||
|
||||
if (!areConnectionMapsEqual(nextConnections, prevConnections)) {
|
||||
prevConnections = nextConnections;
|
||||
set(Array.from(prevConnections?.values() || []));
|
||||
}
|
||||
},
|
||||
initialConnections
|
||||
);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { get } from 'svelte/store';
|
||||
import { errorMessages } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
export function useHandleEdgeSelect() {
|
||||
const {
|
||||
edgeLookup,
|
||||
selectionRect,
|
||||
selectionRectMode,
|
||||
multiselectionKeyPressed,
|
||||
addSelectedEdges,
|
||||
unselectNodesAndEdges,
|
||||
elementsSelectable
|
||||
} = useStore();
|
||||
|
||||
return (id: string) => {
|
||||
const edge = get(edgeLookup).get(id);
|
||||
|
||||
if (!edge) {
|
||||
console.warn('012', errorMessages['error012'](id));
|
||||
return;
|
||||
}
|
||||
|
||||
const selectable =
|
||||
edge.selectable || (get(elementsSelectable) && typeof edge.selectable === 'undefined');
|
||||
|
||||
if (selectable) {
|
||||
selectionRect.set(null);
|
||||
selectionRectMode.set(null);
|
||||
|
||||
if (!edge.selected) {
|
||||
addSelectedEdges([id]);
|
||||
} else if (edge.selected && get(multiselectionKeyPressed)) {
|
||||
unselectNodesAndEdges({ nodes: [], edges: [edge] });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
/**
|
||||
* Hook for seeing if nodes are initialized
|
||||
* @returns A boolean that indicates if nodes are initialized
|
||||
* @public
|
||||
*/
|
||||
export function useNodesInitialized() {
|
||||
const { nodesInitialized } = $derived(useStore());
|
||||
return {
|
||||
get current() {
|
||||
return nodesInitialized;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for seeing if the viewport is initialized
|
||||
* @returns - reactive viewportInitialized
|
||||
*/
|
||||
export function useViewportInitialized() {
|
||||
const { viewportInitialized } = $derived(useStore());
|
||||
return {
|
||||
get current() {
|
||||
return viewportInitialized;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { useStore } from '$lib/store';
|
||||
import type { Readable } from 'svelte/store';
|
||||
|
||||
/**
|
||||
* Hook for seeing if nodes are initialized
|
||||
* @returns - nodesInitialized Writable
|
||||
*/
|
||||
export function useNodesInitialized() {
|
||||
const { nodesInitialized } = useStore();
|
||||
return {
|
||||
subscribe: nodesInitialized.subscribe
|
||||
} as Readable<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for seeing if the flow is initialized
|
||||
* @returns - initialized Writable
|
||||
*/
|
||||
export function useInitialized() {
|
||||
const { initialized } = useStore();
|
||||
return {
|
||||
subscribe: initialized.subscribe
|
||||
} as Readable<boolean>;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useStore } from '$lib/store';
|
||||
import type { InternalNode } from '$lib/types';
|
||||
|
||||
/**
|
||||
* Hook to get an internal node by id.
|
||||
*
|
||||
* @public
|
||||
* @param id - the node id
|
||||
* @returns An internal node or undefined
|
||||
*/
|
||||
export function useInternalNode(id: string): { current: InternalNode | undefined } {
|
||||
const { nodeLookup, nodes } = $derived(useStore());
|
||||
|
||||
const node = $derived.by(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
nodes;
|
||||
return nodeLookup.get(id);
|
||||
});
|
||||
|
||||
return {
|
||||
get current() {
|
||||
return node;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { derived, type Readable } from 'svelte/store';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import type { InternalNode } from '$lib/types';
|
||||
|
||||
/**
|
||||
* Hook to get an internal node by id.
|
||||
*
|
||||
* @public
|
||||
* @param id - the node id
|
||||
* @returns a readable with an internal node or undefined
|
||||
*/
|
||||
export function useInternalNode(id: string): Readable<InternalNode | undefined> {
|
||||
const { nodeLookup, nodes } = useStore();
|
||||
|
||||
return derived([nodeLookup, nodes], ([nodeLookup]) => nodeLookup.get(id));
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
areConnectionMapsEqual,
|
||||
type NodeConnection,
|
||||
type HandleType,
|
||||
type Connection,
|
||||
handleConnectionChange
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
type UseNodeConnectionsParams = {
|
||||
id?: string;
|
||||
handleType?: HandleType;
|
||||
handleId?: string;
|
||||
onConnect?: (connections: Connection[]) => void;
|
||||
onDisconnect?: (connections: Connection[]) => void;
|
||||
};
|
||||
|
||||
const initialConnections: NodeConnection[] = [];
|
||||
|
||||
/**
|
||||
* Hook to retrieve all edges connected to a node. Can be filtered by handle type and id.
|
||||
*
|
||||
* @public
|
||||
* @param param.id - node id - optional if called inside a custom node
|
||||
* @param param.handleType - filter by handle type 'source' or 'target'
|
||||
* @param param.handleId - filter by handle id (this is only needed if the node has multiple handles of the same type)
|
||||
* @param param.onConnect - gets called when a connection is established
|
||||
* @param param.onDisconnect - gets called when a connection is removed
|
||||
* @returns An array with connections
|
||||
*/
|
||||
export function useNodeConnections({
|
||||
id,
|
||||
handleType,
|
||||
handleId,
|
||||
onConnect,
|
||||
onDisconnect
|
||||
}: UseNodeConnectionsParams = {}) {
|
||||
const { edges, connectionLookup } = $derived(useStore());
|
||||
|
||||
const contextNodeId = getContext<string>('svelteflow__node_id');
|
||||
const nodeId = id ?? contextNodeId;
|
||||
|
||||
let prevConnections: Map<string, NodeConnection> = new Map();
|
||||
let connectionsArray: NodeConnection[] = initialConnections;
|
||||
|
||||
const connections = $derived.by(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
edges;
|
||||
const nextConnections =
|
||||
connectionLookup.get(
|
||||
`${nodeId}${handleType ? (handleId ? `-${handleType}-${handleId}` : `-${handleType}`) : ''}`
|
||||
) ?? new Map();
|
||||
if (!areConnectionMapsEqual(nextConnections, prevConnections)) {
|
||||
if (onConnect) handleConnectionChange(nextConnections, prevConnections, onConnect);
|
||||
if (onDisconnect) handleConnectionChange(prevConnections, nextConnections, onDisconnect);
|
||||
|
||||
prevConnections = nextConnections;
|
||||
connectionsArray = Array.from(nextConnections.values() || initialConnections);
|
||||
}
|
||||
return connectionsArray;
|
||||
});
|
||||
|
||||
return {
|
||||
get current() {
|
||||
return connections;
|
||||
}
|
||||
};
|
||||
}
|
||||
+18
-8
@@ -1,4 +1,3 @@
|
||||
import { derived, type Readable } from 'svelte/store';
|
||||
import { shallowNodeData } from '@xyflow/system';
|
||||
|
||||
import type { Node } from '$lib/types';
|
||||
@@ -9,21 +8,24 @@ import { useStore } from '$lib/store';
|
||||
*
|
||||
* @public
|
||||
* @param nodeId - The id (or ids) of the node to get the data from
|
||||
* @returns A readable store with an array of data objects
|
||||
* @returns An array of data objects
|
||||
*/
|
||||
export function useNodesData<NodeType extends Node = Node>(
|
||||
nodeId: string
|
||||
): Readable<Pick<NodeType, 'id' | 'data' | 'type'> | null>;
|
||||
): { current: Pick<NodeType, 'id' | 'data' | 'type'> | null };
|
||||
export function useNodesData<NodeType extends Node = Node>(
|
||||
nodeIds: string[]
|
||||
): Readable<Pick<NodeType, 'id' | 'data' | 'type'>[]>;
|
||||
): { current: Pick<NodeType, 'id' | 'data' | 'type'>[] };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function useNodesData(nodeIds: any): any {
|
||||
const { nodes, nodeLookup } = useStore();
|
||||
const { nodes, nodeLookup } = $derived(useStore());
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let prevNodesData: any[] = [];
|
||||
let initialRun = true;
|
||||
|
||||
return derived([nodes, nodeLookup], ([, nodeLookup], set) => {
|
||||
const nodeData = $derived.by(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
nodes;
|
||||
const nextNodesData = [];
|
||||
const isArrayOfIds = Array.isArray(nodeIds);
|
||||
const _nodeIds = isArrayOfIds ? nodeIds : [nodeIds];
|
||||
@@ -39,9 +41,17 @@ export function useNodesData(nodeIds: any): any {
|
||||
}
|
||||
}
|
||||
|
||||
if (!shallowNodeData(nextNodesData, prevNodesData)) {
|
||||
if (!shallowNodeData(nextNodesData, prevNodesData) || initialRun) {
|
||||
prevNodesData = nextNodesData;
|
||||
set(isArrayOfIds ? nextNodesData : nextNodesData[0] ?? null);
|
||||
initialRun = false;
|
||||
}
|
||||
|
||||
return isArrayOfIds ? prevNodesData : (prevNodesData[0] ?? null);
|
||||
});
|
||||
|
||||
return {
|
||||
get current() {
|
||||
return nodeData;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
/**
|
||||
* Hook for getting the current nodes from the store.
|
||||
*
|
||||
* @public
|
||||
* @returns store with an array of nodes
|
||||
*/
|
||||
export function useNodes() {
|
||||
const { nodes } = useStore();
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for getting the current edges from the store.
|
||||
*
|
||||
* @public
|
||||
* @returns store with an array of edges
|
||||
*/
|
||||
export function useEdges() {
|
||||
const { edges } = useStore();
|
||||
return edges;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useStore } from '$lib/store';
|
||||
import type { Edge, Node } from '$lib/types';
|
||||
import type { Viewport } from '@xyflow/system';
|
||||
|
||||
/**
|
||||
* Hook for getting the current nodes from the store.
|
||||
*
|
||||
* @public
|
||||
* @returns A reactive signal of the current nodes
|
||||
*/
|
||||
export function useNodes() {
|
||||
const store = $derived(useStore());
|
||||
return {
|
||||
get current() {
|
||||
return store.nodes;
|
||||
},
|
||||
set current(nodes) {
|
||||
store.nodes = nodes;
|
||||
},
|
||||
update(updateFn: (nodes: Node[]) => Node[]) {
|
||||
store.nodes = updateFn(store.nodes);
|
||||
},
|
||||
set(nodes: Node[]) {
|
||||
store.nodes = nodes;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for getting the current edges from the store.
|
||||
*
|
||||
* @public
|
||||
* @returns A reactive signal of the current edges
|
||||
*/
|
||||
export function useEdges() {
|
||||
const store = $derived(useStore());
|
||||
return {
|
||||
get current() {
|
||||
return store.edges;
|
||||
},
|
||||
set current(edges) {
|
||||
store.edges = edges;
|
||||
},
|
||||
update(updateFn: (edges: Edge[]) => Edge[]) {
|
||||
store.edges = updateFn(store.edges);
|
||||
},
|
||||
set(edges: Edge[]) {
|
||||
store.edges = edges;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for getting the current viewport from the store.
|
||||
*
|
||||
* @public
|
||||
* @returns A reactive signal of the current viewport
|
||||
*/
|
||||
export function useViewport() {
|
||||
const store = $derived(useStore());
|
||||
return {
|
||||
get current() {
|
||||
return store.viewport;
|
||||
},
|
||||
set current(viewport: Viewport) {
|
||||
store.viewport = viewport;
|
||||
},
|
||||
update(updateFn: (viewport: Viewport) => Viewport) {
|
||||
store.viewport = updateFn(store.viewport);
|
||||
},
|
||||
set(viewport: Viewport) {
|
||||
store.viewport = viewport;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { OnSelectionChange } from '$lib/types';
|
||||
import { useStore } from '$lib/hooks/useStore';
|
||||
|
||||
export function useOnSelectionChange(onselectionchange: OnSelectionChange) {
|
||||
const store = $derived(useStore());
|
||||
const symbol = Symbol();
|
||||
|
||||
$effect(() => {
|
||||
store.selectionChangeHandlers.set(symbol, onselectionchange);
|
||||
|
||||
return () => {
|
||||
store.selectionChangeHandlers.delete(symbol);
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { getContext } from 'svelte';
|
||||
import type { StoreContext, SvelteFlowStore } from '../store/types';
|
||||
|
||||
import { key } from '../store';
|
||||
import { derivedWarning } from './derivedWarning.svelte';
|
||||
import type { Node, Edge } from '$lib/types';
|
||||
|
||||
export function useStore<
|
||||
NodeType extends Node = Node,
|
||||
EdgeType extends Edge = Edge
|
||||
>(): SvelteFlowStore<NodeType, EdgeType> {
|
||||
const storeContext = getContext<StoreContext<NodeType, EdgeType>>(key);
|
||||
|
||||
if (!storeContext) {
|
||||
throw new Error(
|
||||
'To call useStore outside of <SvelteFlow /> you need to wrap your component in a <SvelteFlowProvider />'
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
derivedWarning('useStore');
|
||||
}
|
||||
|
||||
return storeContext.getStore();
|
||||
}
|
||||
+150
-156
@@ -1,4 +1,3 @@
|
||||
import { get, type Writable } from 'svelte/store';
|
||||
import {
|
||||
getOverlappingArea,
|
||||
isRectObject,
|
||||
@@ -22,16 +21,16 @@ import {
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import type { Edge, FitViewOptions, InternalNode, Node } from '$lib/types';
|
||||
import { isNode } from '$lib/utils';
|
||||
import { isEdge, isNode } from '$lib/utils';
|
||||
import { untrack } from 'svelte';
|
||||
|
||||
/**
|
||||
* Hook for accessing the ReactFlow instance.
|
||||
* Hook for accessing the SvelteFlow instance.
|
||||
*
|
||||
* @public
|
||||
*
|
||||
* @returns helper functions
|
||||
* @returns A set of helper functions
|
||||
*/
|
||||
export function useSvelteFlow(): {
|
||||
export function useSvelteFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(): {
|
||||
/**
|
||||
* Zooms viewport in by 1.2.
|
||||
*
|
||||
@@ -50,33 +49,33 @@ export function useSvelteFlow(): {
|
||||
* @param id - the node id
|
||||
* @returns the node or undefined if no node was found
|
||||
*/
|
||||
getInternalNode: (id: string) => InternalNode | undefined;
|
||||
getInternalNode: (id: string) => InternalNode<NodeType> | undefined;
|
||||
/**
|
||||
* Returns a node by id.
|
||||
*
|
||||
* @param id - the node id
|
||||
* @returns the node or undefined if no node was found
|
||||
*/
|
||||
getNode: (id: string) => Node | undefined;
|
||||
getNode: (id: string) => NodeType | undefined;
|
||||
/**
|
||||
* Returns nodes.
|
||||
*
|
||||
* @returns nodes array
|
||||
*/
|
||||
getNodes: (ids?: string[]) => Node[];
|
||||
getNodes: (ids?: string[]) => NodeType[];
|
||||
/**
|
||||
* Returns an edge by id.
|
||||
*
|
||||
* @param id - the edge id
|
||||
* @returns the edge or undefined if no edge was found
|
||||
*/
|
||||
getEdge: (id: string) => Edge | undefined;
|
||||
getEdge: (id: string) => EdgeType | undefined;
|
||||
/**
|
||||
* Returns edges.
|
||||
*
|
||||
* @returns edges array
|
||||
*/
|
||||
getEdges: (ids?: string[]) => Edge[];
|
||||
getEdges: (ids?: string[]) => EdgeType[];
|
||||
/**
|
||||
* Sets the current zoom level.
|
||||
*
|
||||
@@ -132,10 +131,10 @@ export function useSvelteFlow(): {
|
||||
* @returns an array of intersecting nodes
|
||||
*/
|
||||
getIntersectingNodes: (
|
||||
nodeOrRect: Node | { id: Node['id'] } | Rect,
|
||||
nodeOrRect: NodeType | { id: NodeType['id'] } | Rect,
|
||||
partially?: boolean,
|
||||
nodesToIntersect?: Node[]
|
||||
) => Node[];
|
||||
nodesToIntersect?: NodeType[]
|
||||
) => NodeType[];
|
||||
/**
|
||||
* Checks if the given node or rect intersects with the passed rect.
|
||||
*
|
||||
@@ -146,7 +145,7 @@ export function useSvelteFlow(): {
|
||||
* @returns true if the node or rect intersects with the given area
|
||||
*/
|
||||
isNodeIntersecting: (
|
||||
nodeOrRect: Node | { id: Node['id'] } | Rect,
|
||||
nodeOrRect: NodeType | { id: NodeType['id'] } | Rect,
|
||||
area: Rect,
|
||||
partially?: boolean
|
||||
) => boolean;
|
||||
@@ -169,9 +168,9 @@ export function useSvelteFlow(): {
|
||||
nodes,
|
||||
edges
|
||||
}: {
|
||||
nodes?: (Node | { id: Node['id'] })[];
|
||||
edges?: (Edge | { id: Edge['id'] })[];
|
||||
}) => Promise<{ deletedNodes: Node[]; deletedEdges: Edge[] }>;
|
||||
nodes?: (Partial<NodeType> & { id: string })[];
|
||||
edges?: (Partial<EdgeType> & { id: string })[];
|
||||
}) => Promise<{ deletedNodes: NodeType[]; deletedEdges: EdgeType[] }>;
|
||||
/**
|
||||
* Converts a screen / client position to a flow position.
|
||||
*
|
||||
@@ -196,7 +195,6 @@ export function useSvelteFlow(): {
|
||||
* const clientPosition = flowToScreenPosition({ x: node.position.x, y: node.position.y })
|
||||
*/
|
||||
flowToScreenPosition: (flowPosition: XYPosition) => XYPosition;
|
||||
viewport: Writable<Viewport>;
|
||||
/**
|
||||
* Updates a node.
|
||||
*
|
||||
@@ -207,9 +205,14 @@ export function useSvelteFlow(): {
|
||||
* @example
|
||||
* updateNode('node-1', (node) => ({ position: { x: node.position.x + 10, y: node.position.y } }));
|
||||
*/
|
||||
// updateNode: (
|
||||
// id: string,
|
||||
// nodeUpdate: Partial<NodeType> | ((node: NodeType) => Partial<NodeTyp>),
|
||||
// options?: { replace: boolean }
|
||||
// ) => void;
|
||||
updateNode: (
|
||||
id: string,
|
||||
nodeUpdate: Partial<Node> | ((node: Node) => Partial<Node>),
|
||||
nodeUpdate: Partial<NodeType> | ((node: NodeType) => Partial<NodeType>),
|
||||
options?: { replace: boolean }
|
||||
) => void;
|
||||
/**
|
||||
@@ -224,7 +227,7 @@ export function useSvelteFlow(): {
|
||||
*/
|
||||
updateNodeData: (
|
||||
id: string,
|
||||
dataUpdate: object | ((node: Node) => object),
|
||||
dataUpdate: Partial<NodeType['data']> | ((node: NodeType) => Partial<NodeType['data']>),
|
||||
options?: { replace: boolean }
|
||||
) => void;
|
||||
/**
|
||||
@@ -232,7 +235,22 @@ export function useSvelteFlow(): {
|
||||
*
|
||||
* @returns the nodes, edges and the viewport as a JSON object
|
||||
*/
|
||||
toObject: () => { nodes: Node[]; edges: Edge[]; viewport: Viewport };
|
||||
/**
|
||||
* Updates an edge.
|
||||
*
|
||||
* @param id - id of the edge to update
|
||||
* @param edgeUpdate - the edge update as an object or a function that receives the current edge and returns the edge update
|
||||
* @param options.replace - if true, the edge is replaced with the edge update, otherwise the changes get merged
|
||||
*
|
||||
* @example
|
||||
* updateNode('node-1', (node) => ({ position: { x: node.position.x + 10, y: node.position.y } }));
|
||||
*/
|
||||
updateEdge: (
|
||||
id: string,
|
||||
edgeUpdate: Partial<EdgeType> | ((edge: EdgeType) => Partial<EdgeType>),
|
||||
options?: { replace: boolean }
|
||||
) => void;
|
||||
toObject: () => { nodes: NodeType[]; edges: EdgeType[]; viewport: Viewport };
|
||||
/**
|
||||
* Returns the bounds of the given nodes or node ids.
|
||||
*
|
||||
@@ -240,7 +258,7 @@ export function useSvelteFlow(): {
|
||||
*
|
||||
* @returns the bounds of the given nodes
|
||||
*/
|
||||
getNodesBounds: (nodes: (Node | InternalNode | string)[]) => Rect;
|
||||
getNodesBounds: (nodes: (NodeType | InternalNode<NodeType> | string)[]) => Rect;
|
||||
/** Gets all connections for a given handle belonging to a specific node.
|
||||
*
|
||||
* @param type - handle type 'source' or 'target'
|
||||
@@ -258,120 +276,99 @@ export function useSvelteFlow(): {
|
||||
id?: string | null;
|
||||
}) => HandleConnection[];
|
||||
} {
|
||||
const {
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
fitView,
|
||||
onbeforedelete,
|
||||
snapGrid,
|
||||
viewport,
|
||||
width,
|
||||
height,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
panZoom,
|
||||
nodes,
|
||||
edges,
|
||||
domNode,
|
||||
nodeLookup,
|
||||
nodeOrigin,
|
||||
edgeLookup,
|
||||
connectionLookup
|
||||
} = useStore();
|
||||
const store = $derived(useStore<NodeType, EdgeType>());
|
||||
|
||||
const getNodeRect = (node: Node | { id: Node['id'] }): Rect | null => {
|
||||
const $nodeLookup = get(nodeLookup);
|
||||
const nodeToUse = isNode(node) ? node : $nodeLookup.get(node.id)!;
|
||||
const getNodeRect = (node: NodeType | { id: NodeType['id'] }): Rect | null => {
|
||||
const nodeToUse = isNode(node) ? node : store.nodeLookup.get(node.id)!;
|
||||
const position = nodeToUse.parentId
|
||||
? evaluateAbsolutePosition(
|
||||
nodeToUse.position,
|
||||
nodeToUse.measured,
|
||||
nodeToUse.parentId,
|
||||
$nodeLookup,
|
||||
get(nodeOrigin)
|
||||
store.nodeLookup,
|
||||
store.nodeOrigin
|
||||
)
|
||||
: nodeToUse.position;
|
||||
|
||||
const nodeWithPosition = {
|
||||
id: nodeToUse.id,
|
||||
...nodeToUse,
|
||||
position,
|
||||
width: nodeToUse.measured?.width ?? nodeToUse.width,
|
||||
height: nodeToUse.measured?.height ?? nodeToUse.height,
|
||||
data: nodeToUse.data
|
||||
height: nodeToUse.measured?.height ?? nodeToUse.height
|
||||
};
|
||||
|
||||
return nodeToRect(nodeWithPosition);
|
||||
};
|
||||
|
||||
const updateNode = (
|
||||
function updateNode(
|
||||
id: string,
|
||||
nodeUpdate: Partial<Node> | ((node: Node) => Partial<Node>),
|
||||
nodeUpdate: Partial<NodeType> | ((node: NodeType) => Partial<NodeType>),
|
||||
options: { replace: boolean } = { replace: false }
|
||||
) => {
|
||||
const node = get(nodeLookup).get(id)?.internals.userNode;
|
||||
) {
|
||||
store.nodes = untrack(() => store.nodes).map((node) => {
|
||||
if (node.id === id) {
|
||||
const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node) : nodeUpdate;
|
||||
return options?.replace && isNode<NodeType>(nextNode) ? nextNode : { ...node, ...nextNode };
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
return node;
|
||||
});
|
||||
}
|
||||
|
||||
const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node as Node) : nodeUpdate;
|
||||
function updateEdge(
|
||||
id: string,
|
||||
edgeUpdate: Partial<EdgeType> | ((edge: EdgeType) => Partial<EdgeType>),
|
||||
options: { replace: boolean } = { replace: false }
|
||||
) {
|
||||
store.edges = untrack(() => store.edges).map((edge) => {
|
||||
if (edge.id === id) {
|
||||
const nextEdge = typeof edgeUpdate === 'function' ? edgeUpdate(edge) : edgeUpdate;
|
||||
return options.replace && isEdge<EdgeType>(nextEdge) ? nextEdge : { ...edge, ...nextEdge };
|
||||
}
|
||||
|
||||
if (options.replace) {
|
||||
nodes.update((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id === id) {
|
||||
return isNode(nextNode) ? nextNode : { ...node, ...nextNode };
|
||||
}
|
||||
return edge;
|
||||
});
|
||||
}
|
||||
|
||||
return node;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
Object.assign(node, nextNode);
|
||||
nodes.update((nds) => nds);
|
||||
}
|
||||
};
|
||||
|
||||
const getInternalNode = (id: string) => get(nodeLookup).get(id);
|
||||
const getInternalNode = (id: string) => store.nodeLookup.get(id);
|
||||
|
||||
return {
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
zoomIn: store.zoomIn,
|
||||
zoomOut: store.zoomOut,
|
||||
getInternalNode,
|
||||
getNode: (id) => getInternalNode(id)?.internals.userNode,
|
||||
getNodes: (ids) => (ids === undefined ? get(nodes) : getElements(get(nodeLookup), ids)),
|
||||
getEdge: (id) => get(edgeLookup).get(id),
|
||||
getEdges: (ids) => (ids === undefined ? get(edges) : getElements(get(edgeLookup), ids)),
|
||||
getNodes: (ids) => (ids === undefined ? store.nodes : getElements(store.nodeLookup, ids)),
|
||||
getEdge: (id) => store.edgeLookup.get(id),
|
||||
getEdges: (ids) => (ids === undefined ? store.edges : getElements(store.edgeLookup, ids)),
|
||||
setZoom: (zoomLevel, options) => {
|
||||
const currentPanZoom = get(panZoom);
|
||||
return currentPanZoom
|
||||
? currentPanZoom.scaleTo(zoomLevel, { duration: options?.duration })
|
||||
const panZoom = store.panZoom;
|
||||
return panZoom
|
||||
? panZoom.scaleTo(zoomLevel, { duration: options?.duration })
|
||||
: Promise.resolve(false);
|
||||
},
|
||||
getZoom: () => get(viewport).zoom,
|
||||
getZoom: () => store.viewport.zoom,
|
||||
setViewport: async (nextViewport, options) => {
|
||||
const currentViewport = get(viewport);
|
||||
const currentPanZoom = get(panZoom);
|
||||
const currentViewport = store.viewport;
|
||||
|
||||
if (!currentPanZoom) {
|
||||
if (!store.panZoom) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
await currentPanZoom.setViewport(
|
||||
await store.panZoom.setViewport(
|
||||
{
|
||||
x: nextViewport.x ?? currentViewport.x,
|
||||
y: nextViewport.y ?? currentViewport.y,
|
||||
zoom: nextViewport.zoom ?? currentViewport.zoom
|
||||
},
|
||||
{ duration: options?.duration }
|
||||
options
|
||||
);
|
||||
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
getViewport: () => get(viewport),
|
||||
getViewport: () => $state.snapshot(store.viewport),
|
||||
setCenter: async (x, y, options) => {
|
||||
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : get(maxZoom);
|
||||
const currentPanZoom = get(panZoom);
|
||||
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : store.maxZoom;
|
||||
const currentPanZoom = store.panZoom;
|
||||
|
||||
if (!currentPanZoom) {
|
||||
return Promise.resolve(false);
|
||||
@@ -379,40 +376,44 @@ export function useSvelteFlow(): {
|
||||
|
||||
await currentPanZoom.setViewport(
|
||||
{
|
||||
x: get(width) / 2 - x * nextZoom,
|
||||
y: get(height) / 2 - y * nextZoom,
|
||||
x: store.width / 2 - x * nextZoom,
|
||||
y: store.height / 2 - y * nextZoom,
|
||||
zoom: nextZoom
|
||||
},
|
||||
{ duration: options?.duration }
|
||||
{ duration: options?.duration, ease: options?.ease, interpolate: options?.interpolate }
|
||||
);
|
||||
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
fitView,
|
||||
fitView: (options?: FitViewOptions) => {
|
||||
return store.fitView(options);
|
||||
},
|
||||
fitBounds: async (bounds: Rect, options?: FitBoundsOptions) => {
|
||||
const currentPanZoom = get(panZoom);
|
||||
|
||||
if (!currentPanZoom) {
|
||||
if (!store.panZoom) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
const viewport = getViewportForBounds(
|
||||
bounds,
|
||||
get(width),
|
||||
get(height),
|
||||
get(minZoom),
|
||||
get(maxZoom),
|
||||
store.width,
|
||||
store.height,
|
||||
store.minZoom,
|
||||
store.maxZoom,
|
||||
options?.padding ?? 0.1
|
||||
);
|
||||
|
||||
await currentPanZoom.setViewport(viewport, { duration: options?.duration });
|
||||
await store.panZoom.setViewport(viewport, {
|
||||
duration: options?.duration,
|
||||
ease: options?.ease,
|
||||
interpolate: options?.interpolate
|
||||
});
|
||||
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
getIntersectingNodes: (
|
||||
nodeOrRect: Node | { id: Node['id'] } | Rect,
|
||||
nodeOrRect: NodeType | { id: NodeType['id'] } | Rect,
|
||||
partially = true,
|
||||
nodesToIntersect?: Node[]
|
||||
nodesToIntersect?: NodeType[]
|
||||
) => {
|
||||
const isRect = isRectObject(nodeOrRect);
|
||||
const nodeRect = isRect ? nodeOrRect : getNodeRect(nodeOrRect);
|
||||
@@ -421,8 +422,8 @@ export function useSvelteFlow(): {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (nodesToIntersect || get(nodes)).filter((n) => {
|
||||
const internalNode = get(nodeLookup).get(n.id);
|
||||
return (nodesToIntersect || store.nodes).filter((n) => {
|
||||
const internalNode = store.nodeLookup.get(n.id);
|
||||
if (!internalNode || (!isRect && n.id === nodeOrRect.id)) {
|
||||
return false;
|
||||
}
|
||||
@@ -435,7 +436,7 @@ export function useSvelteFlow(): {
|
||||
});
|
||||
},
|
||||
isNodeIntersecting: (
|
||||
nodeOrRect: Node | { id: Node['id'] } | Rect,
|
||||
nodeOrRect: NodeType | { id: NodeType['id'] } | Rect,
|
||||
area: Rect,
|
||||
partially = true
|
||||
) => {
|
||||
@@ -452,23 +453,26 @@ export function useSvelteFlow(): {
|
||||
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
|
||||
},
|
||||
deleteElements: async ({ nodes: nodesToRemove = [], edges: edgesToRemove = [] }) => {
|
||||
const { nodes: matchingNodes, edges: matchingEdges } = await getElementsToRemove({
|
||||
const { nodes: matchingNodes, edges: matchingEdges } = await getElementsToRemove<
|
||||
NodeType,
|
||||
EdgeType
|
||||
>({
|
||||
nodesToRemove,
|
||||
edgesToRemove,
|
||||
nodes: get(nodes),
|
||||
edges: get(edges),
|
||||
onBeforeDelete: get(onbeforedelete)
|
||||
nodes: store.nodes,
|
||||
edges: store.edges,
|
||||
onBeforeDelete: store.onbeforedelete
|
||||
});
|
||||
|
||||
if (matchingNodes) {
|
||||
nodes.update((nds) =>
|
||||
nds.filter((node) => !matchingNodes.some(({ id }) => id === node.id))
|
||||
store.nodes = untrack(() => store.nodes).filter(
|
||||
(node) => !matchingNodes.some(({ id }) => id === node.id)
|
||||
);
|
||||
}
|
||||
|
||||
if (matchingEdges) {
|
||||
edges.update((eds) =>
|
||||
eds.filter((edge) => !matchingEdges.some(({ id }) => id === edge.id))
|
||||
store.edges = untrack(() => store.edges).filter(
|
||||
(edge) => !matchingEdges.some(({ id }) => id === edge.id)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -481,15 +485,13 @@ export function useSvelteFlow(): {
|
||||
position: XYPosition,
|
||||
options: { snapToGrid: boolean } = { snapToGrid: true }
|
||||
) => {
|
||||
const _domNode = get(domNode);
|
||||
|
||||
if (!_domNode) {
|
||||
if (!store.domNode) {
|
||||
return position;
|
||||
}
|
||||
|
||||
const _snapGrid = options.snapToGrid ? get(snapGrid) : false;
|
||||
const { x, y, zoom } = get(viewport);
|
||||
const { x: domX, y: domY } = _domNode.getBoundingClientRect();
|
||||
const _snapGrid = options.snapToGrid ? store.snapGrid : false;
|
||||
const { x, y, zoom } = store.viewport;
|
||||
const { x: domX, y: domY } = store.domNode.getBoundingClientRect();
|
||||
const correctedPosition = {
|
||||
x: position.x - domX,
|
||||
y: position.y - domY
|
||||
@@ -508,14 +510,12 @@ export function useSvelteFlow(): {
|
||||
* @returns
|
||||
*/
|
||||
flowToScreenPosition: (position: XYPosition) => {
|
||||
const _domNode = get(domNode);
|
||||
|
||||
if (!_domNode) {
|
||||
if (!store.domNode) {
|
||||
return position;
|
||||
}
|
||||
|
||||
const { x, y, zoom } = get(viewport);
|
||||
const { x: domX, y: domY } = _domNode.getBoundingClientRect();
|
||||
const { x, y, zoom } = store.viewport;
|
||||
const { x: domX, y: domY } = store.domNode.getBoundingClientRect();
|
||||
const rendererPosition = rendererPointToPoint(position, [x, y, zoom]);
|
||||
|
||||
return {
|
||||
@@ -525,49 +525,43 @@ export function useSvelteFlow(): {
|
||||
},
|
||||
|
||||
toObject: () => {
|
||||
return {
|
||||
nodes: get(nodes).map((node) => ({
|
||||
...node,
|
||||
// we want to make sure that changes to the nodes object that gets returned by toObject
|
||||
// do not affect the nodes object
|
||||
position: { ...node.position },
|
||||
data: { ...node.data }
|
||||
})),
|
||||
edges: get(edges).map((edge) => ({ ...edge })),
|
||||
viewport: { ...get(viewport) }
|
||||
};
|
||||
return structuredClone({
|
||||
nodes: [...store.nodes],
|
||||
edges: [...store.edges],
|
||||
viewport: { ...store.viewport }
|
||||
});
|
||||
},
|
||||
updateNode,
|
||||
updateNodeData: (id, dataUpdate, options) => {
|
||||
const node = get(nodeLookup).get(id)?.internals.userNode;
|
||||
const node = store.nodeLookup.get(id)?.internals.userNode;
|
||||
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextData = typeof dataUpdate === 'function' ? dataUpdate(node) : dataUpdate;
|
||||
|
||||
node.data = options?.replace ? nextData : { ...node.data, ...nextData };
|
||||
|
||||
nodes.update((nds) => nds);
|
||||
updateNode(id, (node) => ({
|
||||
...node,
|
||||
data: options?.replace ? nextData : { ...node.data, ...nextData }
|
||||
}));
|
||||
},
|
||||
updateEdge,
|
||||
getNodesBounds: (nodes) => {
|
||||
const _nodeLookup = get(nodeLookup);
|
||||
const _nodeOrigin = get(nodeOrigin);
|
||||
|
||||
return getNodesBounds(nodes, { nodeLookup: _nodeLookup, nodeOrigin: _nodeOrigin });
|
||||
return getNodesBounds(nodes, { nodeLookup: store.nodeLookup, nodeOrigin: store.nodeOrigin });
|
||||
},
|
||||
getHandleConnections: ({ type, id, nodeId }) =>
|
||||
Array.from(
|
||||
get(connectionLookup)
|
||||
.get(`${nodeId}-${type}-${id ?? null}`)
|
||||
?.values() ?? []
|
||||
),
|
||||
viewport
|
||||
Array.from(store.connectionLookup.get(`${nodeId}-${type}-${id ?? null}`)?.values() ?? [])
|
||||
};
|
||||
}
|
||||
function getElements(lookup: Map<string, InternalNode>, ids: string[]): Node[];
|
||||
function getElements(lookup: Map<string, Edge>, ids: string[]): Edge[];
|
||||
|
||||
function getElements<NodeType extends Node = Node>(
|
||||
lookup: Map<string, InternalNode<NodeType>>,
|
||||
ids: string[]
|
||||
): NodeType[];
|
||||
function getElements<EdgeType extends Edge = Edge>(
|
||||
lookup: Map<string, EdgeType>,
|
||||
ids: string[]
|
||||
): EdgeType[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function getElements(lookup: Map<string, any>, ids: string[]): any[] {
|
||||
const result = [];
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useStore } from '$lib/store';
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
/**
|
||||
* When you programmatically add or remove handles to a node or update a node's
|
||||
* handle position, you need to let Svelte Flow know about it using this hook. This
|
||||
* will update the internal dimensions of the node and properly reposition handles
|
||||
* on the canvas if necessary.
|
||||
*
|
||||
* @public
|
||||
* @returns A function for telling Svelte Flow to update the internal state of one or more nodes
|
||||
* that you have changed programmatically.
|
||||
*/
|
||||
export function useUpdateNodeInternals(): (nodeId?: string | string[]) => void {
|
||||
const { domNode, updateNodeInternals } = $derived(useStore());
|
||||
const nodeId = getContext('svelteflow__node_id') as string | undefined;
|
||||
|
||||
// @todo: do we want to add this to system?
|
||||
const updateInternals = (id?: string | string[]) => {
|
||||
if (!id && !nodeId) {
|
||||
throw new Error('When using outside of a node, you must provide an id.');
|
||||
}
|
||||
const updateIds = id ? (Array.isArray(id) ? id : [id]) : [nodeId];
|
||||
const updates = new Map();
|
||||
|
||||
updateIds.forEach((updateId) => {
|
||||
const nodeElement = domNode?.querySelector(
|
||||
`.svelte-flow__node[data-id="${updateId}"]`
|
||||
) as HTMLDivElement;
|
||||
|
||||
if (nodeElement) {
|
||||
updates.set(updateId, { id: updateId, nodeElement, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
requestAnimationFrame(() => updateNodeInternals(updates));
|
||||
};
|
||||
|
||||
return updateInternals;
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { get } from 'svelte/store';
|
||||
import type { UpdateNodeInternals } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
/**
|
||||
* Hook for updating node internals.
|
||||
*
|
||||
* @public
|
||||
* @returns function for updating node internals
|
||||
*/
|
||||
export function useUpdateNodeInternals(): UpdateNodeInternals {
|
||||
const { domNode, updateNodeInternals } = useStore();
|
||||
|
||||
// @todo: do we want to add this to system?
|
||||
const updateInternals = (id: string | string[]) => {
|
||||
const updateIds = Array.isArray(id) ? id : [id];
|
||||
const updates = new Map();
|
||||
|
||||
updateIds.forEach((updateId) => {
|
||||
const nodeElement = get(domNode)?.querySelector(
|
||||
`.svelte-flow__node[data-id="${updateId}"]`
|
||||
) as HTMLDivElement;
|
||||
|
||||
if (nodeElement) {
|
||||
updates.set(updateId, { id: updateId, nodeElement, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
requestAnimationFrame(() => updateNodeInternals(updates));
|
||||
};
|
||||
|
||||
return updateInternals;
|
||||
}
|
||||
@@ -5,12 +5,17 @@ export * from '$lib/container/SvelteFlow/types';
|
||||
// components
|
||||
export * from '$lib/container/Panel';
|
||||
export * from '$lib/components/SvelteFlowProvider';
|
||||
export * from '$lib/components/EdgeLabelRenderer';
|
||||
export * from '$lib/components/ViewportPortal';
|
||||
export * from '$lib/components/BaseEdge';
|
||||
export { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '$lib/components/edges';
|
||||
export {
|
||||
BezierEdge,
|
||||
StepEdge,
|
||||
SmoothStepEdge,
|
||||
StraightEdge,
|
||||
BaseEdge
|
||||
} from '$lib/components/edges';
|
||||
export * from '$lib/components/Handle';
|
||||
export * from '$lib/components/EdgeLabel';
|
||||
export * from '$lib/components/EdgeReconnectAnchor';
|
||||
|
||||
// plugins
|
||||
export * from '$lib/plugins/Controls';
|
||||
@@ -26,14 +31,18 @@ export { useStore } from '$lib/store';
|
||||
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';
|
||||
export * from '$lib/hooks/useHandleConnections';
|
||||
export * from '$lib/hooks/useNodesData';
|
||||
export * from '$lib/hooks/useInternalNode';
|
||||
export { useInitialized, useNodesInitialized } from '$lib/hooks/useInitialized';
|
||||
export * from '$lib/hooks/useSvelteFlow.svelte';
|
||||
export * from '$lib/hooks/useUpdateNodeInternals.svelte';
|
||||
export * from '$lib/hooks/useConnection.svelte';
|
||||
export * from '$lib/hooks/useNodesEdgesViewport.svelte';
|
||||
export * from '$lib/hooks/useNodeConnections.svelte';
|
||||
export * from '$lib/hooks/useNodesData.svelte';
|
||||
export * from '$lib/hooks/useInternalNode.svelte';
|
||||
export * from '$lib/hooks/useInitialized.svelte';
|
||||
export * from '$lib/hooks/useOnSelectionChange.svelte';
|
||||
|
||||
//actions
|
||||
export * from '$lib/actions/portal';
|
||||
|
||||
// types
|
||||
export type {
|
||||
@@ -46,16 +55,10 @@ export type {
|
||||
EdgeTypes,
|
||||
DefaultEdgeOptions
|
||||
} from '$lib/types/edges';
|
||||
export type { HandleProps, FitViewOptions } from '$lib/types/general';
|
||||
export type {
|
||||
Node,
|
||||
NodeTypes,
|
||||
DefaultNodeOptions,
|
||||
BuiltInNode,
|
||||
NodeProps,
|
||||
InternalNode
|
||||
} from '$lib/types/nodes';
|
||||
export type { FitViewOptions, OnBeforeDelete } from '$lib/types/general';
|
||||
export type { Node, NodeTypes, BuiltInNode, NodeProps, InternalNode } from '$lib/types/nodes';
|
||||
export type { SvelteFlowStore } from '$lib/store/types';
|
||||
export * from '$lib/types/events';
|
||||
|
||||
// system types
|
||||
export {
|
||||
@@ -104,11 +107,12 @@ export {
|
||||
type OnResizeEnd,
|
||||
type ControlPosition,
|
||||
type ControlLinePosition,
|
||||
type ResizeControlVariant,
|
||||
ResizeControlVariant,
|
||||
type ResizeParams,
|
||||
type ResizeParamsWithDirection,
|
||||
type ResizeDragEvent,
|
||||
type IsValidConnection
|
||||
type IsValidConnection,
|
||||
type NodeConnection
|
||||
} from '@xyflow/system';
|
||||
|
||||
// system utils
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script labg="ts" context="module">
|
||||
<script lang="ts" module>
|
||||
const defaultSize = {
|
||||
[BackgroundVariant.Dots]: 1,
|
||||
[BackgroundVariant.Lines]: 1,
|
||||
@@ -7,51 +7,55 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import cc from 'classcat';
|
||||
|
||||
import DotPattern from './DotPattern.svelte';
|
||||
import LinePattern from './LinePattern.svelte';
|
||||
import { useStore } from '$lib/store';
|
||||
import { BackgroundVariant, type BackgroundProps } from './types';
|
||||
|
||||
type $$Props = BackgroundProps;
|
||||
import DotPattern from './DotPattern.svelte';
|
||||
import LinePattern from './LinePattern.svelte';
|
||||
|
||||
export let id: $$Props['id'] = undefined;
|
||||
export let variant: $$Props['variant'] = BackgroundVariant.Dots;
|
||||
export let gap: $$Props['gap'] = 20;
|
||||
export let size: $$Props['size'] = 1;
|
||||
export let lineWidth: $$Props['lineWidth'] = 1;
|
||||
export let bgColor: $$Props['bgColor'] = undefined;
|
||||
export let patternColor: $$Props['patternColor'] = undefined;
|
||||
export let patternClass: $$Props['patternClass'] = undefined;
|
||||
let className: $$Props['class'] = '';
|
||||
export { className as class };
|
||||
let {
|
||||
id,
|
||||
variant = BackgroundVariant.Dots,
|
||||
gap = 20,
|
||||
size,
|
||||
lineWidth = 1,
|
||||
bgColor,
|
||||
patternColor,
|
||||
patternClass,
|
||||
class: className
|
||||
}: BackgroundProps = $props();
|
||||
|
||||
const { viewport, flowId } = useStore();
|
||||
const patternSize = size || defaultSize[variant!];
|
||||
const isDots = variant === BackgroundVariant.Dots;
|
||||
const isCross = variant === BackgroundVariant.Cross;
|
||||
const gapXY: number[] = Array.isArray(gap!) ? gap! : [gap!, gap!];
|
||||
let store = $derived(useStore());
|
||||
|
||||
$: patternId = `background-pattern-${$flowId}-${id ? id : ''}`;
|
||||
$: scaledGap = [gapXY[0] * $viewport.zoom || 1, gapXY[1] * $viewport.zoom || 1];
|
||||
$: scaledSize = patternSize * $viewport.zoom;
|
||||
$: patternDimensions = (isCross ? [scaledSize, scaledSize] : scaledGap) as [number, number];
|
||||
$: patternOffset = isDots
|
||||
? [scaledSize / 2, scaledSize / 2]
|
||||
: [patternDimensions[0] / 2, patternDimensions[1] / 2];
|
||||
let isDots = $derived(variant === BackgroundVariant.Dots);
|
||||
let isCross = $derived(variant === BackgroundVariant.Cross);
|
||||
let gapXY: number[] = $derived(Array.isArray(gap) ? gap : [gap, gap]);
|
||||
|
||||
let patternId = $derived(`background-pattern-${store.flowId}-${id ?? ''}`);
|
||||
let scaledGap = $derived([
|
||||
gapXY[0] * store.viewport.zoom || 1,
|
||||
gapXY[1] * store.viewport.zoom || 1
|
||||
]);
|
||||
let scaledSize = $derived((size ?? defaultSize[variant]) * store.viewport.zoom);
|
||||
|
||||
let patternDimensions = $derived(
|
||||
(isCross ? [scaledSize, scaledSize] : scaledGap) as [number, number]
|
||||
);
|
||||
let patternOffset = $derived(
|
||||
isDots ? [scaledSize / 2, scaledSize / 2] : [patternDimensions[0] / 2, patternDimensions[1] / 2]
|
||||
);
|
||||
</script>
|
||||
|
||||
<svg
|
||||
class={cc(['svelte-flow__background', className])}
|
||||
class={['svelte-flow__background', 'svelte-flow__container', className]}
|
||||
data-testid="svelte-flow__background"
|
||||
style:--xy-background-color-props={bgColor}
|
||||
style:--xy-background-pattern-color-props={patternColor}
|
||||
>
|
||||
<pattern
|
||||
id={patternId}
|
||||
x={$viewport.x % scaledGap[0]}
|
||||
y={$viewport.y % scaledGap[1]}
|
||||
x={store.viewport.x % scaledGap[0]}
|
||||
y={store.viewport.y % scaledGap[1]}
|
||||
width={scaledGap[0]}
|
||||
height={scaledGap[1]}
|
||||
patternUnits="userSpaceOnUse"
|
||||
@@ -65,13 +69,3 @@
|
||||
</pattern>
|
||||
<rect x="0" y="0" width="100%" height="100%" fill={`url(#${patternId})`} />
|
||||
</svg>
|
||||
|
||||
<style>
|
||||
.svelte-flow__background {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
<script lang="ts">
|
||||
import cc from 'classcat';
|
||||
import type { ClassValue } from 'svelte/elements';
|
||||
|
||||
export let radius = 5;
|
||||
let className: string = '';
|
||||
export { className as class };
|
||||
let { radius, class: className }: { radius: number; class?: ClassValue } = $props();
|
||||
</script>
|
||||
|
||||
<circle
|
||||
cx={radius}
|
||||
cy={radius}
|
||||
r={radius}
|
||||
class={cc(['svelte-flow__background-pattern', 'dots', className])}
|
||||
class={['svelte-flow__background-pattern', 'dots', className]}
|
||||
/>
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
<script lang="ts">
|
||||
import cc from 'classcat';
|
||||
import type { ClassValue } from 'svelte/elements';
|
||||
import type { BackgroundVariant } from './types';
|
||||
|
||||
export let lineWidth = 1;
|
||||
export let dimensions: [number, number];
|
||||
export let variant: BackgroundVariant | undefined = undefined;
|
||||
let className: string = '';
|
||||
export { className as class };
|
||||
let {
|
||||
lineWidth,
|
||||
dimensions,
|
||||
variant,
|
||||
class: className
|
||||
}: {
|
||||
lineWidth: number;
|
||||
dimensions: [number, number];
|
||||
variant: BackgroundVariant;
|
||||
class?: ClassValue;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<path
|
||||
stroke-width={lineWidth}
|
||||
d={`M${dimensions[0] / 2} 0 V${dimensions[1]} M0 ${dimensions[1] / 2} H${dimensions[0]}`}
|
||||
class={cc(['svelte-flow__background-pattern', variant, className])}
|
||||
class={['svelte-flow__background-pattern', variant, className]}
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { ClassValue } from 'svelte/elements';
|
||||
|
||||
export enum BackgroundVariant {
|
||||
Lines = 'lines',
|
||||
Dots = 'dots',
|
||||
@@ -5,6 +7,7 @@ export enum BackgroundVariant {
|
||||
}
|
||||
|
||||
export type BackgroundProps = {
|
||||
/** When multiple backgrounds are present on the page, each one should have a unique id. */
|
||||
id?: string;
|
||||
/** Color of the background */
|
||||
bgColor?: string;
|
||||
@@ -13,14 +16,27 @@ export type BackgroundProps = {
|
||||
/** Class applied to the pattern */
|
||||
patternClass?: string;
|
||||
/** Class applied to the container */
|
||||
class?: string;
|
||||
/** Gap between repetitions of the pattern */
|
||||
class?: ClassValue;
|
||||
/**
|
||||
* The gap between patterns. Passing in a tuple allows you to control the x and y gap
|
||||
* independently.
|
||||
* @default 20
|
||||
*/
|
||||
gap?: number | [number, number];
|
||||
/** Size of a single pattern element */
|
||||
/**
|
||||
* The radius of each dot or the size of each rectangle if `BackgroundVariant.Dots` or
|
||||
* `BackgroundVariant.Cross` is used. This defaults to 1 or 6 respectively, or ignored if
|
||||
* `BackgroundVariant.Lines` is used.
|
||||
*/
|
||||
size?: number;
|
||||
/** Line width of the Line pattern */
|
||||
/**
|
||||
* The stroke thickness used when drawing the pattern.
|
||||
* @default 1
|
||||
*/
|
||||
lineWidth?: number;
|
||||
/** Variant of the pattern
|
||||
/**
|
||||
* Variant of the pattern.
|
||||
* @default BackgroundVariant.Dots
|
||||
* @example BackgroundVariant.Lines, BackgroundVariant.Dots, BackgroundVariant.Cross
|
||||
* 'lines', 'dots', 'cross'
|
||||
*/
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
<script lang="ts">
|
||||
import cc from 'classcat';
|
||||
|
||||
import type { ControlButtonProps } from './types';
|
||||
|
||||
type $$Props = ControlButtonProps;
|
||||
|
||||
let className: $$Props['class'] = undefined;
|
||||
let bgColor: $$Props['bgColor'] = undefined;
|
||||
let bgColorHover: $$Props['bgColorHover'] = undefined;
|
||||
let color: $$Props['color'] = undefined;
|
||||
let colorHover: $$Props['colorHover'] = undefined;
|
||||
let borderColor: $$Props['borderColor'] = undefined;
|
||||
|
||||
export { className as class };
|
||||
let {
|
||||
class: className,
|
||||
bgColor,
|
||||
bgColorHover,
|
||||
color,
|
||||
colorHover,
|
||||
borderColor,
|
||||
onclick,
|
||||
children,
|
||||
...restProps
|
||||
}: ControlButtonProps = $props();
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
on:click
|
||||
class={cc(['svelte-flow__controls-button', className])}
|
||||
{onclick}
|
||||
class={['svelte-flow__controls-button', className]}
|
||||
style:--xy-controls-button-background-color-props={bgColor}
|
||||
style:--xy-controls-button-background-color-hover-props={bgColorHover}
|
||||
style:--xy-controls-button-color-props={color}
|
||||
style:--xy-controls-button-color-hover-props={colorHover}
|
||||
style:--xy-controls-button-border-color-props={borderColor}
|
||||
{...$$restProps}
|
||||
{...restProps}
|
||||
>
|
||||
<slot class="button-svg" />
|
||||
{@render children?.()}
|
||||
</button>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script lang="ts">
|
||||
import cc from 'classcat';
|
||||
|
||||
import Panel from '$lib/container/Panel/Panel.svelte';
|
||||
import { useStore } from '$lib/store';
|
||||
import Panel from '$lib/container/Panel/Panel.svelte';
|
||||
import ControlButton from './ControlButton.svelte';
|
||||
import PlusIcon from './Icons/Plus.svelte';
|
||||
import MinusIcon from './Icons/Minus.svelte';
|
||||
@@ -12,36 +10,28 @@
|
||||
|
||||
import type { ControlsProps } from './types';
|
||||
|
||||
type $$Props = ControlsProps;
|
||||
let {
|
||||
position = 'bottom-left',
|
||||
orientation = 'vertical',
|
||||
showZoom = true,
|
||||
showFitView = true,
|
||||
showLock = true,
|
||||
style,
|
||||
class: className,
|
||||
buttonBgColor,
|
||||
buttonBgColorHover,
|
||||
buttonColor,
|
||||
buttonColorHover,
|
||||
buttonBorderColor,
|
||||
'aria-label': ariaLabel,
|
||||
fitViewOptions,
|
||||
children,
|
||||
before,
|
||||
after,
|
||||
...rest
|
||||
}: ControlsProps = $props();
|
||||
|
||||
export let position: $$Props['position'] = 'bottom-left';
|
||||
export let showZoom: $$Props['showZoom'] = true;
|
||||
export let showFitView: $$Props['showFitView'] = true;
|
||||
export let showLock: $$Props['showLock'] = true;
|
||||
export let buttonBgColor: $$Props['buttonBgColor'] = undefined;
|
||||
export let buttonBgColorHover: $$Props['buttonBgColorHover'] = undefined;
|
||||
export let buttonColor: $$Props['buttonColor'] = undefined;
|
||||
export let buttonColorHover: $$Props['buttonColorHover'] = undefined;
|
||||
export let buttonBorderColor: $$Props['buttonColorHover'] = undefined;
|
||||
export let ariaLabel: $$Props['aria-label'] = undefined;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let orientation: $$Props['orientation'] = 'vertical';
|
||||
export let fitViewOptions: $$Props['fitViewOptions'] = undefined;
|
||||
|
||||
let className: $$Props['class'] = '';
|
||||
export { className as class };
|
||||
|
||||
const {
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
fitView,
|
||||
viewport,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
nodesDraggable,
|
||||
nodesConnectable,
|
||||
elementsSelectable
|
||||
} = useStore();
|
||||
let store = $derived(useStore());
|
||||
|
||||
const buttonProps = {
|
||||
bgColor: buttonBgColor,
|
||||
@@ -51,44 +41,47 @@
|
||||
borderColor: buttonBorderColor
|
||||
};
|
||||
|
||||
$: isInteractive = $nodesDraggable || $nodesConnectable || $elementsSelectable;
|
||||
$: minZoomReached = $viewport.zoom <= $minZoom;
|
||||
$: maxZoomReached = $viewport.zoom >= $maxZoom;
|
||||
let isInteractive = $derived(
|
||||
store.nodesDraggable || store.nodesConnectable || store.elementsSelectable
|
||||
);
|
||||
let minZoomReached = $derived(store.viewport.zoom <= store.minZoom);
|
||||
let maxZoomReached = $derived(store.viewport.zoom >= store.maxZoom);
|
||||
let orientationClass = $derived(orientation === 'horizontal' ? 'horizontal' : 'vertical');
|
||||
|
||||
const onZoomInHandler = () => {
|
||||
zoomIn();
|
||||
store.zoomIn();
|
||||
};
|
||||
|
||||
const onZoomOutHandler = () => {
|
||||
zoomOut();
|
||||
store.zoomOut();
|
||||
};
|
||||
|
||||
const onFitViewHandler = () => {
|
||||
fitView(fitViewOptions);
|
||||
store.fitView(fitViewOptions);
|
||||
};
|
||||
|
||||
const onToggleInteractivity = () => {
|
||||
isInteractive = !isInteractive;
|
||||
|
||||
nodesDraggable.set(isInteractive);
|
||||
nodesConnectable.set(isInteractive);
|
||||
elementsSelectable.set(isInteractive);
|
||||
let interactive = !isInteractive;
|
||||
store.nodesDraggable = interactive;
|
||||
store.nodesConnectable = interactive;
|
||||
store.elementsSelectable = interactive;
|
||||
};
|
||||
|
||||
$: orientationClass = orientation === 'horizontal' ? 'horizontal' : 'vertical';
|
||||
</script>
|
||||
|
||||
<Panel
|
||||
class={cc(['svelte-flow__controls', orientationClass, className])}
|
||||
class={['svelte-flow__controls', orientationClass, className]}
|
||||
{position}
|
||||
data-testid="svelte-flow__controls"
|
||||
aria-label={ariaLabel ?? 'Svelte Flow controls'}
|
||||
{style}
|
||||
{...rest}
|
||||
>
|
||||
<slot name="before" />
|
||||
{#if before}
|
||||
{@render before()}
|
||||
{/if}
|
||||
{#if showZoom}
|
||||
<ControlButton
|
||||
on:click={onZoomInHandler}
|
||||
onclick={onZoomInHandler}
|
||||
class="svelte-flow__controls-zoomin"
|
||||
title="zoom in"
|
||||
aria-label="zoom in"
|
||||
@@ -98,7 +91,7 @@
|
||||
<PlusIcon />
|
||||
</ControlButton>
|
||||
<ControlButton
|
||||
on:click={onZoomOutHandler}
|
||||
onclick={onZoomOutHandler}
|
||||
class="svelte-flow__controls-zoomout"
|
||||
title="zoom out"
|
||||
aria-label="zoom out"
|
||||
@@ -111,7 +104,7 @@
|
||||
{#if showFitView}
|
||||
<ControlButton
|
||||
class="svelte-flow__controls-fitview"
|
||||
on:click={onFitViewHandler}
|
||||
onclick={onFitViewHandler}
|
||||
title="fit view"
|
||||
aria-label="fit view"
|
||||
{...buttonProps}
|
||||
@@ -122,7 +115,7 @@
|
||||
{#if showLock}
|
||||
<ControlButton
|
||||
class="svelte-flow__controls-interactive"
|
||||
on:click={onToggleInteractivity}
|
||||
onclick={onToggleInteractivity}
|
||||
title="toggle interactivity"
|
||||
aria-label="toggle interactivity"
|
||||
{...buttonProps}
|
||||
@@ -130,6 +123,10 @@
|
||||
{#if isInteractive}<UnlockIcon />{:else}<LockIcon />{/if}
|
||||
</ControlButton>
|
||||
{/if}
|
||||
<slot />
|
||||
<slot name="after" />
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{/if}
|
||||
{#if after}
|
||||
{@render after()}
|
||||
{/if}
|
||||
</Panel>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { HTMLButtonAttributes } from 'svelte/elements';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { ClassValue, HTMLAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||
import type { PanelPosition } from '@xyflow/system';
|
||||
|
||||
import type { FitViewOptions } from '$lib/types';
|
||||
@@ -19,18 +20,23 @@ export type ControlsProps = {
|
||||
buttonBgColorHover?: string;
|
||||
buttonColor?: string;
|
||||
buttonColorHover?: string;
|
||||
buttonBorderColor?: string;
|
||||
'aria-label'?: string;
|
||||
style?: string;
|
||||
class?: string;
|
||||
class?: ClassValue;
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
children?: Snippet;
|
||||
before?: Snippet;
|
||||
after?: Snippet;
|
||||
fitViewOptions?: FitViewOptions;
|
||||
};
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export type ControlButtonProps = HTMLButtonAttributes & {
|
||||
class?: string;
|
||||
class?: ClassValue;
|
||||
bgColor?: string;
|
||||
bgColorHover?: string;
|
||||
color?: string;
|
||||
colorHover?: string;
|
||||
borderColor?: string;
|
||||
children?: Snippet;
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user