@bemedev/mind-flow 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/helpers/createContext.d.ts +21 -0
- package/lib/helpers/createContext.d.ts.map +1 -0
- package/lib/index.cjs.js +1 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.es.js +859 -0
- package/lib/output.css +2 -0
- package/lib/server.cjs.js +1058 -0
- package/lib/server.es.js +1056 -0
- package/lib/services/main.machine.d.ts +1198 -0
- package/lib/services/main.machine.d.ts.map +1 -0
- package/lib/services/main.typings.d.ts +68 -0
- package/lib/services/main.typings.d.ts.map +1 -0
- package/lib/ui/Flow.d.ts +13 -0
- package/lib/ui/Flow.d.ts.map +1 -0
- package/lib/ui/components/Bounds.d.ts +10 -0
- package/lib/ui/components/Bounds.d.ts.map +1 -0
- package/lib/ui/components/EdgeComponent.d.ts +20 -0
- package/lib/ui/components/EdgeComponent.d.ts.map +1 -0
- package/lib/ui/components/EdgesBoard.d.ts +9 -0
- package/lib/ui/components/EdgesBoard.d.ts.map +1 -0
- package/lib/ui/components/FlowChart.context.d.ts +1228 -0
- package/lib/ui/components/FlowChart.context.d.ts.map +1 -0
- package/lib/ui/components/FlowChart.d.ts +63 -0
- package/lib/ui/components/FlowChart.d.ts.map +1 -0
- package/lib/ui/components/FlowChart.data.d.ts +67 -0
- package/lib/ui/components/FlowChart.data.d.ts.map +1 -0
- package/lib/ui/components/NodeComponent.d.ts +34 -0
- package/lib/ui/components/NodeComponent.d.ts.map +1 -0
- package/lib/ui/components/NodesBoard.d.ts +9 -0
- package/lib/ui/components/NodesBoard.d.ts.map +1 -0
- package/lib/ui/components/classes.d.ts +6 -0
- package/lib/ui/components/classes.d.ts.map +1 -0
- package/package.json +146 -0
- package/src/README.md +36 -0
- package/src/helpers/createContext.ts +37 -0
- package/src/index.ts +3 -0
- package/src/input.css +15 -0
- package/src/services/main.machine.ts +247 -0
- package/src/services/main.typings.ts +50 -0
- package/src/ui/Flow.tsx +18 -0
- package/src/ui/components/Bounds.tsx +87 -0
- package/src/ui/components/EdgeComponent.tsx +106 -0
- package/src/ui/components/EdgesBoard.tsx +56 -0
- package/src/ui/components/FlowChart.context.ts +373 -0
- package/src/ui/components/FlowChart.data.ts +89 -0
- package/src/ui/components/FlowChart.tsx +112 -0
- package/src/ui/components/NodeComponent.tsx +288 -0
- package/src/ui/components/NodesBoard.tsx +298 -0
- package/src/ui/components/classes.ts +85 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { createMachine } from '@bemedev/app';
|
|
2
|
+
import { type } from '@bemedev/app/bemedev';
|
|
3
|
+
import { nanoid } from 'nanoid';
|
|
4
|
+
import { edgeJSON, extremities, nodeJSON } from './main.typings';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Constructs a unique edge identifier string from source and destination
|
|
8
|
+
* node IDs.
|
|
9
|
+
*
|
|
10
|
+
* @param out - The source node ID string.
|
|
11
|
+
* @param _in - The destination node ID string.
|
|
12
|
+
*
|
|
13
|
+
* @returns Formatted edge identifier string.
|
|
14
|
+
*/
|
|
15
|
+
export const buildEdgeId = (out: string, _in: string) => {
|
|
16
|
+
return `edge = ${out} => ${_in}`;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Constructs a formatted node identifier string from a generated ID.
|
|
21
|
+
*
|
|
22
|
+
* @param generated - The generated unique ID string or `null`/`undefined`.
|
|
23
|
+
*
|
|
24
|
+
* @returns Formatted node identifier string.
|
|
25
|
+
*/
|
|
26
|
+
export const buildNodeID = (generated?: string | null) => {
|
|
27
|
+
return `node-${generated}`;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* State machine managing flowchart state transitions, nodes, edges,
|
|
32
|
+
* selection, and layout actions.
|
|
33
|
+
*/
|
|
34
|
+
export const machine = createMachine(
|
|
35
|
+
{
|
|
36
|
+
initial: 'idle',
|
|
37
|
+
states: {
|
|
38
|
+
idle: {
|
|
39
|
+
on: {
|
|
40
|
+
CONFIGURE: { actions: ['configure'], target: '/working' },
|
|
41
|
+
CONFIGURE_EMPTY: '/working',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
construction: {
|
|
46
|
+
always: { actions: ['buildUI'], target: '/working' },
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
working: {
|
|
50
|
+
on: {
|
|
51
|
+
MOVE: {
|
|
52
|
+
actions: ['moveNode', 'buildUI'],
|
|
53
|
+
target: '/construction',
|
|
54
|
+
},
|
|
55
|
+
MOVE_IMMEDIATE: {
|
|
56
|
+
actions: [
|
|
57
|
+
{
|
|
58
|
+
name: 'buildImmediateUI',
|
|
59
|
+
description: 'Must be in the ui',
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
UPDATE_UI: '/construction',
|
|
65
|
+
|
|
66
|
+
ADD_CHILD: {
|
|
67
|
+
actions: [
|
|
68
|
+
'generateID',
|
|
69
|
+
{ name: 'placeChild', description: 'Must be in the ui' },
|
|
70
|
+
'linkChild',
|
|
71
|
+
],
|
|
72
|
+
target: '/construction',
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
ADD_PARENT: {
|
|
76
|
+
actions: [
|
|
77
|
+
'generateID',
|
|
78
|
+
{ name: 'placeParent', description: 'Must be in the ui' },
|
|
79
|
+
'selectParent',
|
|
80
|
+
],
|
|
81
|
+
target: '/construction',
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
ADD_SIBLING: {
|
|
85
|
+
actions: [
|
|
86
|
+
'generateID',
|
|
87
|
+
{ name: 'placeSibling', description: 'Must be in the ui' },
|
|
88
|
+
'linkSibling',
|
|
89
|
+
],
|
|
90
|
+
target: '/construction',
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
ADD_EDGE: { actions: ['addEdge'], target: '/construction' },
|
|
94
|
+
|
|
95
|
+
DELETE: { actions: ['delete'], target: '/construction' },
|
|
96
|
+
|
|
97
|
+
SELECT: { actions: ['select'] },
|
|
98
|
+
|
|
99
|
+
DESELECT: { actions: ['deselect'] },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
eventsMap: type(({ intersection, use, array }) => ({
|
|
106
|
+
CONFIGURE: {
|
|
107
|
+
nodes: array(intersection(use(nodeJSON), { id: 'string' })),
|
|
108
|
+
edges: array(intersection(use(edgeJSON), { id: 'string' })),
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
CONFIGURE_EMPTY: 'never',
|
|
112
|
+
|
|
113
|
+
MOVE: { id: 'string', x: 'number', y: 'number' },
|
|
114
|
+
|
|
115
|
+
MOVE_IMMEDIATE: { id: 'string', x: 'number', y: 'number' },
|
|
116
|
+
|
|
117
|
+
ADD_CHILD: 'string',
|
|
118
|
+
ADD_PARENT: 'never',
|
|
119
|
+
ADD_SIBLING: 'string',
|
|
120
|
+
DELETE: 'string',
|
|
121
|
+
SELECT: 'string',
|
|
122
|
+
DESELECT: 'never',
|
|
123
|
+
ADD_EDGE: use(extremities),
|
|
124
|
+
})),
|
|
125
|
+
|
|
126
|
+
pContext: type(({ union }) => ({
|
|
127
|
+
generatedId: union('string', 'null'),
|
|
128
|
+
})),
|
|
129
|
+
|
|
130
|
+
context: type(({ optional, use, array }) => ({
|
|
131
|
+
data: optional({
|
|
132
|
+
nodes: array({ ...use(nodeJSON), id: 'string' }),
|
|
133
|
+
edges: array({ ...use(edgeJSON), id: 'string' }),
|
|
134
|
+
}),
|
|
135
|
+
|
|
136
|
+
selected: optional('string'),
|
|
137
|
+
updatingUI: optional('boolean'),
|
|
138
|
+
})),
|
|
139
|
+
|
|
140
|
+
sync: true,
|
|
141
|
+
},
|
|
142
|
+
).provideOptions(({ assign, batch, erase }) => ({
|
|
143
|
+
actions: {
|
|
144
|
+
configure: batch(
|
|
145
|
+
assign('context.data', () => ({ nodes: [], edges: [] })),
|
|
146
|
+
|
|
147
|
+
assign('context.data.nodes', {
|
|
148
|
+
CONFIGURE: ({ payload: { nodes } }) => nodes,
|
|
149
|
+
}),
|
|
150
|
+
|
|
151
|
+
assign('context.data.edges', {
|
|
152
|
+
CONFIGURE: ({ payload: { edges } }) => edges,
|
|
153
|
+
}),
|
|
154
|
+
|
|
155
|
+
assign('context.updatingUI', () => false),
|
|
156
|
+
assign('pContext.generatedId', () => null),
|
|
157
|
+
),
|
|
158
|
+
|
|
159
|
+
generateID: assign('pContext.generatedId', () => nanoid()),
|
|
160
|
+
|
|
161
|
+
linkChild: batch(
|
|
162
|
+
assign('context.data.edges', {
|
|
163
|
+
ADD_CHILD: ({ context, pContext, payload }) => {
|
|
164
|
+
const data = context.data;
|
|
165
|
+
const from = payload;
|
|
166
|
+
const generatedId = pContext?.generatedId;
|
|
167
|
+
const to = buildNodeID(generatedId);
|
|
168
|
+
const id = buildEdgeId(from, to);
|
|
169
|
+
return [...(data?.edges ?? []), { id, from, to }];
|
|
170
|
+
},
|
|
171
|
+
}),
|
|
172
|
+
|
|
173
|
+
assign('context.selected', ({ pContext: { generatedId } }) =>
|
|
174
|
+
buildNodeID(generatedId),
|
|
175
|
+
),
|
|
176
|
+
),
|
|
177
|
+
|
|
178
|
+
linkSibling: batch(
|
|
179
|
+
assign('context.data.edges', {
|
|
180
|
+
ADD_SIBLING: ({ pContext, payload, context }) => {
|
|
181
|
+
const edges = context.data?.edges;
|
|
182
|
+
const generatedId = pContext?.generatedId;
|
|
183
|
+
const out = [...(edges ?? [])];
|
|
184
|
+
const from = edges?.find(({ to }) => to === payload)?.from;
|
|
185
|
+
if (!from) return out;
|
|
186
|
+
const to = buildNodeID(generatedId);
|
|
187
|
+
const id = buildEdgeId(from, to);
|
|
188
|
+
out.push({ from, to, id });
|
|
189
|
+
|
|
190
|
+
return out;
|
|
191
|
+
},
|
|
192
|
+
}),
|
|
193
|
+
|
|
194
|
+
assign('context.selected', ({ pContext: { generatedId } }) =>
|
|
195
|
+
buildNodeID(generatedId),
|
|
196
|
+
),
|
|
197
|
+
),
|
|
198
|
+
|
|
199
|
+
selectParent: assign(
|
|
200
|
+
'context.selected',
|
|
201
|
+
({ pContext: { generatedId } }) => buildNodeID(generatedId),
|
|
202
|
+
),
|
|
203
|
+
|
|
204
|
+
moveNode: assign('context.data.nodes', {
|
|
205
|
+
MOVE: ({ context: { data }, payload }) => {
|
|
206
|
+
const { id, x, y } = payload;
|
|
207
|
+
if (!id) return data?.nodes ?? [];
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
data?.nodes?.map(d => {
|
|
211
|
+
if (d.id === id) {
|
|
212
|
+
return { ...d, position: { x, y } };
|
|
213
|
+
}
|
|
214
|
+
return d;
|
|
215
|
+
}) ?? []
|
|
216
|
+
);
|
|
217
|
+
},
|
|
218
|
+
}),
|
|
219
|
+
|
|
220
|
+
select: assign('context.selected', {
|
|
221
|
+
SELECT: ({ payload }) => payload,
|
|
222
|
+
}),
|
|
223
|
+
|
|
224
|
+
delete: assign(['context.data.nodes', 'context.data.edges'], {
|
|
225
|
+
DELETE: ({ context: { data }, payload }) => {
|
|
226
|
+
const nodes = data?.nodes?.filter(({ id }) => id !== payload);
|
|
227
|
+
const edges = data?.edges?.filter(
|
|
228
|
+
({ id, from, to }) =>
|
|
229
|
+
id !== payload && from !== payload && to !== payload,
|
|
230
|
+
);
|
|
231
|
+
return [nodes, edges];
|
|
232
|
+
},
|
|
233
|
+
}),
|
|
234
|
+
|
|
235
|
+
addEdge: assign('context.data.edges', {
|
|
236
|
+
ADD_EDGE: ({ context, payload: { from, to } }) => {
|
|
237
|
+
const edges = context.data?.edges ?? [];
|
|
238
|
+
const id = buildEdgeId(from, to);
|
|
239
|
+
if (edges.some(e => e.id === id)) return edges;
|
|
240
|
+
const out = [...edges, { id, from, to }];
|
|
241
|
+
return out;
|
|
242
|
+
},
|
|
243
|
+
}),
|
|
244
|
+
|
|
245
|
+
deselect: erase('context.selected'),
|
|
246
|
+
},
|
|
247
|
+
}));
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type } from '@bemedev/app/bemedev';
|
|
2
|
+
import type { inferT } from '@bemedev/app/typings';
|
|
3
|
+
|
|
4
|
+
/** Schema definition for 2D coordinates `(x, y)`. */
|
|
5
|
+
export const point = type({ x: 'number', y: 'number' });
|
|
6
|
+
|
|
7
|
+
/** 2D coordinate point type inferred from schema {@linkcode point}. */
|
|
8
|
+
export type Point = inferT<typeof point>;
|
|
9
|
+
|
|
10
|
+
/** Schema definition for node handle offsets (input and output). */
|
|
11
|
+
export const nodeOffset = type(({ optional, use }) => ({
|
|
12
|
+
input: optional(use(point)),
|
|
13
|
+
output: use(point),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
/** Schema definition for edge extremities */
|
|
17
|
+
export const extremities = type({ from: 'string', to: 'string' });
|
|
18
|
+
|
|
19
|
+
/** Schema definition for a serialized flowchart node entity. */
|
|
20
|
+
export const nodeJSON = type(({ optional, use }) => ({
|
|
21
|
+
position: use(point),
|
|
22
|
+
data: { label: optional('string'), content: 'string' },
|
|
23
|
+
input: 'boolean',
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
/** Schema definition for a serialized flowchart edge entity. */
|
|
27
|
+
export const edgeJSON = extremities;
|
|
28
|
+
|
|
29
|
+
/** Schema definition for layout dimensions and connection points of a node. */
|
|
30
|
+
export const dimensions = type(({ optional, use }) => ({
|
|
31
|
+
width: 'number',
|
|
32
|
+
height: 'number',
|
|
33
|
+
id: 'string',
|
|
34
|
+
output: use(point),
|
|
35
|
+
input: optional(use(point)),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Schema definition for a 2D line vector representing edge coordinates
|
|
40
|
+
* `(x0, y0)` to `(x1, y1)`.
|
|
41
|
+
*/
|
|
42
|
+
export const vector = type({
|
|
43
|
+
x0: 'number',
|
|
44
|
+
y0: 'number',
|
|
45
|
+
x1: 'number',
|
|
46
|
+
y1: 'number',
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** 2D vector coordinate type inferred from schema {@linkcode vector}. */
|
|
50
|
+
export type Vector = inferT<typeof vector>;
|
package/src/ui/Flow.tsx
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Component } from 'solid-js';
|
|
2
|
+
import { FlowChart, type FlowProps } from './components/FlowChart';
|
|
3
|
+
import { Provider } from './components/FlowChart.context';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Root Flow component wrapping the {@linkcode FlowChart} inside the flow
|
|
7
|
+
* context provider.
|
|
8
|
+
*
|
|
9
|
+
* @param props - Flow chart configuration and callbacks of type
|
|
10
|
+
* {@linkcode FlowProps}.
|
|
11
|
+
*
|
|
12
|
+
* @returns The rendered Solid component.
|
|
13
|
+
*/
|
|
14
|
+
export const Flow: Component<FlowProps> = props => (
|
|
15
|
+
<Provider>
|
|
16
|
+
<FlowChart {...props} />
|
|
17
|
+
</Provider>
|
|
18
|
+
);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useDragDropContext,
|
|
3
|
+
type Transformer,
|
|
4
|
+
} from '@thisbeyond/solid-dnd';
|
|
5
|
+
import { type Component } from 'solid-js';
|
|
6
|
+
import { BOUNDS_CONSTRAINTS } from './FlowChart.data';
|
|
7
|
+
import { useFlow } from './FlowChart.context';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Drag boundary transformer component that clamps draggable nodes within
|
|
11
|
+
* container scroll bounds.
|
|
12
|
+
*
|
|
13
|
+
* @returns `null` as this component performs side-effect transformer
|
|
14
|
+
* registrations only.
|
|
15
|
+
*/
|
|
16
|
+
export const DragBounds: Component = () => {
|
|
17
|
+
const {
|
|
18
|
+
board: [ref],
|
|
19
|
+
zoom: [zoom],
|
|
20
|
+
} = useFlow();
|
|
21
|
+
|
|
22
|
+
const [
|
|
23
|
+
state,
|
|
24
|
+
{ addTransformer, removeTransformer, onDragStart, onDragEnd },
|
|
25
|
+
] = useDragDropContext()!;
|
|
26
|
+
|
|
27
|
+
const transformer: Transformer = {
|
|
28
|
+
id: 'clamp-to-container',
|
|
29
|
+
order: 100,
|
|
30
|
+
callback: transform => {
|
|
31
|
+
const container = ref()?.parentElement;
|
|
32
|
+
const activeDraggable = state.active.draggable;
|
|
33
|
+
if (!container || !activeDraggable) return transform;
|
|
34
|
+
const draggableLayout = activeDraggable.layout;
|
|
35
|
+
|
|
36
|
+
// #region Inner visible boundaries (excluding borders and scrollbars)
|
|
37
|
+
const containerRect = container.getBoundingClientRect();
|
|
38
|
+
const innerLeft = containerRect.left + container.clientLeft;
|
|
39
|
+
const innerTop = containerRect.top + container.clientTop;
|
|
40
|
+
const innerRight = innerLeft + container.clientWidth;
|
|
41
|
+
const innerBottom = innerTop + container.clientHeight;
|
|
42
|
+
// #endregion
|
|
43
|
+
|
|
44
|
+
// #region Convert boundaries to board coordinate space
|
|
45
|
+
const currentZoom = zoom();
|
|
46
|
+
const minX =
|
|
47
|
+
innerLeft -
|
|
48
|
+
draggableLayout.left +
|
|
49
|
+
BOUNDS_CONSTRAINTS.horizontal * currentZoom;
|
|
50
|
+
|
|
51
|
+
const maxX = Math.max(
|
|
52
|
+
minX,
|
|
53
|
+
innerRight -
|
|
54
|
+
draggableLayout.right -
|
|
55
|
+
BOUNDS_CONSTRAINTS.horizontal * currentZoom,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const minY =
|
|
59
|
+
innerTop -
|
|
60
|
+
draggableLayout.top +
|
|
61
|
+
BOUNDS_CONSTRAINTS.vertical * currentZoom;
|
|
62
|
+
|
|
63
|
+
const maxY = Math.max(
|
|
64
|
+
minY,
|
|
65
|
+
innerBottom -
|
|
66
|
+
draggableLayout.bottom -
|
|
67
|
+
BOUNDS_CONSTRAINTS.vertical * currentZoom,
|
|
68
|
+
);
|
|
69
|
+
// #endregion
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
x: Math.min(Math.max(transform.x, minX), maxX),
|
|
73
|
+
y: Math.min(Math.max(transform.y, minY), maxY),
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
onDragStart(({ draggable }) => {
|
|
79
|
+
addTransformer('draggables', draggable.id, transformer);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
onDragEnd(({ draggable }) => {
|
|
83
|
+
removeTransformer('draggables', draggable.id, transformer.id);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return null;
|
|
87
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useState } from '@bemedev/app-solidjs';
|
|
2
|
+
import { Component, createEffect, createSignal, Show } from 'solid-js';
|
|
3
|
+
import type { Vector } from '../../services/main.typings';
|
|
4
|
+
import { useFlow } from './FlowChart.context';
|
|
5
|
+
|
|
6
|
+
/** Properties for rendering an SVG connection edge between two points. */
|
|
7
|
+
type Props = {
|
|
8
|
+
/** Unique identifier of the edge. */
|
|
9
|
+
id: string;
|
|
10
|
+
/** Whether this is a temporary edge currently being dragged. */
|
|
11
|
+
isNew?: boolean;
|
|
12
|
+
} & Vector;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Computes bezier curve control point offset based on horizontal distance.
|
|
16
|
+
*
|
|
17
|
+
* @param value - Absolute horizontal delta between start and end points.
|
|
18
|
+
*
|
|
19
|
+
* @returns Offset distance in pixels for bezier curvature.
|
|
20
|
+
*/
|
|
21
|
+
const calculateOffset = (value: number) => (value * 100) / 200;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generates an SVG cubic bezier path `d` string between coordinates `(x0,
|
|
25
|
+
* y0)` and `(x1, y1)`.
|
|
26
|
+
*
|
|
27
|
+
* @param vector - Vector coordinates of type {@linkcode Vector}.
|
|
28
|
+
*
|
|
29
|
+
* @returns SVG cubic bezier path string.
|
|
30
|
+
*/
|
|
31
|
+
const draw = ({ x0, y0, x1, y1 }: Vector) => {
|
|
32
|
+
return `M ${x0} ${y0} C ${x0 + calculateOffset(Math.abs(x1 - x0))} ${y0}, ${x1 - calculateOffset(Math.abs(x1 - x0))} ${y1}, ${x1} ${y1}`;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* SVG edge component rendering bezier curves and delete interaction
|
|
37
|
+
* handles for node connections.
|
|
38
|
+
*
|
|
39
|
+
* @param props - Edge properties of type {@linkcode Props}.
|
|
40
|
+
*
|
|
41
|
+
* @returns The rendered SVG JSX elements.
|
|
42
|
+
*/
|
|
43
|
+
export const EdgeComponent: Component<Props> = props => {
|
|
44
|
+
const [middlePoint, setMiddlePoint] = createSignal({
|
|
45
|
+
x: props.x0 + (props.x1 - props.x0) / 2,
|
|
46
|
+
y: props.y0 + (props.y1 - props.y0) / 2,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const { service } = useFlow();
|
|
50
|
+
|
|
51
|
+
createEffect(() => {
|
|
52
|
+
const middleX = props.x0 + (props.x1 - props.x0) / 2;
|
|
53
|
+
const middleY = props.y0 + (props.y1 - props.y0) / 2;
|
|
54
|
+
setMiddlePoint({ x: middleX, y: middleY });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const selected = useState(service, {
|
|
58
|
+
selector: s => s.context.selected === props.id,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<>
|
|
63
|
+
<path
|
|
64
|
+
class='relative cursor-pointer fill-transparent'
|
|
65
|
+
classList={{
|
|
66
|
+
'stroke-[rgba(168,168,168,0.4)] stroke-3 z-200': !!props.isNew,
|
|
67
|
+
'stroke-[rgba(168,168,168,1)] stroke-4 z-100':
|
|
68
|
+
selected() && !props.isNew,
|
|
69
|
+
'stroke-[rgba(168,168,168,0.8)] stroke-3':
|
|
70
|
+
!selected() && !props.isNew,
|
|
71
|
+
}}
|
|
72
|
+
style={{ 'pointer-events': props.isNew ? 'none' : 'all' }}
|
|
73
|
+
d={draw(props)}
|
|
74
|
+
onMouseDown={e => e.stopPropagation()}
|
|
75
|
+
onClick={() => service.send({ type: 'SELECT', payload: props.id })}
|
|
76
|
+
/>
|
|
77
|
+
<Show when={selected()}>
|
|
78
|
+
<g
|
|
79
|
+
cursor='pointer'
|
|
80
|
+
transform={`translate(${middlePoint().x}, ${middlePoint().y})`}
|
|
81
|
+
onMouseDown={e => {
|
|
82
|
+
e.stopPropagation();
|
|
83
|
+
service.send({ type: 'DELETE', payload: props.id });
|
|
84
|
+
}}
|
|
85
|
+
style={{ 'pointer-events': 'all', 'z-index': '30' }}
|
|
86
|
+
>
|
|
87
|
+
<circle cx='0' cy='0' r='12' fill='rgba(168, 168, 168, 1)' />
|
|
88
|
+
<svg
|
|
89
|
+
fill='currentColor'
|
|
90
|
+
stroke-width='0'
|
|
91
|
+
xmlns='http://www.w3.org/2000/svg'
|
|
92
|
+
class='h-25 w-25 bg-white fill-white'
|
|
93
|
+
width='20'
|
|
94
|
+
height='20'
|
|
95
|
+
viewBox='0 0 20 20'
|
|
96
|
+
color='white'
|
|
97
|
+
x='-10'
|
|
98
|
+
y='-10'
|
|
99
|
+
>
|
|
100
|
+
<path d='M10.185,1.417c-4.741,0-8.583,3.842-8.583,8.583c0,4.74,3.842,8.582,8.583,8.582S18.768,14.74,18.768,10C18.768,5.259,14.926,1.417,10.185,1.417 M10.185,17.68c-4.235,0-7.679-3.445-7.679-7.68c0-4.235,3.444-7.679,7.679-7.679S17.864,5.765,17.864,10C17.864,14.234,14.42,17.68,10.185,17.68 M10.824,10l2.842-2.844c0.178-0.176,0.178-0.46,0-0.637c-0.177-0.178-0.461-0.178-0.637,0l-2.844,2.841L7.341,6.52c-0.176-0.178-0.46-0.178-0.637,0c-0.178,0.176-0.178,0.461,0,0.637L9.546,10l-2.841,2.844c-0.178,0.176-0.178,0.461,0,0.637c0.178,0.178,0.459,0.178,0.637,0l2.844-2.841l2.844,2.841c0.178,0.178,0.459,0.178,0.637,0c0.178-0.176,0.178-0.461,0-0.637L10.824,10z'></path>
|
|
101
|
+
</svg>
|
|
102
|
+
</g>
|
|
103
|
+
</Show>
|
|
104
|
+
</>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
createEffect,
|
|
4
|
+
createMemo,
|
|
5
|
+
createSignal,
|
|
6
|
+
For,
|
|
7
|
+
Show,
|
|
8
|
+
} from 'solid-js';
|
|
9
|
+
import { EdgeComponent } from './EdgeComponent';
|
|
10
|
+
import { useFlow } from './FlowChart.context';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* SVG board overlay component that renders all active connecting edges and
|
|
14
|
+
* ongoing edge creation previews.
|
|
15
|
+
*
|
|
16
|
+
* @returns The rendered SVG JSX element.
|
|
17
|
+
*/
|
|
18
|
+
export const EdgesBoard: Component = () => {
|
|
19
|
+
const [selected, setSelected] = createSignal<string>();
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
newEdge: [newEdge],
|
|
23
|
+
edgesPositions: [edgesPositions],
|
|
24
|
+
} = useFlow();
|
|
25
|
+
|
|
26
|
+
const datas = createMemo(() => {
|
|
27
|
+
const entries = Object.entries(edgesPositions());
|
|
28
|
+
return entries.map(([id, vector]) => ({ id, ...vector }));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
createEffect(() => {
|
|
32
|
+
if (selected() && newEdge()) setSelected();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<svg
|
|
37
|
+
class='pointer-events-none h-full w-full overflow-visible'
|
|
38
|
+
// style={{ scale: zoom() }}
|
|
39
|
+
>
|
|
40
|
+
<Show when={newEdge()}>
|
|
41
|
+
{value => (
|
|
42
|
+
<EdgeComponent
|
|
43
|
+
id='__#new-edge#__TEMP'
|
|
44
|
+
isNew
|
|
45
|
+
x0={value().x0}
|
|
46
|
+
y0={value().y0}
|
|
47
|
+
x1={value().x1}
|
|
48
|
+
y1={value().y1}
|
|
49
|
+
/>
|
|
50
|
+
)}
|
|
51
|
+
</Show>
|
|
52
|
+
|
|
53
|
+
<For each={datas()} children={EdgeComponent} />
|
|
54
|
+
</svg>
|
|
55
|
+
);
|
|
56
|
+
};
|