pict-section-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.
Files changed (23) hide show
  1. package/LICENSE +21 -0
  2. package/example_application/css/flowexample.css +65 -0
  3. package/example_application/html/index.html +32 -0
  4. package/example_application/package.json +41 -0
  5. package/example_application/source/Pict-Application-FlowExample-Configuration.json +15 -0
  6. package/example_application/source/Pict-Application-FlowExample.js +241 -0
  7. package/example_application/source/providers/PictRouter-FlowExample-Configuration.json +22 -0
  8. package/example_application/source/views/PictView-FlowExample-About.js +184 -0
  9. package/example_application/source/views/PictView-FlowExample-BottomBar.js +77 -0
  10. package/example_application/source/views/PictView-FlowExample-Documentation.js +325 -0
  11. package/example_application/source/views/PictView-FlowExample-Layout.js +86 -0
  12. package/example_application/source/views/PictView-FlowExample-MainWorkspace.js +191 -0
  13. package/example_application/source/views/PictView-FlowExample-TopBar.js +95 -0
  14. package/package.json +22 -0
  15. package/source/Pict-Section-Flow.js +19 -0
  16. package/source/providers/PictProvider-Flow-EventHandler.js +158 -0
  17. package/source/providers/PictProvider-Flow-NodeTypes.js +174 -0
  18. package/source/services/PictService-Flow-ConnectionRenderer.js +251 -0
  19. package/source/services/PictService-Flow-InteractionManager.js +567 -0
  20. package/source/services/PictService-Flow-Layout.js +207 -0
  21. package/source/views/PictView-Flow-Node.js +267 -0
  22. package/source/views/PictView-Flow-Toolbar.js +223 -0
  23. package/source/views/PictView-Flow.js +1116 -0
