@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,373 @@
|
|
|
1
|
+
import { interpret } from '@bemedev/app';
|
|
2
|
+
import { dequal } from 'dequal/lite';
|
|
3
|
+
import { createSignal } from 'solid-js';
|
|
4
|
+
import { produce } from 'solid-js/store';
|
|
5
|
+
import { createContext } from '../../helpers/createContext';
|
|
6
|
+
import { machine } from '../../services/main.machine';
|
|
7
|
+
import type { Point, Vector } from '../../services/main.typings';
|
|
8
|
+
import {
|
|
9
|
+
BOUNDS_CONSTRAINTS,
|
|
10
|
+
DEFAULT_INPUT_OFFSET,
|
|
11
|
+
getDefaultOutputOffset,
|
|
12
|
+
PARENT_CHILD_GAP_WIDTH,
|
|
13
|
+
} from './FlowChart.data';
|
|
14
|
+
|
|
15
|
+
/** Layout dimensions and handle offset coordinates for a flowchart node. */
|
|
16
|
+
type Dimensions = {
|
|
17
|
+
/** Node width in pixels. */
|
|
18
|
+
width: number;
|
|
19
|
+
/** Node height in pixels. */
|
|
20
|
+
height: number;
|
|
21
|
+
/** Output handle point coordinates of type {@linkcode Point}. */
|
|
22
|
+
output: Point;
|
|
23
|
+
/** Input handle point coordinates of type {@linkcode Point}. */
|
|
24
|
+
input?: Point;
|
|
25
|
+
/** Output handle relative offset point of type {@linkcode Point}. */
|
|
26
|
+
outputOffset?: Point;
|
|
27
|
+
/** Input handle relative offset point of type {@linkcode Point}. */
|
|
28
|
+
inputOffset?: Point;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Connection edge coordinate representation between two endpoints. */
|
|
32
|
+
export type Edge = {
|
|
33
|
+
/** Source node identifier. */
|
|
34
|
+
from: string;
|
|
35
|
+
/** Starting X-coordinate. */
|
|
36
|
+
x0: number;
|
|
37
|
+
/** Starting Y-coordinate. */
|
|
38
|
+
y0: number;
|
|
39
|
+
/** Ending X-coordinate. */
|
|
40
|
+
x1: number;
|
|
41
|
+
/** Ending Y-coordinate. */
|
|
42
|
+
y1: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Shared state machine interpreter service instance for flowchart state
|
|
47
|
+
* management.
|
|
48
|
+
*/
|
|
49
|
+
const service = interpret(machine, {
|
|
50
|
+
context: {},
|
|
51
|
+
pContext: { generatedId: null },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Solid Context Provider component and hook for accessing flowchart board
|
|
56
|
+
* state, services, and zoom.
|
|
57
|
+
*/
|
|
58
|
+
export const [Provider, useFlow] = createContext(
|
|
59
|
+
() => {
|
|
60
|
+
const zoom = createSignal(1);
|
|
61
|
+
const newEdge = createSignal<Edge>();
|
|
62
|
+
const [boardRef, setBoardRef] = createSignal<HTMLDivElement>();
|
|
63
|
+
|
|
64
|
+
const [dimensions, setDimensions] = createSignal<
|
|
65
|
+
Record<string, Dimensions>
|
|
66
|
+
>({}, { equals: dequal });
|
|
67
|
+
|
|
68
|
+
const getBoardPoint = (clientX: number, clientY: number): Point => {
|
|
69
|
+
const el = boardRef();
|
|
70
|
+
if (!el) return { x: clientX, y: clientY };
|
|
71
|
+
const rect = el.getBoundingClientRect();
|
|
72
|
+
const currentZoom = zoom[0]();
|
|
73
|
+
return {
|
|
74
|
+
x: (clientX - rect.left) / currentZoom,
|
|
75
|
+
y: (clientY - rect.top) / currentZoom,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const [edgesPositions, setEdgesPositions] = createSignal<
|
|
80
|
+
Record<string, Vector>
|
|
81
|
+
>({}, { equals: false });
|
|
82
|
+
|
|
83
|
+
const clampPosition = (
|
|
84
|
+
x: number,
|
|
85
|
+
y: number,
|
|
86
|
+
nodeWidth = 192,
|
|
87
|
+
nodeHeight = 50,
|
|
88
|
+
): Point => {
|
|
89
|
+
const container = boardRef()?.parentElement;
|
|
90
|
+
if (!container) return { x, y };
|
|
91
|
+
const currentZoom = zoom[0]();
|
|
92
|
+
|
|
93
|
+
const minX =
|
|
94
|
+
container.scrollLeft / currentZoom + BOUNDS_CONSTRAINTS.horizontal;
|
|
95
|
+
const maxX = Math.max(
|
|
96
|
+
minX,
|
|
97
|
+
(container.scrollLeft + container.clientWidth) / currentZoom -
|
|
98
|
+
BOUNDS_CONSTRAINTS.horizontal -
|
|
99
|
+
nodeWidth,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const minY =
|
|
103
|
+
container.scrollTop / currentZoom + BOUNDS_CONSTRAINTS.vertical;
|
|
104
|
+
const maxY = Math.max(
|
|
105
|
+
minY,
|
|
106
|
+
(container.scrollTop + container.clientHeight) / currentZoom -
|
|
107
|
+
BOUNDS_CONSTRAINTS.vertical -
|
|
108
|
+
nodeHeight,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
x: Math.min(Math.max(x, minX), maxX),
|
|
113
|
+
y: Math.min(Math.max(y, minY), maxY),
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
service.addOptions(({ voidAction, batch, assign }) => ({
|
|
118
|
+
actions: {
|
|
119
|
+
placeChild: assign('context.data.nodes', {
|
|
120
|
+
ADD_CHILD: ({
|
|
121
|
+
payload,
|
|
122
|
+
context: { data },
|
|
123
|
+
pContext: { generatedId },
|
|
124
|
+
}) => {
|
|
125
|
+
const nodes = data?.nodes ?? [];
|
|
126
|
+
if (!payload) return nodes;
|
|
127
|
+
|
|
128
|
+
const parentNode = nodes.find(node => node.id === payload);
|
|
129
|
+
if (!parentNode) return nodes;
|
|
130
|
+
|
|
131
|
+
const id = `node-${generatedId}`;
|
|
132
|
+
const width = dimensions()[payload]?.width ?? 0;
|
|
133
|
+
const height = dimensions()[payload]?.height ?? 0;
|
|
134
|
+
const initialX =
|
|
135
|
+
parentNode.position.x + width + PARENT_CHILD_GAP_WIDTH;
|
|
136
|
+
const initialY = parentNode.position.y;
|
|
137
|
+
const position = clampPosition(
|
|
138
|
+
initialX,
|
|
139
|
+
initialY,
|
|
140
|
+
width,
|
|
141
|
+
height,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
return [
|
|
145
|
+
...nodes,
|
|
146
|
+
{
|
|
147
|
+
id,
|
|
148
|
+
data: { content: '<Nouveau nœud>' },
|
|
149
|
+
input: true,
|
|
150
|
+
position,
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
},
|
|
154
|
+
}),
|
|
155
|
+
|
|
156
|
+
placeParent: assign('context.data.nodes', {
|
|
157
|
+
ADD_PARENT: ({
|
|
158
|
+
context: { data },
|
|
159
|
+
pContext: { generatedId },
|
|
160
|
+
}) => {
|
|
161
|
+
const nodes = data?.nodes ?? [];
|
|
162
|
+
const id = `node-${generatedId}`;
|
|
163
|
+
const container = boardRef()?.parentElement;
|
|
164
|
+
const scrollLeft = container?.scrollLeft ?? 0;
|
|
165
|
+
const scrollTop = container?.scrollTop ?? 0;
|
|
166
|
+
const width = container?.clientWidth ?? 0;
|
|
167
|
+
const height = container?.clientHeight ?? 0;
|
|
168
|
+
const currentZoom = zoom[0]();
|
|
169
|
+
const x = (scrollLeft + width / 2) / currentZoom;
|
|
170
|
+
const y = (scrollTop + height / 2) / currentZoom;
|
|
171
|
+
|
|
172
|
+
return [
|
|
173
|
+
...nodes,
|
|
174
|
+
{
|
|
175
|
+
id,
|
|
176
|
+
data: { content: '<Nouveau nœud>' },
|
|
177
|
+
input: false,
|
|
178
|
+
position: { x, y },
|
|
179
|
+
},
|
|
180
|
+
];
|
|
181
|
+
},
|
|
182
|
+
}),
|
|
183
|
+
|
|
184
|
+
placeSibling: assign('context.data.nodes', {
|
|
185
|
+
ADD_SIBLING: ({
|
|
186
|
+
payload,
|
|
187
|
+
context: { data },
|
|
188
|
+
pContext: { generatedId },
|
|
189
|
+
}) => {
|
|
190
|
+
const edges = data?.edges ?? [];
|
|
191
|
+
const nodes = data?.nodes ?? [];
|
|
192
|
+
|
|
193
|
+
const parentID = edges.find(edge => edge.to === payload)?.from;
|
|
194
|
+
if (!parentID) return nodes;
|
|
195
|
+
|
|
196
|
+
const parentNode = nodes.find(node => node.id === parentID);
|
|
197
|
+
if (!parentNode) return nodes;
|
|
198
|
+
|
|
199
|
+
const id = `node-${generatedId}`;
|
|
200
|
+
const width = dimensions()[parentID]?.width ?? 0;
|
|
201
|
+
const height = dimensions()[parentID]?.height ?? 0;
|
|
202
|
+
const initialX =
|
|
203
|
+
parentNode.position.x + width + PARENT_CHILD_GAP_WIDTH;
|
|
204
|
+
const initialY = parentNode.position.y + 100;
|
|
205
|
+
const position = clampPosition(
|
|
206
|
+
initialX,
|
|
207
|
+
initialY,
|
|
208
|
+
width,
|
|
209
|
+
height,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
return [
|
|
213
|
+
...nodes,
|
|
214
|
+
{
|
|
215
|
+
id,
|
|
216
|
+
data: { content: '<Nouveau nœud>' },
|
|
217
|
+
input: true,
|
|
218
|
+
position,
|
|
219
|
+
},
|
|
220
|
+
];
|
|
221
|
+
},
|
|
222
|
+
}),
|
|
223
|
+
|
|
224
|
+
buildUI: batch(
|
|
225
|
+
voidAction(({ context: { data } }) => {
|
|
226
|
+
const edges = data?.edges ?? [];
|
|
227
|
+
setEdgesPositions(data => {
|
|
228
|
+
const array = Object.entries({ ...data }).filter(([id]) => {
|
|
229
|
+
return edges?.some(edge => edge.id === id);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return Object.fromEntries(array);
|
|
233
|
+
});
|
|
234
|
+
}),
|
|
235
|
+
voidAction({
|
|
236
|
+
else: ({ context: { data } }) => {
|
|
237
|
+
const edges = data?.edges ?? [];
|
|
238
|
+
setEdgesPositions(
|
|
239
|
+
produce(next => {
|
|
240
|
+
edges?.forEach(({ from, id, to }) => {
|
|
241
|
+
const output = dimensions()[from]?.output;
|
|
242
|
+
const input = dimensions()[to]?.input;
|
|
243
|
+
if (output && input) {
|
|
244
|
+
next[id] = {
|
|
245
|
+
x0: output.x,
|
|
246
|
+
y0: output.y,
|
|
247
|
+
x1: input.x,
|
|
248
|
+
y1: input.y,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}),
|
|
253
|
+
);
|
|
254
|
+
},
|
|
255
|
+
MOVE: ({ context: { data }, payload }) => {
|
|
256
|
+
const edges = data?.edges ?? [];
|
|
257
|
+
|
|
258
|
+
setEdgesPositions(
|
|
259
|
+
produce(next => {
|
|
260
|
+
edges?.forEach(({ from, to, id }) => {
|
|
261
|
+
if (from === payload.id) {
|
|
262
|
+
const offset =
|
|
263
|
+
dimensions()[payload.id]?.outputOffset ??
|
|
264
|
+
getDefaultOutputOffset(
|
|
265
|
+
dimensions()[payload.id]?.width,
|
|
266
|
+
);
|
|
267
|
+
const x0 = payload.x + offset.x;
|
|
268
|
+
const y0 = payload.y + offset.y;
|
|
269
|
+
next[id] = { ...next[id], x0, y0 };
|
|
270
|
+
setDimensions(
|
|
271
|
+
produce(data => {
|
|
272
|
+
if (data[payload.id]) {
|
|
273
|
+
data[payload.id] = {
|
|
274
|
+
...data[payload.id],
|
|
275
|
+
output: { x: x0, y: y0 },
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}),
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
if (to === payload.id) {
|
|
282
|
+
const offset =
|
|
283
|
+
dimensions()[payload.id]?.inputOffset ??
|
|
284
|
+
DEFAULT_INPUT_OFFSET;
|
|
285
|
+
const x1 = payload.x + offset.x;
|
|
286
|
+
const y1 = payload.y + offset.y;
|
|
287
|
+
next[id] = { ...next[id], x1, y1 };
|
|
288
|
+
setDimensions(
|
|
289
|
+
produce(data => {
|
|
290
|
+
if (data[payload.id]) {
|
|
291
|
+
data[payload.id] = {
|
|
292
|
+
...data[payload.id],
|
|
293
|
+
input: { x: x1, y: y1 },
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
302
|
+
},
|
|
303
|
+
}),
|
|
304
|
+
assign('context.updatingUI', () => true),
|
|
305
|
+
),
|
|
306
|
+
|
|
307
|
+
buildImmediateUI: voidAction({
|
|
308
|
+
MOVE_IMMEDIATE: ({ context: { data }, payload }) => {
|
|
309
|
+
const edges = data?.edges ?? [];
|
|
310
|
+
|
|
311
|
+
setEdgesPositions(
|
|
312
|
+
produce(next => {
|
|
313
|
+
edges?.forEach(({ from, to, id }) => {
|
|
314
|
+
if (from === payload.id) {
|
|
315
|
+
const offset =
|
|
316
|
+
dimensions()[payload.id]?.outputOffset ??
|
|
317
|
+
getDefaultOutputOffset(
|
|
318
|
+
dimensions()[payload.id]?.width,
|
|
319
|
+
);
|
|
320
|
+
const x0 = payload.x + offset.x;
|
|
321
|
+
const y0 = payload.y + offset.y;
|
|
322
|
+
next[id] = { ...next[id], x0, y0 };
|
|
323
|
+
setDimensions(
|
|
324
|
+
produce(data => {
|
|
325
|
+
if (data[payload.id]) {
|
|
326
|
+
data[payload.id] = {
|
|
327
|
+
...data[payload.id],
|
|
328
|
+
output: { x: x0, y: y0 },
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}),
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
if (to === payload.id) {
|
|
335
|
+
const offset =
|
|
336
|
+
dimensions()[payload.id]?.inputOffset ??
|
|
337
|
+
DEFAULT_INPUT_OFFSET;
|
|
338
|
+
const x1 = payload.x + offset.x;
|
|
339
|
+
const y1 = payload.y + offset.y;
|
|
340
|
+
next[id] = { ...next[id], x1, y1 };
|
|
341
|
+
setDimensions(
|
|
342
|
+
produce(data => {
|
|
343
|
+
if (data[payload.id]) {
|
|
344
|
+
data[payload.id] = {
|
|
345
|
+
...data[payload.id],
|
|
346
|
+
input: { x: x1, y: y1 },
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
}),
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
},
|
|
356
|
+
}),
|
|
357
|
+
},
|
|
358
|
+
}));
|
|
359
|
+
|
|
360
|
+
service.start();
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
dimensions: [dimensions, setDimensions] as const,
|
|
364
|
+
newEdge,
|
|
365
|
+
board: [boardRef, setBoardRef] as const,
|
|
366
|
+
getBoardPoint,
|
|
367
|
+
edgesPositions: [edgesPositions, setEdgesPositions] as const,
|
|
368
|
+
service,
|
|
369
|
+
zoom,
|
|
370
|
+
};
|
|
371
|
+
},
|
|
372
|
+
{ name: 'FlowContext' },
|
|
373
|
+
);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { NodeProps } from './FlowChart';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Horizontal gap in pixels between a parent node and newly created child
|
|
5
|
+
* node.
|
|
6
|
+
*/
|
|
7
|
+
export const PARENT_CHILD_GAP_WIDTH = 100;
|
|
8
|
+
|
|
9
|
+
// #region Node & Handle Layout Constants & Formulas
|
|
10
|
+
/** Border width in pixels for a node container. */
|
|
11
|
+
export const NODE_BORDER_WIDTH = 1.5;
|
|
12
|
+
|
|
13
|
+
/** Diameter in pixels of a node connection handle. */
|
|
14
|
+
export const HANDLE_SIZE = 12;
|
|
15
|
+
|
|
16
|
+
/** Radius in pixels of a node connection handle. */
|
|
17
|
+
export const HANDLE_RADIUS = HANDLE_SIZE / 2;
|
|
18
|
+
|
|
19
|
+
/** Top margin in pixels for positioning connection handles. */
|
|
20
|
+
export const HANDLE_MARGIN_TOP = 12;
|
|
21
|
+
|
|
22
|
+
/** Horizontal container offset in pixels for handle placement. */
|
|
23
|
+
export const HANDLE_CONTAINER_OFFSET_X = 18;
|
|
24
|
+
|
|
25
|
+
/** Y-axis center coordinate in pixels of a handle. */
|
|
26
|
+
export const HANDLE_CENTER_Y = HANDLE_MARGIN_TOP + HANDLE_RADIUS;
|
|
27
|
+
|
|
28
|
+
/** X-axis center offset in pixels from the node container edge. */
|
|
29
|
+
export const HANDLE_CENTER_X_OFFSET =
|
|
30
|
+
HANDLE_CONTAINER_OFFSET_X - NODE_BORDER_WIDTH - HANDLE_RADIUS;
|
|
31
|
+
|
|
32
|
+
/** Default X offset in pixels for input connection handles. */
|
|
33
|
+
export const DEFAULT_INPUT_OFFSET_X = -HANDLE_CENTER_X_OFFSET;
|
|
34
|
+
|
|
35
|
+
/** Default Y offset in pixels for input connection handles. */
|
|
36
|
+
export const DEFAULT_INPUT_OFFSET_Y = HANDLE_CENTER_Y;
|
|
37
|
+
|
|
38
|
+
/** Default 2D offset coordinates for input connection handles. */
|
|
39
|
+
export const DEFAULT_INPUT_OFFSET = {
|
|
40
|
+
x: DEFAULT_INPUT_OFFSET_X,
|
|
41
|
+
y: DEFAULT_INPUT_OFFSET_Y,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Dimensions representing width and height of the flow chart container
|
|
46
|
+
* canvas.
|
|
47
|
+
*/
|
|
48
|
+
export const CONTAINER_DIMENSIONS = { WIDTH: 5000, HEIGHT: 3500 };
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Computes default output handle offset coordinates given a node width.
|
|
52
|
+
*
|
|
53
|
+
* @param width - Node width in pixels, defaults to `0`.
|
|
54
|
+
*
|
|
55
|
+
* @returns 2D offset coordinates for the output handle.
|
|
56
|
+
*/
|
|
57
|
+
export const getDefaultOutputOffset = (width = 0) => ({
|
|
58
|
+
x: width + HANDLE_CENTER_X_OFFSET,
|
|
59
|
+
y: HANDLE_CENTER_Y,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/** Top offset in pixels for node action toolbars. */
|
|
63
|
+
export const TOOLBAR_TOP_OFFSET = 30;
|
|
64
|
+
|
|
65
|
+
/** Safety buffer in pixels applied above the toolbar. */
|
|
66
|
+
export const TOOLBAR_BUFFER = 5;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Boundary padding constraints for node dragging and positioning within
|
|
70
|
+
* the viewport.
|
|
71
|
+
*/
|
|
72
|
+
export const BOUNDS_CONSTRAINTS = {
|
|
73
|
+
horizontal: HANDLE_CONTAINER_OFFSET_X + 2,
|
|
74
|
+
vertical: TOOLBAR_TOP_OFFSET + TOOLBAR_BUFFER,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** Multiplier factor used to compute canvas virtual scroll dimensions. */
|
|
78
|
+
export const CANVAS_FACTOR = 5;
|
|
79
|
+
// #endregion
|
|
80
|
+
|
|
81
|
+
/** Default node items provided when no initial configuration is given. */
|
|
82
|
+
export const DEFAULT_NODES: (NodeProps & { id: string })[] = [
|
|
83
|
+
{
|
|
84
|
+
id: 'node-0',
|
|
85
|
+
data: { content: 'Some text', label: 'Root node' },
|
|
86
|
+
input: false,
|
|
87
|
+
position: { x: 350, y: 100 },
|
|
88
|
+
},
|
|
89
|
+
];
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { inferT } from '@bemedev/app/typings';
|
|
2
|
+
import { Component, onCleanup, onMount } from 'solid-js';
|
|
3
|
+
import type { edgeJSON, nodeJSON } from '../../services/main.typings';
|
|
4
|
+
import { useFlow } from './FlowChart.context';
|
|
5
|
+
import { NodesBoard } from './NodesBoard';
|
|
6
|
+
import { DEFAULT_NODES } from './FlowChart.data';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Serialized node properties type inferred from schema
|
|
10
|
+
* {@linkcode nodeJSON}.
|
|
11
|
+
*/
|
|
12
|
+
export type NodeProps = inferT<typeof nodeJSON>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Serialized edge properties type inferred from schema
|
|
16
|
+
* {@linkcode edgeJSON}.
|
|
17
|
+
*/
|
|
18
|
+
export type EdgeProps = inferT<typeof edgeJSON>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Configuration options and callback handlers for the {@linkcode FlowChart}
|
|
22
|
+
* component.
|
|
23
|
+
*/
|
|
24
|
+
export type FlowProps = {
|
|
25
|
+
/** Initial flowchart state configuration with nodes and edges. */
|
|
26
|
+
config?: {
|
|
27
|
+
nodes?: (NodeProps & { id: string })[];
|
|
28
|
+
edges?: (EdgeProps & { id: string })[];
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Callback triggered when a new node is created.
|
|
32
|
+
*
|
|
33
|
+
* @param node - The created node object of type {@linkcode NodeProps}.
|
|
34
|
+
*/
|
|
35
|
+
onNodeAdded?: (node: NodeProps) => void;
|
|
36
|
+
/**
|
|
37
|
+
* Callback triggered when a node is deleted.
|
|
38
|
+
*
|
|
39
|
+
* @param nodeId - The identifier of the deleted node.
|
|
40
|
+
*/
|
|
41
|
+
onNodeDeleted?: (nodeId: string) => void;
|
|
42
|
+
/**
|
|
43
|
+
* Callback triggered when an edge is created.
|
|
44
|
+
*
|
|
45
|
+
* @param edge - The created edge object of type {@linkcode EdgeProps}.
|
|
46
|
+
*/
|
|
47
|
+
onEdgeAdded?: (edge: EdgeProps) => void;
|
|
48
|
+
/**
|
|
49
|
+
* Callback triggered when an edge is deleted.
|
|
50
|
+
*
|
|
51
|
+
* @param edgeId - The identifier of the deleted edge.
|
|
52
|
+
*/
|
|
53
|
+
onEdgeDeleted?: (edgeId: string) => void;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// const PARENT_CHILD_GAP_WIDTH = 75;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Flowchart board canvas component that renders interactive nodes, edges,
|
|
60
|
+
* pan/zoom, and toolbar controls.
|
|
61
|
+
*
|
|
62
|
+
* @param props - Flowchart configuration and event handlers of type
|
|
63
|
+
* {@linkcode FlowProps}.
|
|
64
|
+
*
|
|
65
|
+
* @returns The rendered Solid component.
|
|
66
|
+
*/
|
|
67
|
+
export const FlowChart: Component<FlowProps> = props => {
|
|
68
|
+
const primaryNodes = props.config?.nodes ?? DEFAULT_NODES;
|
|
69
|
+
const primaryEdges = props.config?.edges;
|
|
70
|
+
|
|
71
|
+
const {
|
|
72
|
+
service,
|
|
73
|
+
newEdge: [newEdge, setNewEdge],
|
|
74
|
+
getBoardPoint,
|
|
75
|
+
} = useFlow();
|
|
76
|
+
|
|
77
|
+
onMount(() => {
|
|
78
|
+
service.start();
|
|
79
|
+
service.send({
|
|
80
|
+
type: 'CONFIGURE',
|
|
81
|
+
payload: { nodes: primaryNodes, edges: primaryEdges ?? [] },
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
onCleanup(service.stop);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
class='relative h-full w-full'
|
|
90
|
+
onMouseUp={() => setNewEdge()}
|
|
91
|
+
onMouseMove={event => {
|
|
92
|
+
const edge = newEdge();
|
|
93
|
+
if (edge) {
|
|
94
|
+
const boardPoint = getBoardPoint(event.clientX, event.clientY);
|
|
95
|
+
setNewEdge({ ...edge, x1: boardPoint.x, y1: boardPoint.y });
|
|
96
|
+
}
|
|
97
|
+
}}
|
|
98
|
+
style={{}}
|
|
99
|
+
>
|
|
100
|
+
<div
|
|
101
|
+
class='relative h-full w-full bg-white bg-size-[30px_30px]'
|
|
102
|
+
style={{
|
|
103
|
+
cursor: newEdge() ? 'inherit' : 'crosshair',
|
|
104
|
+
'background-image':
|
|
105
|
+
'radial-gradient(circle, #b8b8b8bf 1px, rgba(0, 0, 0, 0) 1px)',
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<NodesBoard />
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
};
|