@bemedev/mind-flow 0.0.1 → 0.1.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/README.md +95 -0
- package/lib/helpers/createContext.d.ts +8 -11
- package/lib/helpers/createContext.d.ts.map +1 -1
- package/lib/index.cjs.js +1 -1
- package/lib/index.es.js +240 -235
- package/lib/server.cjs.js +181 -121
- package/lib/server.es.js +181 -121
- package/lib/services/main.machine.d.ts +7 -9
- package/lib/services/main.machine.d.ts.map +1 -1
- package/lib/services/main.typings.d.ts +3 -3
- package/lib/services/main.typings.d.ts.map +1 -1
- package/lib/ui/Flow.d.ts +2 -2
- package/lib/ui/Flow.d.ts.map +1 -1
- package/lib/ui/components/Bounds.d.ts +4 -4
- package/lib/ui/components/Bounds.d.ts.map +1 -1
- package/lib/ui/components/EdgeComponent.d.ts +2 -2
- package/lib/ui/components/EdgeComponent.d.ts.map +1 -1
- package/lib/ui/components/EdgesBoard.d.ts +2 -2
- package/lib/ui/components/EdgesBoard.d.ts.map +1 -1
- package/lib/ui/components/FlowChart.context.d.ts +10 -18
- package/lib/ui/components/FlowChart.context.d.ts.map +1 -1
- package/lib/ui/components/FlowChart.d.ts +4 -10
- package/lib/ui/components/FlowChart.d.ts.map +1 -1
- package/lib/ui/components/FlowChart.data.d.ts +10 -11
- package/lib/ui/components/FlowChart.data.d.ts.map +1 -1
- package/lib/ui/components/NodeComponent.d.ts +2 -2
- package/lib/ui/components/NodeComponent.d.ts.map +1 -1
- package/lib/ui/components/NodesBoard.d.ts +2 -2
- package/lib/ui/components/NodesBoard.d.ts.map +1 -1
- package/lib/ui/components/classes.d.ts +2 -2
- package/package.json +3 -9
- package/src/README.md +2 -2
- package/src/helpers/createContext.ts +8 -11
- package/src/services/main.machine.ts +18 -46
- package/src/services/main.typings.ts +3 -3
- package/src/ui/Flow.tsx +3 -2
- package/src/ui/components/Bounds.tsx +35 -44
- package/src/ui/components/EdgeComponent.tsx +7 -8
- package/src/ui/components/EdgesBoard.tsx +3 -2
- package/src/ui/components/FlowChart.context.ts +75 -82
- package/src/ui/components/FlowChart.data.ts +12 -15
- package/src/ui/components/FlowChart.tsx +6 -11
- package/src/ui/components/NodeComponent.tsx +13 -25
- package/src/ui/components/NodesBoard.tsx +120 -111
- package/src/ui/components/classes.ts +2 -2
|
@@ -5,35 +5,26 @@ import {
|
|
|
5
5
|
DragOverlay,
|
|
6
6
|
} from '@thisbeyond/solid-dnd';
|
|
7
7
|
import { dequal } from 'dequal';
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
createEffect,
|
|
11
|
-
createSignal,
|
|
12
|
-
For,
|
|
13
|
-
on,
|
|
14
|
-
onCleanup,
|
|
15
|
-
Show,
|
|
16
|
-
} from 'solid-js';
|
|
8
|
+
import { Component, createEffect, createSignal, For, on, Show } from 'solid-js';
|
|
9
|
+
|
|
17
10
|
import { DragBounds } from './Bounds';
|
|
18
11
|
import { EdgesBoard } from './EdgesBoard';
|
|
19
12
|
import { useFlow } from './FlowChart.context';
|
|
13
|
+
import { CANVAS_FACTOR, SCROLL_MULTIPLIER } from './FlowChart.data';
|
|
20
14
|
import { NodeComponent } from './NodeComponent';
|
|
21
|
-
import { CANVAS_FACTOR } from './FlowChart.data';
|
|
22
15
|
|
|
23
16
|
/**
|
|
24
|
-
* Interactive board component containing the drag-drop viewport, zoom
|
|
25
|
-
*
|
|
17
|
+
* Interactive board component containing the drag-drop viewport, zoom controls,
|
|
18
|
+
* panning gestures, and rendered nodes/edges.
|
|
26
19
|
*
|
|
27
20
|
* @returns The rendered Solid component.
|
|
28
21
|
*/
|
|
29
22
|
export const NodesBoard: Component = () => {
|
|
30
|
-
let containerRef: HTMLDivElement
|
|
23
|
+
let containerRef: HTMLDivElement;
|
|
31
24
|
const [isPanning, setIsPanning] = createSignal(false);
|
|
32
25
|
const [transform, setTransform] = createSignal({ x: 0, y: 0 });
|
|
33
26
|
const [id, setId] = createSignal<string | number>('');
|
|
34
27
|
const [previousZoom, setPreviousZoom] = createSignal<number>();
|
|
35
|
-
let cleanupPanning = () => {};
|
|
36
|
-
onCleanup(cleanupPanning);
|
|
37
28
|
let percentX = 0;
|
|
38
29
|
let percentY = 0;
|
|
39
30
|
|
|
@@ -44,11 +35,13 @@ export const NodesBoard: Component = () => {
|
|
|
44
35
|
zoom: [zoom, setZoom],
|
|
45
36
|
} = useFlow();
|
|
46
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Calculates and saves the normalized scroll percentages across X and Y axes for
|
|
40
|
+
* preserving viewport alignment on zoom changes.
|
|
41
|
+
*/
|
|
47
42
|
const updateScrollPercentages = () => {
|
|
48
|
-
if (!containerRef) return;
|
|
49
43
|
const maxScrollX = containerRef.scrollWidth - containerRef.clientWidth;
|
|
50
|
-
const maxScrollY =
|
|
51
|
-
containerRef.scrollHeight - containerRef.clientHeight;
|
|
44
|
+
const maxScrollY = containerRef.scrollHeight - containerRef.clientHeight;
|
|
52
45
|
percentX = maxScrollX > 0 ? containerRef.scrollLeft / maxScrollX : 0;
|
|
53
46
|
percentY = maxScrollY > 0 ? containerRef.scrollTop / maxScrollY : 0;
|
|
54
47
|
};
|
|
@@ -57,23 +50,24 @@ export const NodesBoard: Component = () => {
|
|
|
57
50
|
on(
|
|
58
51
|
zoom,
|
|
59
52
|
() => {
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
const maxScrollY =
|
|
64
|
-
containerRef.scrollHeight - containerRef.clientHeight;
|
|
65
|
-
if (maxScrollX > 0)
|
|
66
|
-
containerRef.scrollLeft = percentX * maxScrollX;
|
|
53
|
+
const maxScrollX = containerRef.scrollWidth - containerRef.clientWidth;
|
|
54
|
+
const maxScrollY = containerRef.scrollHeight - containerRef.clientHeight;
|
|
55
|
+
if (maxScrollX > 0) containerRef.scrollLeft = percentX * maxScrollX;
|
|
67
56
|
if (maxScrollY > 0) containerRef.scrollTop = percentY * maxScrollY;
|
|
68
57
|
},
|
|
69
58
|
{ defer: true },
|
|
70
59
|
),
|
|
71
60
|
);
|
|
72
61
|
|
|
73
|
-
const selectedId = useState(service, {
|
|
74
|
-
selector: s => s.context?.selected,
|
|
75
|
-
});
|
|
62
|
+
const selectedId = useState(service, { selector: s => s.context?.selected });
|
|
76
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Determines whether the given element ID matches the currently selected entity.
|
|
66
|
+
*
|
|
67
|
+
* @param id - The node or edge identifier to check.
|
|
68
|
+
*
|
|
69
|
+
* @returns `true` if the item is currently selected, otherwise `false`.
|
|
70
|
+
*/
|
|
77
71
|
const selected = (id: string | number) => selectedId() === id;
|
|
78
72
|
|
|
79
73
|
const nodes = useState(service, {
|
|
@@ -91,76 +85,93 @@ export const NodesBoard: Component = () => {
|
|
|
91
85
|
equals: dequal,
|
|
92
86
|
});
|
|
93
87
|
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return CANVAS_FACTOR * 100;
|
|
100
|
-
};
|
|
88
|
+
const CANVAS_SIZE = CANVAS_FACTOR * 100;
|
|
89
|
+
const MARGIN_X = 53 * CANVAS_FACTOR;
|
|
90
|
+
const MARGIN_Y = 85 * CANVAS_FACTOR;
|
|
91
|
+
const cWidth = () => `calc((${CANVAS_SIZE}vw - ${MARGIN_X}px) * ${zoom()})`;
|
|
92
|
+
const cHeight = () => `calc((${CANVAS_SIZE}vh - ${MARGIN_Y}px) * ${zoom()})`;
|
|
101
93
|
|
|
102
94
|
return (
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const y = node.offsetTop + _transform.y / zoom();
|
|
112
|
-
overlay?.node.style.setProperty('top', y * 2 + 'px');
|
|
113
|
-
overlay?.node.style.setProperty('left', x * 2 + 'px');
|
|
114
|
-
const deltaX = _transform.x / zoom();
|
|
115
|
-
const deltaY = _transform.y / zoom();
|
|
116
|
-
// Directly update the draggable node's CSS transform adjusted for zoom:
|
|
117
|
-
node.style.setProperty(
|
|
118
|
-
'transform',
|
|
119
|
-
`translate3d(${deltaX}px, ${deltaY}px, 0)`,
|
|
95
|
+
<div
|
|
96
|
+
onWheel={e => {
|
|
97
|
+
if (e.ctrlKey || e.metaKey) {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
updateScrollPercentages();
|
|
100
|
+
const delta = e.deltaY < 0 ? 0.1 : -0.1;
|
|
101
|
+
setZoom(prev =>
|
|
102
|
+
Math.min(Math.max(Number((prev + delta).toFixed(2)), 0.2), 3),
|
|
120
103
|
);
|
|
121
|
-
|
|
122
|
-
service.send({
|
|
123
|
-
type: 'MOVE_IMMEDIATE',
|
|
124
|
-
payload: { id: `${id}`, x, y },
|
|
125
|
-
});
|
|
126
|
-
setTransform({ ..._transform });
|
|
127
104
|
}
|
|
128
105
|
}}
|
|
129
106
|
|
|
130
|
-
|
|
131
|
-
|
|
107
|
+
class='relative mx-auto h-[calc(100vh-64px)] w-[calc(100vw-32px)] overflow-hidden'
|
|
108
|
+
>
|
|
109
|
+
<DragDropProvider
|
|
110
|
+
onDragMove={({ draggable: { transform: _transform, node, id } }) => {
|
|
111
|
+
setId(id);
|
|
112
|
+
if (selected(id)) {
|
|
113
|
+
const grandParent = node.parentElement?.parentElement;
|
|
114
|
+
const currentZoom = zoom();
|
|
115
|
+
const minX = 0;
|
|
116
|
+
const maxX = grandParent
|
|
117
|
+
? Math.max(0, grandParent.clientWidth / currentZoom - node.offsetWidth)
|
|
118
|
+
: Infinity;
|
|
119
|
+
const minY = 0;
|
|
120
|
+
const maxY = grandParent
|
|
121
|
+
? Math.max(
|
|
122
|
+
0,
|
|
123
|
+
grandParent.clientHeight / currentZoom - node.offsetHeight,
|
|
124
|
+
)
|
|
125
|
+
: Infinity;
|
|
132
126
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
node.style.setProperty('top', Y + 'px');
|
|
136
|
-
node.style.setProperty('left', X + 'px');
|
|
137
|
-
node.style.removeProperty('transform');
|
|
127
|
+
const targetX = node.offsetLeft + _transform.x / currentZoom;
|
|
128
|
+
const targetY = node.offsetTop + _transform.y / currentZoom;
|
|
138
129
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
onWheel={e => {
|
|
150
|
-
if (e.ctrlKey || e.metaKey) {
|
|
151
|
-
e.preventDefault();
|
|
152
|
-
updateScrollPercentages();
|
|
153
|
-
const delta = e.deltaY < 0 ? 0.1 : -0.1;
|
|
154
|
-
setZoom(prev =>
|
|
155
|
-
Math.min(
|
|
156
|
-
Math.max(Number((prev + delta).toFixed(2)), 0.2),
|
|
157
|
-
3,
|
|
158
|
-
),
|
|
130
|
+
const x = Math.min(Math.max(targetX, minX), maxX);
|
|
131
|
+
const y = Math.min(Math.max(targetY, minY), maxY);
|
|
132
|
+
|
|
133
|
+
const deltaX = x - node.offsetLeft;
|
|
134
|
+
const deltaY = y - node.offsetTop;
|
|
135
|
+
|
|
136
|
+
// Directly update the draggable node's CSS transform adjusted for zoom:
|
|
137
|
+
node.style.setProperty(
|
|
138
|
+
'transform',
|
|
139
|
+
`translate3d(${deltaX}px, ${deltaY}px, 0)`,
|
|
159
140
|
);
|
|
141
|
+
|
|
142
|
+
service.send({ type: 'MOVE_IMMEDIATE', payload: { id: `${id}`, x, y } });
|
|
143
|
+
setTransform({ x: deltaX * currentZoom, y: deltaY * currentZoom });
|
|
160
144
|
}
|
|
161
145
|
}}
|
|
162
146
|
|
|
163
|
-
|
|
147
|
+
onDragEnd={({ draggable: { node, id } }) => {
|
|
148
|
+
if (!selected(id)) return;
|
|
149
|
+
|
|
150
|
+
const grandParent = node.parentElement?.parentElement;
|
|
151
|
+
const currentZoom = zoom();
|
|
152
|
+
const minX = 0;
|
|
153
|
+
const maxX = grandParent
|
|
154
|
+
? Math.max(0, grandParent.clientWidth / currentZoom - node.offsetWidth)
|
|
155
|
+
: Infinity;
|
|
156
|
+
const minY = 0;
|
|
157
|
+
const maxY = grandParent
|
|
158
|
+
? Math.max(0, grandParent.clientHeight / currentZoom - node.offsetHeight)
|
|
159
|
+
: Infinity;
|
|
160
|
+
|
|
161
|
+
const rawX = node.offsetLeft + transform().x / currentZoom;
|
|
162
|
+
const rawY = node.offsetTop + transform().y / currentZoom;
|
|
163
|
+
const X = Math.min(Math.max(rawX, minX), maxX);
|
|
164
|
+
const Y = Math.min(Math.max(rawY, minY), maxY);
|
|
165
|
+
|
|
166
|
+
node.style.setProperty('top', Y + 'px');
|
|
167
|
+
node.style.setProperty('left', X + 'px');
|
|
168
|
+
node.style.removeProperty('transform');
|
|
169
|
+
|
|
170
|
+
setTimeout(() => {
|
|
171
|
+
service.send({ type: 'MOVE', payload: { id: `${id}`, x: X, y: Y } });
|
|
172
|
+
setTransform({ x: 0, y: 0 });
|
|
173
|
+
}, 0);
|
|
174
|
+
}}
|
|
164
175
|
>
|
|
165
176
|
<div
|
|
166
177
|
ref={el => {
|
|
@@ -170,21 +181,14 @@ export const NodesBoard: Component = () => {
|
|
|
170
181
|
class='relative h-full w-full overflow-scroll rounded-lg border-2 border-gray-600'
|
|
171
182
|
>
|
|
172
183
|
<DragDropSensors />
|
|
173
|
-
<DragBounds />
|
|
174
184
|
<div
|
|
175
185
|
ref={setRef}
|
|
176
|
-
class='relative cursor-crosshair'
|
|
186
|
+
class='relative cursor-crosshair overflow-hidden'
|
|
177
187
|
classList={{ 'cursor-grabbing': isPanning() }}
|
|
178
|
-
style={{
|
|
179
|
-
|
|
180
|
-
width: `calc(${cDim()}vw - 53px)`,
|
|
181
|
-
scale: zoom(),
|
|
182
|
-
'transform-origin': 'top left',
|
|
183
|
-
}}
|
|
188
|
+
style={{ height: cHeight(), width: cWidth() }}
|
|
189
|
+
|
|
184
190
|
onMouseDown={e => {
|
|
185
191
|
if (newEdge() || e.button !== 0) return;
|
|
186
|
-
if (!containerRef) return;
|
|
187
|
-
|
|
188
192
|
service.send('DESELECT');
|
|
189
193
|
|
|
190
194
|
// #region Props
|
|
@@ -197,11 +201,10 @@ export const NodesBoard: Component = () => {
|
|
|
197
201
|
setIsPanning(true);
|
|
198
202
|
|
|
199
203
|
const handleMouseMove = (moveEvent: MouseEvent) => {
|
|
200
|
-
if (!containerRef) return;
|
|
201
204
|
const dx = moveEvent.clientX - startX;
|
|
202
205
|
const dy = moveEvent.clientY - startY;
|
|
203
|
-
containerRef.scrollLeft = startScrollLeft - dx *
|
|
204
|
-
containerRef.scrollTop = startScrollTop - dy *
|
|
206
|
+
containerRef.scrollLeft = startScrollLeft - dx * SCROLL_MULTIPLIER;
|
|
207
|
+
containerRef.scrollTop = startScrollTop - dy * SCROLL_MULTIPLIER;
|
|
205
208
|
updateScrollPercentages();
|
|
206
209
|
};
|
|
207
210
|
|
|
@@ -209,18 +212,22 @@ export const NodesBoard: Component = () => {
|
|
|
209
212
|
window.removeEventListener('mousemove', handleMouseMove);
|
|
210
213
|
window.removeEventListener('mouseup', handleMouseUp);
|
|
211
214
|
setTimeout(() => setIsPanning(false), 200);
|
|
212
|
-
(cleanupPanning as any) = undefined;
|
|
213
215
|
};
|
|
214
216
|
|
|
215
217
|
// #region Attach Windows listeners
|
|
216
|
-
cleanupPanning = handleMouseUp;
|
|
217
218
|
window.addEventListener('mousemove', handleMouseMove);
|
|
218
219
|
window.addEventListener('mouseup', handleMouseUp);
|
|
219
220
|
// #endregion
|
|
220
221
|
}}
|
|
221
222
|
>
|
|
222
|
-
<
|
|
223
|
-
|
|
223
|
+
<div
|
|
224
|
+
class='relative h-full w-full'
|
|
225
|
+
style={{ scale: zoom(), 'transform-origin': 'top left' }}
|
|
226
|
+
>
|
|
227
|
+
<DragBounds />
|
|
228
|
+
<EdgesBoard />
|
|
229
|
+
<For each={nodes()} children={NodeComponent} />
|
|
230
|
+
</div>
|
|
224
231
|
</div>
|
|
225
232
|
</div>
|
|
226
233
|
|
|
@@ -233,7 +240,7 @@ export const NodesBoard: Component = () => {
|
|
|
233
240
|
updateScrollPercentages();
|
|
234
241
|
setPreviousZoom(undefined);
|
|
235
242
|
setZoom(prev =>
|
|
236
|
-
Math.max(
|
|
243
|
+
Math.max(1 / CANVAS_FACTOR, Number((prev - 0.1).toFixed(2))),
|
|
237
244
|
);
|
|
238
245
|
}}
|
|
239
246
|
title='Zoom out'
|
|
@@ -266,7 +273,10 @@ export const NodesBoard: Component = () => {
|
|
|
266
273
|
updateScrollPercentages();
|
|
267
274
|
setPreviousZoom(undefined);
|
|
268
275
|
setZoom(prev =>
|
|
269
|
-
Math.min(
|
|
276
|
+
Math.min(
|
|
277
|
+
Math.min(3, CANVAS_FACTOR),
|
|
278
|
+
Number((prev + 0.1).toFixed(2)),
|
|
279
|
+
),
|
|
270
280
|
);
|
|
271
281
|
}}
|
|
272
282
|
title='Zoom in'
|
|
@@ -288,11 +298,10 @@ export const NodesBoard: Component = () => {
|
|
|
288
298
|
</svg>
|
|
289
299
|
</button>
|
|
290
300
|
</div>
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
</DragDropProvider>
|
|
301
|
+
<Show when={!id() || !selected(id())}>
|
|
302
|
+
<DragOverlay children='' />
|
|
303
|
+
</Show>
|
|
304
|
+
</DragDropProvider>
|
|
305
|
+
</div>
|
|
297
306
|
);
|
|
298
307
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* List of Tailwind CSS class names used across flowchart UI components to
|
|
3
|
-
*
|
|
2
|
+
* List of Tailwind CSS class names used across flowchart UI components to ensure
|
|
3
|
+
* compilation safelist.
|
|
4
4
|
*/
|
|
5
5
|
export const CLASSES = [
|
|
6
6
|
'fill-transparent',
|