feat(svelte): edge marker support

This commit is contained in:
moklick
2023-05-15 16:15:42 +02:00
parent 24400d259b
commit 425f625020
22 changed files with 236 additions and 60 deletions
@@ -1,15 +1,11 @@
import { memo, useCallback } from 'react';
import type { EdgeMarker } from '@reactflow/system';
import { getMarkerId } from '@reactflow/utils';
import type { MarkerProps } from '@reactflow/system';
import { createMarkerIds } from '@reactflow/utils';
import { useStore } from '../../hooks/useStore';
import { useMarkerSymbol } from './MarkerSymbols';
import type { ReactFlowState } from '../../types';
type MarkerProps = EdgeMarker & {
id: string;
};
type MarkerDefinitionsProps = {
defaultColor: string;
rfId?: string;
@@ -51,22 +47,9 @@ const Marker = ({
const markerSelector =
({ defaultColor, rfId }: { defaultColor: string; rfId?: string }) =>
(s: ReactFlowState) => {
const ids: string[] = [];
const markers = createMarkerIds(s.edges, { id: rfId, defaultColor });
return s.edges
.reduce<MarkerProps[]>((markers, edge) => {
[edge.markerStart, edge.markerEnd].forEach((marker) => {
if (marker && typeof marker === 'object') {
const markerId = getMarkerId(marker, rfId);
if (!ids.includes(markerId)) {
markers.push({ id: markerId, color: marker.color || defaultColor, ...marker });
ids.push(markerId);
}
}
});
return markers;
}, [])
.sort((a, b) => a.id.localeCompare(b.id));
return markers;
};
// when you have multiple flows on a page and you hide the first one, the other ones have no markers anymore
@@ -4,14 +4,26 @@
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 markerStart: $$Props['markerStart'] = undefined;
export let markerEnd: $$Props['markerEnd'] = undefined;
export let style: $$Props['style'] = undefined;
export let interactionWidth: $$Props['interactionWidth'] = 20;
</script>
<path d={path} fill="none" class="svelte-flow__edge-path" />
<path
d={path}
id={id}
fill="none"
class="svelte-flow__edge-path"
marker-start={markerStart}
marker-end={markerEnd}
style={style}
/>
{#if interactionWidth}
<path
@@ -1,7 +1,10 @@
import type { EdgeProps } from '$lib/types';
export type BaseEdgeProps = Pick<EdgeProps, 'interactionWidth' | 'label'> & {
export type BaseEdgeProps = Pick<EdgeProps, 'interactionWidth' | 'label' | 'style'> & {
id?: string;
path: string;
labelX?: number;
labelY?: number;
markerStart?: string;
markerEnd?: string;
};
@@ -1,6 +1,7 @@
<script lang="ts">
import { createEventDispatcher, type SvelteComponentTyped } from 'svelte';
import { Position } from '@reactflow/system';
import { getMarkerId } from '@reactflow/utils';
import { useStore } from '$lib/store';
import BezierEdge from '$lib/components/edges/BezierEdge.svelte';
@@ -16,20 +17,28 @@
export let sourceY: $$Props['sourceY'] = 0;
export let targetX: $$Props['targetX'] = 0;
export let targetY: $$Props['targetY'] = 0;
// @ todo: support edge updates
// export let sourceHandleId: $$Props['sourceHandleId'] = undefined;
// export let targetHandleId: $$Props['targetHandleId'] = undefined;
export let data: $$Props['data'] = {};
export let style: $$Props['style'] = undefined;
export let sourcePosition: $$Props['sourcePosition'] = Position.Bottom;
export let targetPosition: $$Props['targetPosition'] = Position.Top;
export let animated: $$Props['animated'] = false;
export let selected: $$Props['selected'] = false;
export let label: $$Props['label'] = undefined;
export let markerStart: $$Props['markerStart'] = undefined;
export let markerEnd: $$Props['markerEnd'] = undefined;
export let sourceHandleId: $$Props['sourceHandleId'] = undefined;
export let targetHandleId: $$Props['targetHandleId'] = undefined;
const { edges, edgeTypes } = useStore();
// @ todo: support edge updates
const { edges, edgeTypes, flowId } = useStore();
const dispatch = createEventDispatcher();
const edgeComponent: typeof SvelteComponentTyped<EdgeProps> = $edgeTypes[type!] || BezierEdge;
$: markerStartUrl = markerStart ? `url(#${getMarkerId(markerStart, $flowId)})` : undefined;
$: markerEndUrl = markerEnd ? `url(#${getMarkerId(markerEnd, $flowId)})` : undefined;
function onClick() {
const edge = $edges.find(e => e.id === id);
dispatch('edge:click', edge);
@@ -57,6 +66,12 @@
{animated}
{selected}
{label}
{data}
{style}
{sourceHandleId}
{targetHandleId}
markerStart={markerStartUrl}
markerEnd={markerEndUrl}
/>
</g>
@@ -16,4 +16,15 @@
});
</script>
<BaseEdge {path} {labelX} {labelY} {...$$props} />
<BaseEdge
{path}
{labelX}
{labelY}
id={$$props.id}
label={$$props.label}
markerStart={$$props.markerStart}
markerEnd={$$props.markerEnd}
interactionWidth={$$props.interactionWidth}
style={$$props.style}
/>
@@ -16,4 +16,14 @@
});
</script>
<BaseEdge {path} {labelX} {labelY} {...$$props} />
<BaseEdge
{path}
{labelX}
{labelY}
id={$$props.id}
label={$$props.label}
markerStart={$$props.markerStart}
markerEnd={$$props.markerEnd}
interactionWidth={$$props.interactionWidth}
style={$$props.style}
/>
@@ -17,4 +17,14 @@
});
</script>
<BaseEdge {path} {labelX} {labelY} {...$$props} />
<BaseEdge
{path}
{labelX}
{labelY}
id={$$props.id}
label={$$props.label}
markerStart={$$props.markerStart}
markerEnd={$$props.markerEnd}
interactionWidth={$$props.interactionWidth}
style={$$props.style}
/>
@@ -14,4 +14,15 @@
});
</script>
<BaseEdge {path} {labelX} {labelY} {...$$props} />
<BaseEdge
{path}
{labelX}
{labelY}
id={$$props.id}
label={$$props.label}
markerStart={$$props.markerStart}
markerEnd={$$props.markerEnd}
interactionWidth={$$props.interactionWidth}
style={$$props.style}
/>
@@ -1,5 +1,6 @@
<script lang="ts">
import { EdgeWrapper } from '$lib/components/EdgeWrapper';
import { MarkerDefinition } from '$lib/container/EdgeRenderer/MarkerDefinition';
import { useStore } from '$lib/store';
const { edgesLayouted, width, height } = useStore();
@@ -9,8 +10,11 @@
{#each $edgesLayouted as edge (edge.id)}
<EdgeWrapper {...edge} on:edge:click />
{/each}
<MarkerDefinition />
</svg>
<style>
.svelte-flow__edges {
width: 100%;
@@ -0,0 +1,46 @@
<script lang="ts">
import { type MarkerProps, MarkerType } from '@reactflow/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;
</script>
<marker
class="svelte-flow__arrowhead"
id={id}
markerWidth={`${width}`}
markerHeight={`${height}`}
viewBox="-10 -10 20 20"
markerUnits={markerUnits}
orient={orient}
refX="0"
refY="0"
>
{#if type === MarkerType.Arrow}
<polyline
stroke={color}
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={strokeWidth}
fill="none"
points="-5,-4 0,0 -5,4"
/>
{:else if type === MarkerType.ArrowClosed}
<polyline
stroke={color}
stroke-linecap="round"
stroke-linejoin="round"
stroke-width={strokeWidth}
fill={color}
points="-5,-4 0,0 -5,4 -5,-4"
/>
{/if}
</marker>
@@ -0,0 +1,21 @@
<script lang="ts">
import { useStore } from '$lib/store';
import { Marker } from '$lib/container/EdgeRenderer/MarkerDefinition';
const { markers } = useStore();
</script>
<defs>
{#each $markers as marker(marker.id)}
<Marker
id={marker.id}
type={marker.type}
color={marker.color}
width={marker.width}
height={marker.height}
markerUnits={marker.markerUnits}
strokeWidth={marker.strokeWidth}
orient={marker.orient}
/>
{/each}
</defs>
@@ -0,0 +1,2 @@
export { default as MarkerDefinition } from './MarkerDefinition.svelte';
export { default as Marker } from './Marker.svelte';
@@ -17,7 +17,7 @@
type $$Props = SvelteFlowProps;
export let id: $$Props['id'] = '1';
export let id = '1';
export let fitView: $$Props['fitView'] = undefined;
export let minZoom: $$Props['minZoom'] = undefined;
export let maxZoom: $$Props['maxZoom'] = undefined;
@@ -31,13 +31,14 @@
export let connectionRadius: $$Props['connectionRadius'] = undefined;
export let connectionLineType: $$Props['connectionLineType'] = undefined;
export let isValidConnection: $$Props['isValidConnection'] = undefined;
export let defaultMarkerColor = '#b1b1b7';
export let style: $$Props['style'] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
let domNode: HTMLDivElement;
$: flowId = id;
const store = useStore();
onMount(() => {
@@ -53,12 +54,13 @@
$: {
const updatableProps = {
id,
flowId,
connectionLineType,
connectionRadius,
selectionMode,
snapGrid,
isValidConnection
isValidConnection,
defaultMarkerColor
};
Object.keys(updatableProps).forEach(prop => {
@@ -32,6 +32,7 @@ export type SvelteFlowProps = DOMAttributes<HTMLDivElement> & {
connectionRadius?: number;
selectionMode?: SelectionMode;
snapGrid?: SnapGrid;
defaultMarkerColor?: string;
class?: string;
style?: string;
@@ -31,14 +31,14 @@
let className: $$Props['class'] = '';
export { className as class };
const { transform, id } = useStore();
const { transform, flowId } = useStore();
const patternColor = color || defaultColor[variant!];
const patternSize = size || defaultSize[variant!];
const isDots = variant === BackgroundVariant.Dots;
const isCross = variant === BackgroundVariant.Cross;
const gapXY: number[] = Array.isArray(gap!) ? gap! : [gap!, gap!];
$: patternId = `background-pattern-${$id}`;
$: patternId = `background-pattern-${$flowId}`;
$: scaledGap = [gapXY[0] * $transform[2] || 1, gapXY[1] * $transform[2] || 1];
$: scaledSize = patternSize * $transform[2];
$: patternDimensions = (isCross ? [scaledSize, scaledSize] : scaledGap) as [number, number];
+8 -1
View File
@@ -1,5 +1,5 @@
import { getContext } from 'svelte';
import { get } from 'svelte/store';
import { derived, get } from 'svelte/store';
import { zoomIdentity } from 'd3-zoom';
import {
type NodeDragItem,
@@ -11,6 +11,7 @@ import {
type CoordinateExtent
} from '@reactflow/system';
import {
createMarkerIds,
fitView as fitViewUtil,
getD3Transition,
getDimensions,
@@ -314,6 +315,12 @@ export function createStore(params: CreateStoreParams): SvelteFlowStore {
// derived state
edgesLayouted: getEdgesLayouted(store),
connectionPath: getConnectionPath(store),
markers: derived(
[store.edges, store.defaultMarkerColor, store.flowId],
([edges, defaultColor, id]) => {
return createMarkerIds(edges, { defaultColor, id });
}
),
// actions
setNodeTypes,
@@ -7,7 +7,8 @@ import {
ConnectionLineType,
type SelectionRect,
type Transform,
type SnapGrid
type SnapGrid,
type MarkerProps
} from '@reactflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
@@ -49,7 +50,7 @@ export const initialEdgeTypes = {
};
export const initialStoreState = {
id: writable<string | null>(null),
flowId: writable<string | null>(null),
nodes: writable<Node[]>([]),
edges: writable<Edge[]>([]),
edgesLayouted: readable<EdgeLayouted[]>([]),
@@ -80,5 +81,7 @@ export const initialStoreState = {
connection: writable<ConnectionData>(initConnectionData),
connectionRadius: writable<number>(25),
connectionLineType: writable<ConnectionLineType>(ConnectionLineType.Bezier),
isValidConnection: writable<IsValidConnection>(() => true)
isValidConnection: writable<IsValidConnection>(() => true),
markers: readable<MarkerProps[]>([]),
defaultMarkerColor: writable<string>('#b1b1b7')
};
+8
View File
@@ -47,11 +47,15 @@ export type EdgeLayouted = Omit<Edge, 'sourceHandle' | 'targetHandle'> & {
targetPosition: Position;
sourceHandleId?: string;
targetHandleId?: string;
markerStart?: string;
markerEnd?: string;
};
export type EdgeProps = Pick<
EdgeLayouted,
| 'id'
| 'data'
| 'style'
| 'source'
| 'target'
| 'sourceX'
@@ -64,6 +68,10 @@ export type EdgeProps = Pick<
| 'selected'
| 'label'
| 'interactionWidth'
| 'markerStart'
| 'markerEnd'
| 'sourceHandleId'
| 'targetHandleId'
>;
export type EdgeTypes = Record<string, typeof SvelteComponentTyped<EdgeProps>>;
+4
View File
@@ -57,3 +57,7 @@ export enum MarkerType {
Arrow = 'arrow',
ArrowClosed = 'arrowclosed',
}
export type MarkerProps = EdgeMarker & {
id: string;
};
-18
View File
@@ -5,7 +5,6 @@ import { boxToRect, clamp, devWarn, getBoundsOfBoxes, getOverlappingArea, rectTo
import {
errorMessages,
type Connection,
type EdgeMarkerType,
type Transform,
type XYPosition,
type Rect,
@@ -54,23 +53,6 @@ export const getIncomersBase = <NodeType extends BaseNode = BaseNode, EdgeType e
const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection | BaseEdge): string =>
`reactflow__edge-${source}${sourceHandle || ''}-${target}${targetHandle || ''}`;
export const getMarkerId = (marker: EdgeMarkerType | undefined, rfId?: string): string => {
if (typeof marker === 'undefined') {
return '';
}
if (typeof marker === 'string') {
return marker;
}
const idPrefix = rfId ? `${rfId}__` : '';
return `${idPrefix}${Object.keys(marker)
.sort()
.map((key: string) => `${key}=${(marker as any)[key]}`)
.join('&')}`;
};
const connectionExists = (edge: BaseEdge, edges: BaseEdge[]) => {
return edges.some(
(el) =>
+1
View File
@@ -1,2 +1,3 @@
export * from './graph';
export * from './utils';
export * from './marker';
+40
View File
@@ -0,0 +1,40 @@
import type { BaseEdge, EdgeMarker, EdgeMarkerType, MarkerProps } from '@reactflow/system';
export function getMarkerId(marker: EdgeMarkerType | undefined, id?: string | null): string {
if (!marker) {
return '';
}
if (typeof marker === 'string') {
return marker;
}
const idPrefix = id ? `${id}__` : '';
return `${idPrefix}${Object.keys(marker)
.sort()
.map((key) => `${key}=${marker[key as keyof EdgeMarker]}`)
.join('&')}`;
}
export function createMarkerIds(
edges: BaseEdge[],
{ id, defaultColor }: { id?: string | null; defaultColor?: string }
) {
const ids: string[] = [];
return edges
.reduce<MarkerProps[]>((markers, edge) => {
[edge.markerStart, edge.markerEnd].forEach((marker) => {
if (marker && typeof marker === 'object') {
const markerId = getMarkerId(marker, id);
if (!ids.includes(markerId)) {
markers.push({ id: markerId, color: marker.color || defaultColor, ...marker });
ids.push(markerId);
}
}
});
return markers;
}, [])
.sort((a, b) => a.id.localeCompare(b.id));
}