Skip to content

Commit 80be07d

Browse files
committed
interaction: Implement user-created edges
The user can create edges by dragging from a handle to another (or node) Config options to allow this must be set
1 parent 91259cf commit 80be07d

21 files changed

+434
-134
lines changed

examples/src/App.tsx

+8-4
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,19 @@ const nodes: NewNode[] = [
1111
]
1212

1313
const edges: NewEdge[] = [
14-
{id: "0", source: "1", target: "0", label: "hehe", markerStart: "arrow", markerEnd: "arrow-filled"},
15-
{id: "1", source: "0", target: "2", label: "hihi", markerEnd: "arrow", targetHandle: null},
16-
{id: "2", source: "3", target: "2", label: "a", markerEnd: "arrow"},
17-
{id: "3", source: "2", target: "3", label: "b", markerEnd: "arrow"},
14+
{id: "0", source: "1", target: "0", label: "hehe", markerStart: "arrow-filled", markerEnd: "arrow-filled"},
15+
{id: "1", source: "0", target: "2", label: "hihi", targetHandle: null},
16+
{id: "2", source: "3", target: "2", label: "a"},
17+
{id: "3", source: "2", target: "3", label: "b"},
1818
]
1919

2020
const config: GrapherConfig = {
2121
nodeDefaults: {
2222
handlePointerEvents: true,
23+
allowGrabbingHandles: true,
24+
allowNewEdgesFromHandles: true,
25+
allowNewEdgeTarget: true,
26+
allowNewEdgeTargetForHandles: true,
2327
},
2428
edgeDefaults: {
2529
}

src/components/BaseEdge.tsx

+18-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import React, {useContext, useEffect, useRef} from "react";
1+
import React, {SVGProps, useContext, useEffect, useRef} from "react";
22
import {InternalContext} from "../context/InternalContext";
33
import {cx} from "@emotion/css";
4-
import {EDGE_CLASS, EDGE_HANDLE_CLASS, EDGE_LABEL_BACKGROUND_CLASS, EDGE_LABEL_CLASS, EDGE_PATH_CLASS} from "../util/constants";
4+
import {EDGE_CLASS, EDGE_HANDLE_CLASS, EDGE_IN_PROGRESS_CLASS, EDGE_LABEL_BACKGROUND_CLASS, EDGE_LABEL_CLASS, EDGE_PATH_CLASS} from "../util/constants";
55
import {errorUnknownEdge, warnInvalidPropValue} from "../util/log";
66
// Used by documentation
77
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -49,6 +49,10 @@ export interface BaseEdgeProps {
4949
* Label rotation angle.
5050
*/
5151
labelAngle: number
52+
/**
53+
* Label background border radius
54+
*/
55+
labelBackgroundRadius: NonNullable<SVGProps<SVGRectElement>["rx"]>
5256
/**
5357
* Whether this edge is selected
5458
*/
@@ -69,10 +73,15 @@ export interface BaseEdgeProps {
6973
* Whether to enable pointer events for this edge.
7074
*/
7175
pointerEvents: boolean
76+
/**
77+
* Whether this is an in-progress edge (i.e. currently being created by the user by dragging from a node handle).
78+
* This will swap the base class "react-grapher-edge", on the root element, for "react-grapher-edge-in-progress".
79+
*/
80+
inProgress?: boolean
7281
}
7382

74-
export function BaseEdge({id, path, classes, boxWidth, label, labelPosition, labelOffset, labelAnchor, labelBaseline, labelAngle,
75-
selected, grabbed, markerStart, markerEnd, pointerEvents} : BaseEdgeProps) {
83+
export function BaseEdge({id, path, classes, boxWidth, label, labelPosition, labelOffset, labelAnchor, labelBaseline, labelAngle, labelBackgroundRadius,
84+
selected, grabbed, markerStart, markerEnd, pointerEvents, inProgress} : BaseEdgeProps) {
7685
const internals = useContext(InternalContext)
7786

7887
// Ref to the Edge <g> and <path> elements
@@ -81,7 +90,7 @@ export function BaseEdge({id, path, classes, boxWidth, label, labelPosition, lab
8190
const labelRef = useRef<SVGTextElement>(null), labelBgRef = useRef<SVGRectElement>(null)
8291
// Get internal edge object
8392
const edge = internals.getEdge(id)
84-
if (edge == null) errorUnknownEdge(id)
93+
if (edge == null && !inProgress) errorUnknownEdge(id)
8594

8695
// Set listeners
8796
useEffect(() => {
@@ -146,19 +155,19 @@ export function BaseEdge({id, path, classes, boxWidth, label, labelPosition, lab
146155
labelBg.setAttribute("y", String(labelBounds.y - p))
147156
labelBg.setAttribute("width", String(labelBounds.width + p * 2))
148157
labelBg.setAttribute("height", String(labelBounds.height + p * 2))
149-
} else console.log({labelElem, labelBg, path, label})
158+
}
150159

151160
}, [internals, edge, id, path, grabbed, selected, label, labelPosition, labelOffset, labelAngle])
152161

153162
const baseID = internals.id
154-
return <g ref={ref} id={`${baseID}-edge-${id}`} className={cx(classes, EDGE_CLASS)} pointerEvents={!pointerEvents || internals.isStatic ? "none" : "stroke"}
155-
data-grabbed={grabbed} data-selected={selected} data-id={id} data-type={"edge"}>
163+
return <g ref={ref} id={`${baseID}-edge-${id}`} className={cx(classes, inProgress ? EDGE_IN_PROGRESS_CLASS : EDGE_CLASS)} data-id={id} data-type={"edge"}
164+
pointerEvents={!pointerEvents || internals.isStatic || inProgress ? "none" : "stroke"} data-grabbed={grabbed} data-selected={selected}>
156165
<path d={path} className={EDGE_HANDLE_CLASS} stroke={"transparent"} fill={"none"} strokeWidth={boxWidth}/>
157166
<path ref={pathRef} d={path} className={EDGE_PATH_CLASS}
158167
markerStart={markerStart != null ? `url(#${baseID}-${markerStart})` : undefined}
159168
markerEnd={markerEnd != null ? `url(#${baseID}-${markerEnd})` : undefined}/>
160169
{label != null && <>
161-
<rect ref={labelBgRef} className={EDGE_LABEL_BACKGROUND_CLASS} rx={edge?.labelRadius} pointerEvents={!pointerEvents || internals.isStatic ? "none" : "fill"}/>
170+
<rect ref={labelBgRef} className={EDGE_LABEL_BACKGROUND_CLASS} rx={labelBackgroundRadius} pointerEvents={!pointerEvents || internals.isStatic ? "none" : "fill"}/>
162171
<text ref={labelRef} className={EDGE_LABEL_CLASS} data-label-pos={String(labelPosition)} textAnchor={labelAnchor} dominantBaseline={labelBaseline}
163172
x={typeof labelPosition === "object" ? labelPosition.x : undefined}
164173
y={typeof labelPosition === "object" ? labelPosition.y : undefined}>

src/components/BaseNode.tsx

+22-17
Original file line numberDiff line numberDiff line change
@@ -224,22 +224,19 @@ export function BaseNode({id, classes, absolutePosition, grabbed, selected, resi
224224
const handleElems = container.querySelectorAll<HTMLElement>("." + NODE_HANDLE_CONTAINER_CLASS)
225225
// Check if handles have changed and update permissions (that we don't care if they change, no need to re-render)
226226
let handlesChanged = false
227-
if (node.handles == null || borderChanged || sizeChanged) handlesChanged = true
228-
else {
229-
if (node.handles.length !== handleElems.length) handlesChanged = true
230-
else for (let i = 0; i < handleElems.length; ++i) {
231-
const handleElem = handleElems[i] // handle element
232-
const nodeHandle = node.handles[i] // node handle
233-
const style = getComputedStyle(handleElem)
234-
235-
const [x, y] = [resolveValue(style.left, 0) + border[3] - node.width / 2, resolveValue(style.top, 0) + border[0] - node.height / 2]
236-
237-
if (handleElem.dataset.name !== nodeHandle.name || Math.abs(x - nodeHandle.x) > 2 || Math.abs(y - nodeHandle.y) > 2) handlesChanged = true
238-
else {
239-
nodeHandle.allowNewEdges = stringToBoolean(handleElem.dataset.allowNewEdges)
240-
nodeHandle.allowNewEdgeTarget = stringToBoolean(handleElem.dataset.allowNewEdgesTarget)
241-
nodeHandle.allowGrabbing = stringToBoolean(handleElem.dataset.allowGrabbing)
242-
}
227+
if (borderChanged || sizeChanged || node.handles.length !== handleElems.length) handlesChanged = true
228+
else for (let i = 0; i < handleElems.length; ++i) {
229+
const handleElem = handleElems[i] // handle element
230+
const nodeHandle = node.handles[i] // node handle
231+
const style = getComputedStyle(handleElem)
232+
233+
const [x, y] = [resolveValue(style.left, 0) + border[3] - node.width / 2, resolveValue(style.top, 0) + border[0] - node.height / 2]
234+
235+
if (handleElem.dataset.name !== nodeHandle.name || Math.abs(x - nodeHandle.x) > 2 || Math.abs(y - nodeHandle.y) > 2) handlesChanged = true
236+
else {
237+
nodeHandle.allowNewEdges = stringToBoolean(handleElem.dataset.allowNewEdges)
238+
nodeHandle.allowNewEdgeTarget = stringToBoolean(handleElem.dataset.allowNewEdgesTarget)
239+
nodeHandle.allowGrabbing = stringToBoolean(handleElem.dataset.allowGrabbing)
243240
}
244241
}
245242
if (!handlesChanged) return
@@ -379,7 +376,15 @@ export function BaseNode({id, classes, absolutePosition, grabbed, selected, resi
379376
const allowGrabbing = stringToBoolean(h.dataset.allowGrabbing)
380377

381378
// Save data and make x and y relative to the node's center
382-
handles.push({name, roles, x: x - node.width / 2, y: y - node.height / 2, allowNewEdges: allowCreatingEdges, allowNewEdgeTarget: allowCreatingEdgesTarget, allowGrabbing})
379+
handles.push({
380+
name,
381+
roles,
382+
x: x - node.width / 2,
383+
y: y - node.height / 2,
384+
allowNewEdges: allowCreatingEdges,
385+
allowNewEdgeTarget: allowCreatingEdgesTarget,
386+
allowGrabbing
387+
})
383388
}
384389
node.handles = handles
385390
}, [id, internals, node])

0 commit comments

Comments
 (0)