@@ -0,0 +1,567 @@
1
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
2
+
3
+ /**
4
+ * Interaction states for the flow diagram
5
+ */
6
+ const INTERACTION_STATES =
7
+ {
8
+ IDLE: 'idle',
9
+ DRAGGING_NODE: 'dragging-node',
10
+ CONNECTING: 'connecting',
11
+ PANNING: 'panning'
12
+ };
13
+
14
+ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
15
+ {
16
+ constructor(pFable, pOptions, pServiceHash)
17
+ {
18
+ super(pFable, pOptions, pServiceHash);
19
+
20
+ this.serviceType = 'PictServiceFlowInteractionManager';
21
+
22
+ this._FlowView = (pOptions && pOptions.FlowView) ? pOptions.FlowView : null;
23
+
24
+ this._SVGElement = null;
25
+ this._ViewportElement = null;
26
+
27
+ // Interaction state
28
+ this._State = INTERACTION_STATES.IDLE;
29
+
30
+ // Drag state
31
+ this._DragNodeHash = null;
32
+ this._DragStartX = 0;
33
+ this._DragStartY = 0;
34
+ this._DragNodeStartX = 0;
35
+ this._DragNodeStartY = 0;
36
+
37
+ // Pan state
38
+ this._PanStartX = 0;
39
+ this._PanStartY = 0;
40
+ this._PanStartPanX = 0;
41
+ this._PanStartPanY = 0;
42
+
43
+ // Connection drag state
44
+ this._ConnectSourceNodeHash = null;
45
+ this._ConnectSourcePortHash = null;
46
+ this._ConnectDragLine = null;
47
+
48
+ // Bound event handlers (for removeEventListener)
49
+ this._boundOnPointerDown = this._onPointerDown.bind(this);
50
+ this._boundOnPointerMove = this._onPointerMove.bind(this);
51
+ this._boundOnPointerUp = this._onPointerUp.bind(this);
52
+ this._boundOnWheel = this._onWheel.bind(this);
53
+ this._boundOnKeyDown = this._onKeyDown.bind(this);
54
+ }
55
+
56
+ /**
57
+ * Initialize event listeners on the SVG element
58
+ * @param {SVGSVGElement} pSVGElement
59
+ * @param {SVGGElement} pViewportElement
60
+ */
61
+ initialize(pSVGElement, pViewportElement)
62
+ {
63
+ this._SVGElement = pSVGElement;
64
+ this._ViewportElement = pViewportElement;
65
+
66
+ if (!this._SVGElement) return;
67
+
68
+ // Use pointer events for unified mouse/touch handling
69
+ this._SVGElement.addEventListener('pointerdown', this._boundOnPointerDown);
70
+ this._SVGElement.addEventListener('pointermove', this._boundOnPointerMove);
71
+ this._SVGElement.addEventListener('pointerup', this._boundOnPointerUp);
72
+ this._SVGElement.addEventListener('pointerleave', this._boundOnPointerUp);
73
+ this._SVGElement.addEventListener('wheel', this._boundOnWheel, { passive: false });
74
+
75
+ // Keyboard events for delete
76
+ document.addEventListener('keydown', this._boundOnKeyDown);
77
+
78
+ // Prevent context menu on right-click
79
+ this._SVGElement.addEventListener('contextmenu', (pEvent) =>
80
+ {
81
+ pEvent.preventDefault();
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Remove all event listeners
87
+ */
88
+ destroy()
89
+ {
90
+ if (this._SVGElement)
91
+ {
92
+ this._SVGElement.removeEventListener('pointerdown', this._boundOnPointerDown);
93
+ this._SVGElement.removeEventListener('pointermove', this._boundOnPointerMove);
94
+ this._SVGElement.removeEventListener('pointerup', this._boundOnPointerUp);
95
+ this._SVGElement.removeEventListener('pointerleave', this._boundOnPointerUp);
96
+ this._SVGElement.removeEventListener('wheel', this._boundOnWheel);
97
+ }
98
+
99
+ document.removeEventListener('keydown', this._boundOnKeyDown);
100
+ }
101
+
102
+ /**
103
+ * Handle pointer down event
104
+ * @param {PointerEvent} pEvent
105
+ */
106
+ _onPointerDown(pEvent)
107
+ {
108
+ if (!this._FlowView) return;
109
+
110
+ let tmpTarget = pEvent.target;
111
+ let tmpElementType = this._getElementType(tmpTarget);
112
+
113
+ // Capture pointer for smooth dragging
114
+ this._SVGElement.setPointerCapture(pEvent.pointerId);
115
+
116
+ switch (tmpElementType)
117
+ {
118
+ case 'port':
119
+ this._startConnection(pEvent, tmpTarget);
120
+ break;
121
+
122
+ case 'node':
123
+ case 'node-body':
124
+ this._startNodeDrag(pEvent, tmpTarget);
125
+ break;
126
+
127
+ case 'connection':
128
+ case 'connection-hitarea':
129
+ this._selectConnection(tmpTarget);
130
+ break;
131
+
132
+ default:
133
+ // Click on background - start panning or deselect
134
+ if (pEvent.button === 0 && this._FlowView.options.EnablePanning)
135
+ {
136
+ this._startPanning(pEvent);
137
+ }
138
+ break;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Handle pointer move event
144
+ * @param {PointerEvent} pEvent
145
+ */
146
+ _onPointerMove(pEvent)
147
+ {
148
+ if (!this._FlowView) return;
149
+
150
+ switch (this._State)
151
+ {
152
+ case INTERACTION_STATES.DRAGGING_NODE:
153
+ this._onNodeDrag(pEvent);
154
+ break;
155
+
156
+ case INTERACTION_STATES.CONNECTING:
157
+ this._onConnectionDrag(pEvent);
158
+ break;
159
+
160
+ case INTERACTION_STATES.PANNING:
161
+ this._onPan(pEvent);
162
+ break;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Handle pointer up event
168
+ * @param {PointerEvent} pEvent
169
+ */
170
+ _onPointerUp(pEvent)
171
+ {
172
+ if (!this._FlowView) return;
173
+
174
+ // Release pointer capture
175
+ if (this._SVGElement.hasPointerCapture && this._SVGElement.hasPointerCapture(pEvent.pointerId))
176
+ {
177
+ this._SVGElement.releasePointerCapture(pEvent.pointerId);
178
+ }
179
+
180
+ switch (this._State)
181
+ {
182
+ case INTERACTION_STATES.DRAGGING_NODE:
183
+ this._endNodeDrag(pEvent);
184
+ break;
185
+
186
+ case INTERACTION_STATES.CONNECTING:
187
+ this._endConnection(pEvent);
188
+ break;
189
+
190
+ case INTERACTION_STATES.PANNING:
191
+ this._endPanning(pEvent);
192
+ break;
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Handle mouse wheel for zoom
198
+ * @param {WheelEvent} pEvent
199
+ */
200
+ _onWheel(pEvent)
201
+ {
202
+ if (!this._FlowView || !this._FlowView.options.EnableZooming) return;
203
+
204
+ pEvent.preventDefault();
205
+
206
+ let tmpDelta = pEvent.deltaY > 0 ? -this._FlowView.options.ZoomStep : this._FlowView.options.ZoomStep;
207
+ let tmpNewZoom = this._FlowView.viewState.Zoom + tmpDelta;
208
+
209
+ // Zoom toward mouse position
210
+ let tmpRect = this._SVGElement.getBoundingClientRect();
211
+ let tmpMouseX = pEvent.clientX - tmpRect.left;
212
+ let tmpMouseY = pEvent.clientY - tmpRect.top;
213
+
214
+ this._FlowView.setZoom(tmpNewZoom, tmpMouseX, tmpMouseY);
215
+ }
216
+
217
+ /**
218
+ * Handle keyboard events
219
+ * @param {KeyboardEvent} pEvent
220
+ */
221
+ _onKeyDown(pEvent)
222
+ {
223
+ if (!this._FlowView) return;
224
+
225
+ // Only handle events when the flow is focused/visible
226
+ if (pEvent.key === 'Delete' || pEvent.key === 'Backspace')
227
+ {
228
+ // Don't delete if user is typing in an input
229
+ if (pEvent.target && (pEvent.target.tagName === 'INPUT' || pEvent.target.tagName === 'TEXTAREA' || pEvent.target.tagName === 'SELECT'))
230
+ {
231
+ return;
232
+ }
233
+
234
+ this._FlowView.deleteSelected();
235
+ pEvent.preventDefault();
236
+ }
237
+ else if (pEvent.key === 'Escape')
238
+ {
239
+ if (this._State === INTERACTION_STATES.CONNECTING)
240
+ {
241
+ this._cancelConnection();
242
+ }
243
+ this._FlowView.deselectAll();
244
+ }
245
+ }
246
+
247
+ // ---- Node Dragging ----
248
+
249
+ /**
250
+ * Start dragging a node
251
+ * @param {PointerEvent} pEvent
252
+ * @param {Element} pTarget
253
+ */
254
+ _startNodeDrag(pEvent, pTarget)
255
+ {
256
+ if (!this._FlowView.options.EnableNodeDragging) return;
257
+
258
+ let tmpNodeHash = this._getNodeHash(pTarget);
259
+ if (!tmpNodeHash) return;
260
+
261
+ // Select the node
262
+ this._FlowView.selectNode(tmpNodeHash);
263
+
264
+ let tmpNode = this._FlowView.getNode(tmpNodeHash);
265
+ if (!tmpNode) return;
266
+
267
+ this._State = INTERACTION_STATES.DRAGGING_NODE;
268
+ this._DragNodeHash = tmpNodeHash;
269
+ this._DragStartX = pEvent.clientX;
270
+ this._DragStartY = pEvent.clientY;
271
+ this._DragNodeStartX = tmpNode.X;
272
+ this._DragNodeStartY = tmpNode.Y;
273
+
274
+ // Add dragging class
275
+ this._SVGElement.classList.add('panning');
276
+
277
+ let tmpNodeGroup = this._FlowView._NodesLayer.querySelector(`[data-node-hash="${tmpNodeHash}"]`);
278
+ if (tmpNodeGroup)
279
+ {
280
+ tmpNodeGroup.classList.add('dragging');
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Handle node dragging
286
+ * @param {PointerEvent} pEvent
287
+ */
288
+ _onNodeDrag(pEvent)
289
+ {
290
+ if (!this._DragNodeHash) return;
291
+
292
+ let tmpVS = this._FlowView.viewState;
293
+ let tmpDX = (pEvent.clientX - this._DragStartX) / tmpVS.Zoom;
294
+ let tmpDY = (pEvent.clientY - this._DragStartY) / tmpVS.Zoom;
295
+
296
+ let tmpNewX = this._DragNodeStartX + tmpDX;
297
+ let tmpNewY = this._DragNodeStartY + tmpDY;
298
+
299
+ this._FlowView.updateNodePosition(this._DragNodeHash, tmpNewX, tmpNewY);
300
+ }
301
+
302
+ /**
303
+ * End node dragging
304
+ * @param {PointerEvent} pEvent
305
+ */
306
+ _endNodeDrag(pEvent)
307
+ {
308
+ this._SVGElement.classList.remove('panning');
309
+
310
+ let tmpNodeGroup = this._FlowView._NodesLayer.querySelector(`[data-node-hash="${this._DragNodeHash}"]`);
311
+ if (tmpNodeGroup)
312
+ {
313
+ tmpNodeGroup.classList.remove('dragging');
314
+ }
315
+
316
+ // Full re-render to finalize positions
317
+ this._FlowView.renderFlow();
318
+ this._FlowView.marshalFromView();
319
+
320
+ let tmpNode = this._FlowView.getNode(this._DragNodeHash);
321
+ if (tmpNode && this._FlowView._EventHandlerProvider)
322
+ {
323
+ this._FlowView._EventHandlerProvider.fireEvent('onNodeMoved', tmpNode);
324
+ this._FlowView._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowView.flowData);
325
+ }
326
+
327
+ this._State = INTERACTION_STATES.IDLE;
328
+ this._DragNodeHash = null;
329
+ }
330
+
331
+ // ---- Connection Creation ----
332
+
333
+ /**
334
+ * Start creating a connection from a port
335
+ * @param {PointerEvent} pEvent
336
+ * @param {Element} pTarget
337
+ */
338
+ _startConnection(pEvent, pTarget)
339
+ {
340
+ if (!this._FlowView.options.EnableConnectionCreation) return;
341
+
342
+ let tmpNodeHash = pTarget.getAttribute('data-node-hash');
343
+ let tmpPortHash = pTarget.getAttribute('data-port-hash');
344
+ let tmpPortDirection = pTarget.getAttribute('data-port-direction');
345
+
346
+ if (!tmpNodeHash || !tmpPortHash) return;
347
+
348
+ // Only allow starting connections from output ports
349
+ if (tmpPortDirection !== 'output')
350
+ {
351
+ return;
352
+ }
353
+
354
+ this._State = INTERACTION_STATES.CONNECTING;
355
+ this._ConnectSourceNodeHash = tmpNodeHash;
356
+ this._ConnectSourcePortHash = tmpPortHash;
357
+
358
+ this._SVGElement.classList.add('connecting');
359
+
360
+ // Create drag line
361
+ let tmpPortPos = this._FlowView.getPortPosition(tmpNodeHash, tmpPortHash);
362
+ if (tmpPortPos)
363
+ {
364
+ this._ConnectDragLine = document.createElementNS('http://www.w3.org/2000/svg', 'path');
365
+ this._ConnectDragLine.setAttribute('class', 'pict-flow-drag-connection');
366
+ this._ConnectDragLine.setAttribute('d', `M ${tmpPortPos.x} ${tmpPortPos.y} L ${tmpPortPos.x} ${tmpPortPos.y}`);
367
+
368
+ // Add to viewport (so it transforms with pan/zoom)
369
+ this._FlowView._ViewportElement.appendChild(this._ConnectDragLine);
370
+ }
371
+
372
+ pEvent.stopPropagation();
373
+ }
374
+
375
+ /**
376
+ * Handle connection drag
377
+ * @param {PointerEvent} pEvent
378
+ */
379
+ _onConnectionDrag(pEvent)
380
+ {
381
+ if (!this._ConnectDragLine) return;
382
+
383
+ let tmpSourcePos = this._FlowView.getPortPosition(this._ConnectSourceNodeHash, this._ConnectSourcePortHash);
384
+ if (!tmpSourcePos) return;
385
+
386
+ let tmpEndCoords = this._FlowView.screenToSVGCoords(pEvent.clientX, pEvent.clientY);
387
+
388
+ // Render a bezier curve for the drag line
389
+ let tmpDX = Math.abs(tmpEndCoords.x - tmpSourcePos.x) * 0.5;
390
+ let tmpPath = `M ${tmpSourcePos.x} ${tmpSourcePos.y} C ${tmpSourcePos.x + tmpDX} ${tmpSourcePos.y}, ${tmpEndCoords.x - tmpDX} ${tmpEndCoords.y}, ${tmpEndCoords.x} ${tmpEndCoords.y}`;
391
+ this._ConnectDragLine.setAttribute('d', tmpPath);
392
+ }
393
+
394
+ /**
395
+ * End connection creation
396
+ * @param {PointerEvent} pEvent
397
+ */
398
+ _endConnection(pEvent)
399
+ {
400
+ // Remove drag line
401
+ if (this._ConnectDragLine && this._ConnectDragLine.parentNode)
402
+ {
403
+ this._ConnectDragLine.parentNode.removeChild(this._ConnectDragLine);
404
+ }
405
+ this._ConnectDragLine = null;
406
+
407
+ this._SVGElement.classList.remove('connecting');
408
+
409
+ // Check if we're over a valid target port
410
+ let tmpTarget = document.elementFromPoint(pEvent.clientX, pEvent.clientY);
411
+ if (tmpTarget)
412
+ {
413
+ let tmpTargetPortHash = tmpTarget.getAttribute('data-port-hash');
414
+ let tmpTargetNodeHash = tmpTarget.getAttribute('data-node-hash');
415
+ let tmpTargetPortDirection = tmpTarget.getAttribute('data-port-direction');
416
+
417
+ if (tmpTargetPortHash && tmpTargetNodeHash && tmpTargetPortDirection === 'input')
418
+ {
419
+ this._FlowView.addConnection(
420
+ this._ConnectSourceNodeHash,
421
+ this._ConnectSourcePortHash,
422
+ tmpTargetNodeHash,
423
+ tmpTargetPortHash
424
+ );
425
+ }
426
+ }
427
+
428
+ this._State = INTERACTION_STATES.IDLE;
429
+ this._ConnectSourceNodeHash = null;
430
+ this._ConnectSourcePortHash = null;
431
+ }
432
+
433
+ /**
434
+ * Cancel connection creation (e.g., on Escape)
435
+ */
436
+ _cancelConnection()
437
+ {
438
+ if (this._ConnectDragLine && this._ConnectDragLine.parentNode)
439
+ {
440
+ this._ConnectDragLine.parentNode.removeChild(this._ConnectDragLine);
441
+ }
442
+ this._ConnectDragLine = null;
443
+
444
+ this._SVGElement.classList.remove('connecting');
445
+
446
+ this._State = INTERACTION_STATES.IDLE;
447
+ this._ConnectSourceNodeHash = null;
448
+ this._ConnectSourcePortHash = null;
449
+ }
450
+
451
+ // ---- Panning ----
452
+
453
+ /**
454
+ * Start panning the viewport
455
+ * @param {PointerEvent} pEvent
456
+ */
457
+ _startPanning(pEvent)
458
+ {
459
+ // Deselect if clicking on empty space
460
+ this._FlowView.deselectAll();
461
+
462
+ this._State = INTERACTION_STATES.PANNING;
463
+ this._PanStartX = pEvent.clientX;
464
+ this._PanStartY = pEvent.clientY;
465
+ this._PanStartPanX = this._FlowView.viewState.PanX;
466
+ this._PanStartPanY = this._FlowView.viewState.PanY;
467
+
468
+ this._SVGElement.classList.add('panning');
469
+ }
470
+
471
+ /**
472
+ * Handle panning
473
+ * @param {PointerEvent} pEvent
474
+ */
475
+ _onPan(pEvent)
476
+ {
477
+ let tmpDX = pEvent.clientX - this._PanStartX;
478
+ let tmpDY = pEvent.clientY - this._PanStartY;
479
+
480
+ this._FlowView.viewState.PanX = this._PanStartPanX + tmpDX;
481
+ this._FlowView.viewState.PanY = this._PanStartPanY + tmpDY;
482
+
483
+ this._FlowView.updateViewportTransform();
484
+ }
485
+
486
+ /**
487
+ * End panning
488
+ * @param {PointerEvent} pEvent
489
+ */
490
+ _endPanning(pEvent)
491
+ {
492
+ this._SVGElement.classList.remove('panning');
493
+ this._State = INTERACTION_STATES.IDLE;
494
+ }
495
+
496
+ // ---- Connection Selection ----
497
+
498
+ /**
499
+ * Select a connection
500
+ * @param {Element} pTarget
501
+ */
502
+ _selectConnection(pTarget)
503
+ {
504
+ let tmpConnectionHash = pTarget.getAttribute('data-connection-hash');
505
+ if (tmpConnectionHash)
506
+ {
507
+ this._FlowView.selectConnection(tmpConnectionHash);
508
+ }
509
+ }
510
+
511
+ // ---- Utilities ----
512
+
513
+ /**
514
+ * Get the element type from a target element (walks up to find data attributes)
515
+ * @param {Element} pTarget
516
+ * @returns {string} The element type
517
+ */
518
+ _getElementType(pTarget)
519
+ {
520
+ if (!pTarget) return 'background';
521
+
522
+ // Check the element itself
523
+ let tmpType = pTarget.getAttribute('data-element-type');
524
+ if (tmpType) return tmpType;
525
+
526
+ // Walk up to find the closest element with a data attribute
527
+ let tmpParent = pTarget.parentElement;
528
+ let tmpDepth = 0;
529
+ while (tmpParent && tmpDepth < 5)
530
+ {
531
+ tmpType = tmpParent.getAttribute('data-element-type');
532
+ if (tmpType) return tmpType;
533
+ tmpParent = tmpParent.parentElement;
534
+ tmpDepth++;
535
+ }
536
+
537
+ return 'background';
538
+ }
539
+
540
+ /**
541
+ * Get the node hash from a target element (walks up parents)
542
+ * @param {Element} pTarget
543
+ * @returns {string|null}
544
+ */
545
+ _getNodeHash(pTarget)
546
+ {
547
+ if (!pTarget) return null;
548
+
549
+ let tmpHash = pTarget.getAttribute('data-node-hash');
550
+ if (tmpHash) return tmpHash;
551
+
552
+ let tmpParent = pTarget.parentElement;
553
+ let tmpDepth = 0;
554
+ while (tmpParent && tmpDepth < 5)
555
+ {
556
+ tmpHash = tmpParent.getAttribute('data-node-hash');
557
+ if (tmpHash) return tmpHash;
558
+ tmpParent = tmpParent.parentElement;
559
+ tmpDepth++;
560
+ }
561
+
562
+ return null;
563
+ }
564
+ }
565
+
566
+ module.exports = PictServiceFlowInteractionManager;
567
+ module.exports.INTERACTION_STATES = INTERACTION_STATES;