pict-section-flow 1.2.0 → 1.4.0
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/package.json +1 -1
- package/source/providers/PictProvider-Flow-CSS.js +49 -0
- package/source/providers/PictProvider-Flow-ConnectorShapes.js +8 -0
- package/source/providers/PictProvider-Flow-Icons.js +8 -0
- package/source/services/PictService-Flow-ConnectionRenderer.js +76 -4
- package/source/services/PictService-Flow-DataManager.js +1 -1
- package/source/services/PictService-Flow-InteractionManager.js +358 -24
- package/source/services/PictService-Flow-RenderManager.js +3 -1
- package/source/services/PictService-Flow-SelectionManager.js +86 -5
- package/source/views/PictView-Flow-FloatingToolbar.js +53 -0
- package/source/views/PictView-Flow-Node.js +56 -2
- package/source/views/PictView-Flow-PropertiesPanel.js +27 -5
- package/source/views/PictView-Flow-Toolbar.js +99 -11
- package/source/views/PictView-Flow.js +85 -9
- package/test/CardPalette_tests.js +43 -0
- package/test/ConnectionStyle_tests.js +90 -0
- package/test/InteractionManager_tests.js +279 -0
- package/test/NodeView_tests.js +17 -0
- package/test/SelectionManager_tests.js +185 -0
- package/test/ToolbarExtraButtons_tests.js +138 -0
- package/test/UndirectedConnections_tests.js +70 -0
|
@@ -11,7 +11,9 @@ const INTERACTION_STATES =
|
|
|
11
11
|
DRAGGING_HANDLE: 'dragging-handle',
|
|
12
12
|
CONNECTING: 'connecting',
|
|
13
13
|
PANNING: 'panning',
|
|
14
|
-
RESIZING_PANEL: 'resizing-panel'
|
|
14
|
+
RESIZING_PANEL: 'resizing-panel',
|
|
15
|
+
RESIZING_NODE: 'resizing-node',
|
|
16
|
+
MARQUEE: 'marquee'
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
class PictServiceFlowInteractionManager extends libFableServiceProviderBase
|
|
@@ -71,6 +73,13 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
|
|
|
71
73
|
this._ResizeStartY = 0;
|
|
72
74
|
this._ResizePanelStartHeight = 0;
|
|
73
75
|
|
|
76
|
+
// Node resize state (drag a node's bottom-right handle to set Width/Height)
|
|
77
|
+
this._ResizeNodeHash = null;
|
|
78
|
+
this._ResizeNodeStartX = 0;
|
|
79
|
+
this._ResizeNodeStartY = 0;
|
|
80
|
+
this._ResizeNodeStartWidth = 0;
|
|
81
|
+
this._ResizeNodeStartHeight = 0;
|
|
82
|
+
|
|
74
83
|
// Double-click detection for connections
|
|
75
84
|
this._LastConnectionClickTime = 0;
|
|
76
85
|
this._LastConnectionClickHash = null;
|
|
@@ -196,6 +205,15 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
|
|
|
196
205
|
let tmpNodeHash = this._getNodeHash(tmpTarget);
|
|
197
206
|
let tmpNow = Date.now();
|
|
198
207
|
|
|
208
|
+
// Shift-click toggles this node in the multi-selection (no drag, no panel).
|
|
209
|
+
if (tmpNodeHash && this._FlowView.options.EnableMultiSelect && pEvent.shiftKey)
|
|
210
|
+
{
|
|
211
|
+
this._LastClickTime = 0;
|
|
212
|
+
this._LastClickNodeHash = null;
|
|
213
|
+
this._FlowView.toggleNodeSelection(tmpNodeHash);
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
|
|
199
217
|
// Check for double-click on same node
|
|
200
218
|
if (tmpNodeHash && tmpNodeHash === this._LastClickNodeHash
|
|
201
219
|
&& (tmpNow - this._LastClickTime) < this._DoubleClickThreshold)
|
|
@@ -223,6 +241,10 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
|
|
|
223
241
|
this._startPanelResize(pEvent, tmpTarget);
|
|
224
242
|
break;
|
|
225
243
|
|
|
244
|
+
case 'node-resize':
|
|
245
|
+
this._startNodeResize(pEvent, tmpTarget);
|
|
246
|
+
break;
|
|
247
|
+
|
|
226
248
|
case 'panel-close':
|
|
227
249
|
{
|
|
228
250
|
let tmpPanelHash = this._getPanelHash(tmpTarget);
|
|
@@ -343,10 +365,19 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
|
|
|
343
365
|
}
|
|
344
366
|
|
|
345
367
|
default:
|
|
346
|
-
// Click
|
|
347
|
-
if (pEvent.button === 0
|
|
368
|
+
// Click / drag on the empty background.
|
|
369
|
+
if (pEvent.button === 0)
|
|
348
370
|
{
|
|
349
|
-
this.
|
|
371
|
+
if (this._FlowView.options.EnableMultiSelect && !pEvent.shiftKey)
|
|
372
|
+
{
|
|
373
|
+
// Plain drag marquee-selects; a click (no drag) clears the selection on release.
|
|
374
|
+
this._startMarquee(pEvent);
|
|
375
|
+
}
|
|
376
|
+
else if (this._FlowView.options.EnablePanning)
|
|
377
|
+
{
|
|
378
|
+
// With multi-select on, shift+drag pans; otherwise this is the normal pan.
|
|
379
|
+
this._startPanning(pEvent);
|
|
380
|
+
}
|
|
350
381
|
}
|
|
351
382
|
break;
|
|
352
383
|
}
|
|
@@ -382,9 +413,17 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
|
|
|
382
413
|
this._onPanelResize(pEvent);
|
|
383
414
|
break;
|
|
384
415
|
|
|
416
|
+
case INTERACTION_STATES.RESIZING_NODE:
|
|
417
|
+
this._onNodeResize(pEvent);
|
|
418
|
+
break;
|
|
419
|
+
|
|
385
420
|
case INTERACTION_STATES.PANNING:
|
|
386
421
|
this._onPan(pEvent);
|
|
387
422
|
break;
|
|
423
|
+
|
|
424
|
+
case INTERACTION_STATES.MARQUEE:
|
|
425
|
+
this._onMarquee(pEvent);
|
|
426
|
+
break;
|
|
388
427
|
}
|
|
389
428
|
}
|
|
390
429
|
|
|
@@ -420,6 +459,10 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
|
|
|
420
459
|
this._endPanelResize(pEvent);
|
|
421
460
|
break;
|
|
422
461
|
|
|
462
|
+
case INTERACTION_STATES.RESIZING_NODE:
|
|
463
|
+
this._endNodeResize(pEvent);
|
|
464
|
+
break;
|
|
465
|
+
|
|
423
466
|
case INTERACTION_STATES.CONNECTING:
|
|
424
467
|
this._endConnection(pEvent);
|
|
425
468
|
break;
|
|
@@ -427,6 +470,10 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
|
|
|
427
470
|
case INTERACTION_STATES.PANNING:
|
|
428
471
|
this._endPanning(pEvent);
|
|
429
472
|
break;
|
|
473
|
+
|
|
474
|
+
case INTERACTION_STATES.MARQUEE:
|
|
475
|
+
this._endMarquee(pEvent);
|
|
476
|
+
break;
|
|
430
477
|
}
|
|
431
478
|
}
|
|
432
479
|
|
|
@@ -513,63 +560,208 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
|
|
|
513
560
|
let tmpNodeHash = this._getNodeHash(pTarget);
|
|
514
561
|
if (!tmpNodeHash) return;
|
|
515
562
|
|
|
516
|
-
|
|
563
|
+
// If the grabbed node is part of a multi-selection, drag the whole set; otherwise select just
|
|
564
|
+
// it. (Grabbing a node outside the current selection collapses to that single node.)
|
|
565
|
+
let tmpSelected = this._FlowView.getSelectedNodeHashes ? this._FlowView.getSelectedNodeHashes() : [];
|
|
566
|
+
let tmpIsMulti = this._FlowView.options.EnableMultiSelect && tmpSelected.length > 1 && tmpSelected.indexOf(tmpNodeHash) >= 0;
|
|
567
|
+
if (!tmpIsMulti)
|
|
568
|
+
{
|
|
569
|
+
this._FlowView.selectNode(tmpNodeHash);
|
|
570
|
+
tmpSelected = [tmpNodeHash];
|
|
571
|
+
}
|
|
517
572
|
|
|
518
|
-
|
|
519
|
-
|
|
573
|
+
// Capture each dragged node's start position so one delta applies to all of them.
|
|
574
|
+
this._DragNodes = [];
|
|
575
|
+
for (let i = 0; i < tmpSelected.length; i++)
|
|
576
|
+
{
|
|
577
|
+
let tmpN = this._FlowView.getNode(tmpSelected[i]);
|
|
578
|
+
if (tmpN) { this._DragNodes.push({ Hash: tmpN.Hash, StartX: tmpN.X, StartY: tmpN.Y }); }
|
|
579
|
+
}
|
|
580
|
+
if (this._DragNodes.length === 0) return;
|
|
520
581
|
|
|
521
582
|
this._State = INTERACTION_STATES.DRAGGING_NODE;
|
|
522
583
|
this._DragNodeHash = tmpNodeHash;
|
|
523
584
|
this._DragStartX = pEvent.clientX;
|
|
524
585
|
this._DragStartY = pEvent.clientY;
|
|
525
|
-
this._DragNodeStartX = tmpNode.X;
|
|
526
|
-
this._DragNodeStartY = tmpNode.Y;
|
|
527
586
|
|
|
528
587
|
this._SVGElement.classList.add('panning');
|
|
529
588
|
|
|
530
|
-
let
|
|
531
|
-
if (tmpNodeGroup)
|
|
589
|
+
for (let i = 0; i < this._DragNodes.length; i++)
|
|
532
590
|
{
|
|
533
|
-
tmpNodeGroup.
|
|
591
|
+
let tmpNodeGroup = this._FlowView._NodesLayer.querySelector(`[data-node-hash="${this._DragNodes[i].Hash}"]`);
|
|
592
|
+
if (tmpNodeGroup) { tmpNodeGroup.classList.add('dragging'); }
|
|
534
593
|
}
|
|
535
594
|
}
|
|
536
595
|
|
|
537
596
|
_onNodeDrag(pEvent)
|
|
538
597
|
{
|
|
539
|
-
if (!this.
|
|
598
|
+
if (!this._DragNodes || this._DragNodes.length === 0) return;
|
|
540
599
|
|
|
541
600
|
let tmpVS = this._FlowView.viewState;
|
|
542
601
|
let tmpDX = (pEvent.clientX - this._DragStartX) / tmpVS.Zoom;
|
|
543
602
|
let tmpDY = (pEvent.clientY - this._DragStartY) / tmpVS.Zoom;
|
|
544
603
|
|
|
545
|
-
|
|
546
|
-
|
|
604
|
+
// A single-node drag can show alignment guides and snap to other nodes' edges/centers.
|
|
605
|
+
// Multi-drag keeps the rigid group offset (aligning a moving group is ambiguous).
|
|
606
|
+
if (this._DragNodes.length === 1 && this._FlowView.options.EnableAlignmentGuides)
|
|
607
|
+
{
|
|
608
|
+
let tmpHash = this._DragNodes[0].Hash;
|
|
609
|
+
let tmpNode = this._FlowView.getNode(tmpHash);
|
|
610
|
+
let tmpX = this._snapToGrid(this._DragNodes[0].StartX + tmpDX);
|
|
611
|
+
let tmpY = this._snapToGrid(this._DragNodes[0].StartY + tmpDY);
|
|
612
|
+
if (tmpNode)
|
|
613
|
+
{
|
|
614
|
+
let tmpAlign = this._alignmentFor(tmpNode, tmpX, tmpY);
|
|
615
|
+
this._drawGuides(tmpAlign.Guides);
|
|
616
|
+
this._FlowView.updateNodePosition(tmpHash, tmpAlign.X, tmpAlign.Y);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
547
620
|
|
|
548
|
-
this.
|
|
621
|
+
for (let i = 0; i < this._DragNodes.length; i++)
|
|
622
|
+
{
|
|
623
|
+
let tmpNewX = this._snapToGrid(this._DragNodes[i].StartX + tmpDX);
|
|
624
|
+
let tmpNewY = this._snapToGrid(this._DragNodes[i].StartY + tmpDY);
|
|
625
|
+
this._FlowView.updateNodePosition(this._DragNodes[i].Hash, tmpNewX, tmpNewY);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ---- Alignment guides (single-node drag) ----
|
|
630
|
+
// Compare the dragged node's six reference lines (left / center-x / right and top / center-y /
|
|
631
|
+
// bottom) against every other node's. Within a small threshold the dragged node snaps so the lines
|
|
632
|
+
// coincide, and a guide line is reported spanning both nodes. Returns the (possibly snapped)
|
|
633
|
+
// position plus the guides to draw; a pure computation so it can be unit-tested without a DOM.
|
|
634
|
+
_alignmentFor(pDragNode, pX, pY)
|
|
635
|
+
{
|
|
636
|
+
let tmpZoom = (this._FlowView.viewState && this._FlowView.viewState.Zoom) ? this._FlowView.viewState.Zoom : 1;
|
|
637
|
+
let tmpThreshold = 6 / tmpZoom;
|
|
638
|
+
let tmpDefaultW = this._FlowView.options.DefaultNodeWidth || 180;
|
|
639
|
+
let tmpDefaultH = this._FlowView.options.DefaultNodeHeight || 80;
|
|
640
|
+
let tmpW = (typeof pDragNode.Width === 'number') ? pDragNode.Width : tmpDefaultW;
|
|
641
|
+
let tmpH = (typeof pDragNode.Height === 'number') ? pDragNode.Height : tmpDefaultH;
|
|
642
|
+
|
|
643
|
+
let tmpDragV = [ pX, pX + tmpW / 2, pX + tmpW ]; // left, center-x, right
|
|
644
|
+
let tmpDragH = [ pY, pY + tmpH / 2, pY + tmpH ]; // top, center-y, bottom
|
|
645
|
+
|
|
646
|
+
let tmpBestV = null;
|
|
647
|
+
let tmpBestH = null;
|
|
648
|
+
let tmpNodes = this._FlowView._FlowData.Nodes || [];
|
|
649
|
+
|
|
650
|
+
for (let i = 0; i < tmpNodes.length; i++)
|
|
651
|
+
{
|
|
652
|
+
let tmpO = tmpNodes[i];
|
|
653
|
+
if (tmpO.Hash === pDragNode.Hash) continue;
|
|
654
|
+
let tmpOW = (typeof tmpO.Width === 'number') ? tmpO.Width : tmpDefaultW;
|
|
655
|
+
let tmpOH = (typeof tmpO.Height === 'number') ? tmpO.Height : tmpDefaultH;
|
|
656
|
+
let tmpOtherV = [ tmpO.X, tmpO.X + tmpOW / 2, tmpO.X + tmpOW ];
|
|
657
|
+
let tmpOtherH = [ tmpO.Y, tmpO.Y + tmpOH / 2, tmpO.Y + tmpOH ];
|
|
658
|
+
|
|
659
|
+
for (let a = 0; a < tmpDragV.length; a++)
|
|
660
|
+
{
|
|
661
|
+
for (let b = 0; b < tmpOtherV.length; b++)
|
|
662
|
+
{
|
|
663
|
+
let tmpD = Math.abs(tmpDragV[a] - tmpOtherV[b]);
|
|
664
|
+
if (tmpD <= tmpThreshold && (!tmpBestV || tmpD < tmpBestV.Delta))
|
|
665
|
+
{
|
|
666
|
+
tmpBestV = { Delta: tmpD, Pos: tmpOtherV[b], SnapX: pX + (tmpOtherV[b] - tmpDragV[a]), OtherTop: tmpO.Y, OtherBottom: tmpO.Y + tmpOH };
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
for (let a = 0; a < tmpDragH.length; a++)
|
|
671
|
+
{
|
|
672
|
+
for (let b = 0; b < tmpOtherH.length; b++)
|
|
673
|
+
{
|
|
674
|
+
let tmpD = Math.abs(tmpDragH[a] - tmpOtherH[b]);
|
|
675
|
+
if (tmpD <= tmpThreshold && (!tmpBestH || tmpD < tmpBestH.Delta))
|
|
676
|
+
{
|
|
677
|
+
tmpBestH = { Delta: tmpD, Pos: tmpOtherH[b], SnapY: pY + (tmpOtherH[b] - tmpDragH[a]), OtherLeft: tmpO.X, OtherRight: tmpO.X + tmpOW };
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
let tmpResult = { X: pX, Y: pY, Guides: [] };
|
|
684
|
+
if (tmpBestV)
|
|
685
|
+
{
|
|
686
|
+
tmpResult.X = tmpBestV.SnapX;
|
|
687
|
+
tmpResult.Guides.push({ Type: 'v', Pos: tmpBestV.Pos, A: Math.min(tmpResult.Y, tmpBestV.OtherTop), B: Math.max(tmpResult.Y + tmpH, tmpBestV.OtherBottom) });
|
|
688
|
+
}
|
|
689
|
+
if (tmpBestH)
|
|
690
|
+
{
|
|
691
|
+
tmpResult.Y = tmpBestH.SnapY;
|
|
692
|
+
tmpResult.Guides.push({ Type: 'h', Pos: tmpBestH.Pos, A: Math.min(tmpResult.X, tmpBestH.OtherLeft), B: Math.max(tmpResult.X + tmpW, tmpBestH.OtherRight) });
|
|
693
|
+
}
|
|
694
|
+
return tmpResult;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
_drawGuides(pGuides)
|
|
698
|
+
{
|
|
699
|
+
this._clearGuides();
|
|
700
|
+
if (!pGuides || pGuides.length === 0 || !this._FlowView._ViewportElement) return;
|
|
701
|
+
this._GuideElements = [];
|
|
702
|
+
for (let i = 0; i < pGuides.length; i++)
|
|
703
|
+
{
|
|
704
|
+
let tmpG = pGuides[i];
|
|
705
|
+
let tmpLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
706
|
+
tmpLine.setAttribute('class', 'pict-flow-align-guide');
|
|
707
|
+
if (tmpG.Type === 'v') { tmpLine.setAttribute('x1', tmpG.Pos); tmpLine.setAttribute('x2', tmpG.Pos); tmpLine.setAttribute('y1', tmpG.A); tmpLine.setAttribute('y2', tmpG.B); }
|
|
708
|
+
else { tmpLine.setAttribute('y1', tmpG.Pos); tmpLine.setAttribute('y2', tmpG.Pos); tmpLine.setAttribute('x1', tmpG.A); tmpLine.setAttribute('x2', tmpG.B); }
|
|
709
|
+
this._FlowView._ViewportElement.appendChild(tmpLine);
|
|
710
|
+
this._GuideElements.push(tmpLine);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
_clearGuides()
|
|
715
|
+
{
|
|
716
|
+
if (this._GuideElements)
|
|
717
|
+
{
|
|
718
|
+
for (let i = 0; i < this._GuideElements.length; i++)
|
|
719
|
+
{
|
|
720
|
+
let tmpEl = this._GuideElements[i];
|
|
721
|
+
if (tmpEl.parentNode) { tmpEl.parentNode.removeChild(tmpEl); }
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
this._GuideElements = null;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Snap a coordinate or size to the grid when the flow has EnableGridSnap on; otherwise pass through.
|
|
728
|
+
_snapToGrid(pValue)
|
|
729
|
+
{
|
|
730
|
+
if (!this._FlowView.options.EnableGridSnap) { return pValue; }
|
|
731
|
+
// A defined GridSnapSize of 0 means "no snap"; only an absent size falls back to the default.
|
|
732
|
+
let tmpGrid = (typeof this._FlowView.options.GridSnapSize === 'number') ? this._FlowView.options.GridSnapSize : 20;
|
|
733
|
+
if (tmpGrid <= 0) { return pValue; }
|
|
734
|
+
return Math.round(pValue / tmpGrid) * tmpGrid;
|
|
549
735
|
}
|
|
550
736
|
|
|
551
737
|
_endNodeDrag(pEvent)
|
|
552
738
|
{
|
|
553
739
|
this._SVGElement.classList.remove('panning');
|
|
740
|
+
this._clearGuides();
|
|
554
741
|
|
|
555
|
-
let
|
|
556
|
-
|
|
742
|
+
let tmpDragged = this._DragNodes || [];
|
|
743
|
+
for (let i = 0; i < tmpDragged.length; i++)
|
|
557
744
|
{
|
|
558
|
-
tmpNodeGroup.
|
|
745
|
+
let tmpNodeGroup = this._FlowView._NodesLayer.querySelector(`[data-node-hash="${tmpDragged[i].Hash}"]`);
|
|
746
|
+
if (tmpNodeGroup) { tmpNodeGroup.classList.remove('dragging'); }
|
|
559
747
|
}
|
|
560
748
|
|
|
561
749
|
this._FlowView.renderFlow();
|
|
562
750
|
this._FlowView.marshalFromView();
|
|
563
751
|
|
|
564
|
-
|
|
565
|
-
if (tmpNode && this._FlowView._EventHandlerProvider)
|
|
752
|
+
if (this._FlowView._EventHandlerProvider)
|
|
566
753
|
{
|
|
567
|
-
|
|
754
|
+
for (let i = 0; i < tmpDragged.length; i++)
|
|
755
|
+
{
|
|
756
|
+
let tmpNode = this._FlowView.getNode(tmpDragged[i].Hash);
|
|
757
|
+
if (tmpNode) { this._FlowView._EventHandlerProvider.fireEvent('onNodeMoved', tmpNode); }
|
|
758
|
+
}
|
|
568
759
|
this._FlowView._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowView.flowData);
|
|
569
760
|
}
|
|
570
761
|
|
|
571
762
|
this._State = INTERACTION_STATES.IDLE;
|
|
572
763
|
this._DragNodeHash = null;
|
|
764
|
+
this._DragNodes = null;
|
|
573
765
|
}
|
|
574
766
|
|
|
575
767
|
// ---- Panel Dragging ----
|
|
@@ -684,6 +876,71 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
|
|
|
684
876
|
this._ResizePanelHash = null;
|
|
685
877
|
}
|
|
686
878
|
|
|
879
|
+
// ---- Node Resizing ----
|
|
880
|
+
// Drag the bottom-right handle (rendered on the selected node when the flow's
|
|
881
|
+
// EnableNodeResizing option is on) to set Width/Height. Deltas are divided by the
|
|
882
|
+
// zoom so the corner tracks the pointer at any zoom level.
|
|
883
|
+
|
|
884
|
+
_startNodeResize(pEvent, pTarget)
|
|
885
|
+
{
|
|
886
|
+
if (!this._FlowView.options.EnableNodeResizing) return;
|
|
887
|
+
|
|
888
|
+
let tmpNodeHash = this._getNodeHash(pTarget);
|
|
889
|
+
if (!tmpNodeHash) return;
|
|
890
|
+
|
|
891
|
+
let tmpNode = this._FlowView.getNode(tmpNodeHash);
|
|
892
|
+
if (!tmpNode) return;
|
|
893
|
+
|
|
894
|
+
this._State = INTERACTION_STATES.RESIZING_NODE;
|
|
895
|
+
this._ResizeNodeHash = tmpNodeHash;
|
|
896
|
+
this._ResizeNodeStartX = pEvent.clientX;
|
|
897
|
+
this._ResizeNodeStartY = pEvent.clientY;
|
|
898
|
+
this._ResizeNodeStartWidth = tmpNode.Width || 180;
|
|
899
|
+
this._ResizeNodeStartHeight = tmpNode.Height || 80;
|
|
900
|
+
|
|
901
|
+
this._SVGElement.classList.add('panning');
|
|
902
|
+
if (pEvent.stopPropagation) pEvent.stopPropagation();
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
_onNodeResize(pEvent)
|
|
906
|
+
{
|
|
907
|
+
if (!this._ResizeNodeHash) return;
|
|
908
|
+
|
|
909
|
+
let tmpVS = this._FlowView.viewState;
|
|
910
|
+
let tmpDX = (pEvent.clientX - this._ResizeNodeStartX) / tmpVS.Zoom;
|
|
911
|
+
let tmpDY = (pEvent.clientY - this._ResizeNodeStartY) / tmpVS.Zoom;
|
|
912
|
+
|
|
913
|
+
let tmpMinW = this._FlowView.options.MinimumNodeWidth || 48;
|
|
914
|
+
let tmpMinH = this._FlowView.options.MinimumNodeHeight || 32;
|
|
915
|
+
|
|
916
|
+
let tmpNode = this._FlowView.getNode(this._ResizeNodeHash);
|
|
917
|
+
if (!tmpNode) return;
|
|
918
|
+
|
|
919
|
+
tmpNode.Width = Math.max(tmpMinW, this._snapToGrid(Math.round(this._ResizeNodeStartWidth + tmpDX)));
|
|
920
|
+
tmpNode.Height = Math.max(tmpMinH, this._snapToGrid(Math.round(this._ResizeNodeStartHeight + tmpDY)));
|
|
921
|
+
|
|
922
|
+
// Re-render so the body (and any HTML/image content) reflows to the new size.
|
|
923
|
+
this._FlowView.renderFlow();
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
_endNodeResize(pEvent)
|
|
927
|
+
{
|
|
928
|
+
this._SVGElement.classList.remove('panning');
|
|
929
|
+
|
|
930
|
+
this._FlowView.renderFlow();
|
|
931
|
+
this._FlowView.marshalFromView();
|
|
932
|
+
|
|
933
|
+
let tmpNode = this._FlowView.getNode(this._ResizeNodeHash);
|
|
934
|
+
if (tmpNode && this._FlowView._EventHandlerProvider)
|
|
935
|
+
{
|
|
936
|
+
this._FlowView._EventHandlerProvider.fireEvent('onNodeResized', tmpNode);
|
|
937
|
+
this._FlowView._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowView.flowData);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
this._State = INTERACTION_STATES.IDLE;
|
|
941
|
+
this._ResizeNodeHash = null;
|
|
942
|
+
}
|
|
943
|
+
|
|
687
944
|
// ---- Handle Dragging ----
|
|
688
945
|
|
|
689
946
|
_startHandleDrag(pEvent, pConnectionHash, pPanelHash, pHandleType, pIsTether)
|
|
@@ -906,7 +1163,8 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
|
|
|
906
1163
|
|
|
907
1164
|
if (!tmpNodeHash || !tmpPortHash) return;
|
|
908
1165
|
|
|
909
|
-
|
|
1166
|
+
// A connection normally starts from an output port; undirected mode lets any port start one.
|
|
1167
|
+
if (!this._FlowView.options.EnableUndirectedConnections && tmpPortDirection !== 'output')
|
|
910
1168
|
{
|
|
911
1169
|
return;
|
|
912
1170
|
}
|
|
@@ -960,7 +1218,8 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
|
|
|
960
1218
|
let tmpTargetNodeHash = tmpTarget.getAttribute('data-node-hash');
|
|
961
1219
|
let tmpTargetPortDirection = tmpTarget.getAttribute('data-port-direction');
|
|
962
1220
|
|
|
963
|
-
|
|
1221
|
+
// A connection normally completes on an input port; undirected mode lets any port receive it.
|
|
1222
|
+
if (tmpTargetPortHash && tmpTargetNodeHash && (this._FlowView.options.EnableUndirectedConnections || tmpTargetPortDirection === 'input'))
|
|
964
1223
|
{
|
|
965
1224
|
this._FlowView.addConnection(
|
|
966
1225
|
this._ConnectSourceNodeHash,
|
|
@@ -1023,6 +1282,81 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
|
|
|
1023
1282
|
this._State = INTERACTION_STATES.IDLE;
|
|
1024
1283
|
}
|
|
1025
1284
|
|
|
1285
|
+
// ---- Marquee selection (multi-select) ----
|
|
1286
|
+
// A rubber-band rectangle drawn in world coordinates (inside the transformed viewport group), so it
|
|
1287
|
+
// tracks the nodes under it regardless of pan/zoom. On release every node whose box intersects the
|
|
1288
|
+
// rectangle is selected; a rectangle too small to be a deliberate drag is treated as a click that
|
|
1289
|
+
// clears the selection.
|
|
1290
|
+
|
|
1291
|
+
_startMarquee(pEvent)
|
|
1292
|
+
{
|
|
1293
|
+
let tmpStart = this._FlowView.screenToSVGCoords(pEvent.clientX, pEvent.clientY);
|
|
1294
|
+
this._MarqueeStartX = tmpStart.x;
|
|
1295
|
+
this._MarqueeStartY = tmpStart.y;
|
|
1296
|
+
this._MarqueeCurrentX = tmpStart.x;
|
|
1297
|
+
this._MarqueeCurrentY = tmpStart.y;
|
|
1298
|
+
this._State = INTERACTION_STATES.MARQUEE;
|
|
1299
|
+
|
|
1300
|
+
let tmpRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
1301
|
+
tmpRect.setAttribute('class', 'pict-flow-marquee');
|
|
1302
|
+
tmpRect.setAttribute('x', tmpStart.x);
|
|
1303
|
+
tmpRect.setAttribute('y', tmpStart.y);
|
|
1304
|
+
tmpRect.setAttribute('width', 0);
|
|
1305
|
+
tmpRect.setAttribute('height', 0);
|
|
1306
|
+
this._MarqueeElement = tmpRect;
|
|
1307
|
+
if (this._FlowView._ViewportElement) { this._FlowView._ViewportElement.appendChild(tmpRect); }
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
_onMarquee(pEvent)
|
|
1311
|
+
{
|
|
1312
|
+
if (!this._MarqueeElement) return;
|
|
1313
|
+
let tmpCur = this._FlowView.screenToSVGCoords(pEvent.clientX, pEvent.clientY);
|
|
1314
|
+
this._MarqueeCurrentX = tmpCur.x;
|
|
1315
|
+
this._MarqueeCurrentY = tmpCur.y;
|
|
1316
|
+
let tmpX = Math.min(this._MarqueeStartX, tmpCur.x);
|
|
1317
|
+
let tmpY = Math.min(this._MarqueeStartY, tmpCur.y);
|
|
1318
|
+
this._MarqueeElement.setAttribute('x', tmpX);
|
|
1319
|
+
this._MarqueeElement.setAttribute('y', tmpY);
|
|
1320
|
+
this._MarqueeElement.setAttribute('width', Math.abs(tmpCur.x - this._MarqueeStartX));
|
|
1321
|
+
this._MarqueeElement.setAttribute('height', Math.abs(tmpCur.y - this._MarqueeStartY));
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
_endMarquee(pEvent)
|
|
1325
|
+
{
|
|
1326
|
+
let tmpX = Math.min(this._MarqueeStartX, this._MarqueeCurrentX);
|
|
1327
|
+
let tmpY = Math.min(this._MarqueeStartY, this._MarqueeCurrentY);
|
|
1328
|
+
let tmpW = Math.abs(this._MarqueeCurrentX - this._MarqueeStartX);
|
|
1329
|
+
let tmpH = Math.abs(this._MarqueeCurrentY - this._MarqueeStartY);
|
|
1330
|
+
|
|
1331
|
+
if (this._MarqueeElement && this._MarqueeElement.parentNode)
|
|
1332
|
+
{
|
|
1333
|
+
this._MarqueeElement.parentNode.removeChild(this._MarqueeElement);
|
|
1334
|
+
}
|
|
1335
|
+
this._MarqueeElement = null;
|
|
1336
|
+
this._State = INTERACTION_STATES.IDLE;
|
|
1337
|
+
|
|
1338
|
+
// Too small to be a deliberate drag: treat as a background click and clear the selection.
|
|
1339
|
+
if (tmpW < 4 && tmpH < 4)
|
|
1340
|
+
{
|
|
1341
|
+
this._FlowView.deselectAll();
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
let tmpHits = [];
|
|
1346
|
+
let tmpNodes = this._FlowView._FlowData.Nodes || [];
|
|
1347
|
+
let tmpDefaultW = this._FlowView.options.DefaultNodeWidth || 180;
|
|
1348
|
+
let tmpDefaultH = this._FlowView.options.DefaultNodeHeight || 80;
|
|
1349
|
+
for (let i = 0; i < tmpNodes.length; i++)
|
|
1350
|
+
{
|
|
1351
|
+
let tmpN = tmpNodes[i];
|
|
1352
|
+
let tmpNW = (typeof tmpN.Width === 'number') ? tmpN.Width : tmpDefaultW;
|
|
1353
|
+
let tmpNH = (typeof tmpN.Height === 'number') ? tmpN.Height : tmpDefaultH;
|
|
1354
|
+
let tmpIntersects = !(tmpN.X > tmpX + tmpW || (tmpN.X + tmpNW) < tmpX || tmpN.Y > tmpY + tmpH || (tmpN.Y + tmpNH) < tmpY);
|
|
1355
|
+
if (tmpIntersects) { tmpHits.push(tmpN.Hash); }
|
|
1356
|
+
}
|
|
1357
|
+
this._FlowView.selectNodes(tmpHits);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1026
1360
|
// ---- Connection Selection ----
|
|
1027
1361
|
|
|
1028
1362
|
_selectConnection(pTarget)
|
|
@@ -63,7 +63,9 @@ class PictServiceFlowRenderManager extends libFableServiceProviderBase
|
|
|
63
63
|
for (let i = 0; i < this._FlowView._FlowData.Nodes.length; i++)
|
|
64
64
|
{
|
|
65
65
|
let tmpNode = this._FlowView._FlowData.Nodes[i];
|
|
66
|
-
let
|
|
66
|
+
let tmpSelectedSet = this._FlowView._FlowData.ViewState.SelectedNodeHashes;
|
|
67
|
+
let tmpIsSelected = (this._FlowView._FlowData.ViewState.SelectedNodeHash === tmpNode.Hash)
|
|
68
|
+
|| (Array.isArray(tmpSelectedSet) && tmpSelectedSet.indexOf(tmpNode.Hash) >= 0);
|
|
67
69
|
let tmpNodeTypeConfig = this._FlowView._NodeTypeProvider.getNodeType(tmpNode.Type);
|
|
68
70
|
|
|
69
71
|
// Enrich saved port data with metadata from the node type's DefaultPorts.
|
|
@@ -25,6 +25,9 @@ class PictServiceFlowSelectionManager extends libFableServiceProviderBase
|
|
|
25
25
|
{
|
|
26
26
|
let tmpPreviousSelection = this._FlowView._FlowData.ViewState.SelectedNodeHash;
|
|
27
27
|
this._FlowView._FlowData.ViewState.SelectedNodeHash = pNodeHash;
|
|
28
|
+
// Single selection keeps the set in lockstep, so the renderer (which highlights the set) and
|
|
29
|
+
// any multi-select consumer see one consistent picture.
|
|
30
|
+
this._FlowView._FlowData.ViewState.SelectedNodeHashes = pNodeHash ? [pNodeHash] : [];
|
|
28
31
|
this._FlowView._FlowData.ViewState.SelectedConnectionHash = null;
|
|
29
32
|
this._FlowView._FlowData.ViewState.SelectedTetherHash = null;
|
|
30
33
|
|
|
@@ -37,6 +40,72 @@ class PictServiceFlowSelectionManager extends libFableServiceProviderBase
|
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
/**
|
|
44
|
+
* The current selection set as an array of node hashes.
|
|
45
|
+
* @returns {Array<string>}
|
|
46
|
+
*/
|
|
47
|
+
getSelectedNodeHashes()
|
|
48
|
+
{
|
|
49
|
+
let tmpSet = this._FlowView._FlowData.ViewState.SelectedNodeHashes;
|
|
50
|
+
return Array.isArray(tmpSet) ? tmpSet.slice() : [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Toggle a node's membership in the selection set (shift-click). The primary SelectedNodeHash
|
|
55
|
+
* tracks the most recently affected member (or null when the set empties).
|
|
56
|
+
* @param {string} pNodeHash
|
|
57
|
+
*/
|
|
58
|
+
toggleNodeSelection(pNodeHash)
|
|
59
|
+
{
|
|
60
|
+
if (!pNodeHash) return;
|
|
61
|
+
let tmpVS = this._FlowView._FlowData.ViewState;
|
|
62
|
+
let tmpSet = Array.isArray(tmpVS.SelectedNodeHashes) ? tmpVS.SelectedNodeHashes.slice() : [];
|
|
63
|
+
let tmpIndex = tmpSet.indexOf(pNodeHash);
|
|
64
|
+
if (tmpIndex >= 0)
|
|
65
|
+
{
|
|
66
|
+
tmpSet.splice(tmpIndex, 1);
|
|
67
|
+
tmpVS.SelectedNodeHash = tmpSet.length ? tmpSet[tmpSet.length - 1] : null;
|
|
68
|
+
}
|
|
69
|
+
else
|
|
70
|
+
{
|
|
71
|
+
tmpSet.push(pNodeHash);
|
|
72
|
+
tmpVS.SelectedNodeHash = pNodeHash;
|
|
73
|
+
}
|
|
74
|
+
tmpVS.SelectedNodeHashes = tmpSet;
|
|
75
|
+
tmpVS.SelectedConnectionHash = null;
|
|
76
|
+
tmpVS.SelectedTetherHash = null;
|
|
77
|
+
|
|
78
|
+
this._FlowView.renderFlow();
|
|
79
|
+
|
|
80
|
+
if (this._FlowView._EventHandlerProvider)
|
|
81
|
+
{
|
|
82
|
+
let tmpNode = tmpVS.SelectedNodeHash ? this._FlowView._FlowData.Nodes.find((pNode) => pNode.Hash === tmpVS.SelectedNodeHash) : null;
|
|
83
|
+
this._FlowView._EventHandlerProvider.fireEvent('onNodeSelected', tmpNode);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Replace the selection set with the given node hashes (marquee result).
|
|
89
|
+
* @param {Array<string>} pNodeHashes
|
|
90
|
+
*/
|
|
91
|
+
selectNodes(pNodeHashes)
|
|
92
|
+
{
|
|
93
|
+
let tmpVS = this._FlowView._FlowData.ViewState;
|
|
94
|
+
let tmpSet = Array.isArray(pNodeHashes) ? pNodeHashes.slice() : [];
|
|
95
|
+
tmpVS.SelectedNodeHashes = tmpSet;
|
|
96
|
+
tmpVS.SelectedNodeHash = tmpSet.length ? tmpSet[tmpSet.length - 1] : null;
|
|
97
|
+
tmpVS.SelectedConnectionHash = null;
|
|
98
|
+
tmpVS.SelectedTetherHash = null;
|
|
99
|
+
|
|
100
|
+
this._FlowView.renderFlow();
|
|
101
|
+
|
|
102
|
+
if (this._FlowView._EventHandlerProvider)
|
|
103
|
+
{
|
|
104
|
+
let tmpNode = tmpVS.SelectedNodeHash ? this._FlowView._FlowData.Nodes.find((pNode) => pNode.Hash === tmpVS.SelectedNodeHash) : null;
|
|
105
|
+
this._FlowView._EventHandlerProvider.fireEvent('onNodeSelected', tmpNode);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
40
109
|
/**
|
|
41
110
|
* Select a connection
|
|
42
111
|
* @param {string|null} pConnectionHash - Hash of the connection to select, or null to deselect
|
|
@@ -83,24 +152,36 @@ class PictServiceFlowSelectionManager extends libFableServiceProviderBase
|
|
|
83
152
|
deselectAll()
|
|
84
153
|
{
|
|
85
154
|
this._FlowView._FlowData.ViewState.SelectedNodeHash = null;
|
|
155
|
+
this._FlowView._FlowData.ViewState.SelectedNodeHashes = [];
|
|
86
156
|
this._FlowView._FlowData.ViewState.SelectedConnectionHash = null;
|
|
87
157
|
this._FlowView._FlowData.ViewState.SelectedTetherHash = null;
|
|
88
158
|
this._FlowView.renderFlow();
|
|
89
159
|
}
|
|
90
160
|
|
|
91
161
|
/**
|
|
92
|
-
* Delete the
|
|
162
|
+
* Delete the current selection: every node in the multi-select set (falling back to the single
|
|
163
|
+
* SelectedNodeHash), or the selected connection.
|
|
93
164
|
* @returns {boolean}
|
|
94
165
|
*/
|
|
95
166
|
deleteSelected()
|
|
96
167
|
{
|
|
97
|
-
|
|
168
|
+
let tmpVS = this._FlowView._FlowData.ViewState;
|
|
169
|
+
let tmpSet = Array.isArray(tmpVS.SelectedNodeHashes) ? tmpVS.SelectedNodeHashes.slice() : [];
|
|
170
|
+
if (tmpSet.length === 0 && tmpVS.SelectedNodeHash) { tmpSet = [tmpVS.SelectedNodeHash]; }
|
|
171
|
+
|
|
172
|
+
if (tmpSet.length > 0)
|
|
98
173
|
{
|
|
99
|
-
|
|
174
|
+
tmpVS.SelectedNodeHash = null;
|
|
175
|
+
tmpVS.SelectedNodeHashes = [];
|
|
176
|
+
let tmpRemovedAny = false;
|
|
177
|
+
// removeNode marshals/renders/fires events per call; for a multi-delete that is acceptable
|
|
178
|
+
// (selections are small) and keeps each removal's event semantics intact.
|
|
179
|
+
tmpSet.forEach((pHash) => { if (this._FlowView.removeNode(pHash)) { tmpRemovedAny = true; } });
|
|
180
|
+
return tmpRemovedAny;
|
|
100
181
|
}
|
|
101
|
-
if (
|
|
182
|
+
if (tmpVS.SelectedConnectionHash)
|
|
102
183
|
{
|
|
103
|
-
return this._FlowView.removeConnection(
|
|
184
|
+
return this._FlowView.removeConnection(tmpVS.SelectedConnectionHash);
|
|
104
185
|
}
|
|
105
186
|
return false;
|
|
106
187
|
}
|