pict-section-flow 0.0.13 → 0.0.16

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pict-section-flow",
3
- "version": "0.0.13",
3
+ "version": "0.0.16",
4
4
  "description": "Pict Section Flow Diagram",
5
5
  "main": "source/Pict-Section-Flow.js",
6
6
  "scripts": {
@@ -934,13 +934,6 @@ class PictProviderFlowCSS extends libFableServiceProviderBase
934
934
  .pict-flow-toolbar-btn:active {
935
935
  background-color: var(--pf-button-active-bg);
936
936
  }
937
- .pict-flow-toolbar-btn.danger {
938
- color: var(--pf-button-danger-text);
939
- border-color: var(--pf-button-danger-text);
940
- }
941
- .pict-flow-toolbar-btn.danger:hover {
942
- background-color: var(--pf-button-danger-hover-bg);
943
- }
944
937
  .pict-flow-toolbar-btn-icon {
945
938
  display: inline-flex;
946
939
  align-items: center;
@@ -1397,9 +1390,6 @@ class PictProviderFlowCSS extends libFableServiceProviderBase
1397
1390
  .pict-flow-floating-btn:hover {
1398
1391
  background-color: var(--pf-button-hover-bg);
1399
1392
  }
1400
- .pict-flow-floating-btn.danger:hover {
1401
- background-color: var(--pf-button-danger-hover-bg);
1402
- }
1403
1393
  .pict-flow-floating-separator {
1404
1394
  height: 1px;
1405
1395
  background-color: var(--pf-divider-light);
@@ -104,8 +104,15 @@ class PictProviderFlowNodeTypes extends libPictProvider
104
104
 
105
105
  this._FlowView = (pOptions && pOptions.FlowView) ? pOptions.FlowView : null;
106
106
 
107
- // Initialize with default node types
108
- this._NodeTypes = JSON.parse(JSON.stringify(_DefaultNodeTypes));
107
+ // Initialize with default node types unless explicitly disabled
108
+ if (pOptions && pOptions.IncludeDefaultNodeTypes === false)
109
+ {
110
+ this._NodeTypes = {};
111
+ }
112
+ else
113
+ {
114
+ this._NodeTypes = JSON.parse(JSON.stringify(_DefaultNodeTypes));
115
+ }
109
116
 
110
117
  // Merge any additional node types passed in via options
111
118
  if (pOptions && pOptions.AdditionalNodeTypes && typeof pOptions.AdditionalNodeTypes === 'object')
@@ -223,6 +223,9 @@ class PictServiceFlowConnectionHandleManager extends libFableServiceProviderBase
223
223
  if (tmpConn.Data && tmpConn.Data.HandleCustomized)
224
224
  {
225
225
  tmpConn.Data.HandleCustomized = false;
226
+ // Clear multi-handle array (current format)
227
+ tmpConn.Data.BezierHandles = [];
228
+ // Clear legacy single-handle fields
226
229
  tmpConn.Data.BezierHandleX = null;
227
230
  tmpConn.Data.BezierHandleY = null;
228
231
  tmpConn.Data.OrthoCorner1X = null;
@@ -75,6 +75,10 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
75
75
  this._LastConnectionClickTime = 0;
76
76
  this._LastConnectionClickHash = null;
77
77
 
78
+ // Double-click detection for tethers
79
+ this._LastTetherClickTime = 0;
80
+ this._LastTetherClickHash = null;
81
+
78
82
  // Double-click detection for handles
79
83
  this._LastHandleClickTime = 0;
80
84
  this._LastHandleClickHash = null;
@@ -128,6 +132,15 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
128
132
  case 'connection-handle':
129
133
  this._removeBezierHandle(tmpTarget);
130
134
  break;
135
+
136
+ case 'tether':
137
+ case 'tether-hitarea':
138
+ this._addTetherBezierHandle(tmpTarget, pEvent);
139
+ break;
140
+
141
+ case 'tether-handle':
142
+ this._removeTetherBezierHandle(tmpTarget);
143
+ break;
131
144
  }
132
145
  });
133
146
  }
@@ -252,8 +265,26 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
252
265
 
253
266
  case 'tether':
254
267
  case 'tether-hitarea':
255
- this._selectTether(tmpTarget);
268
+ {
269
+ let tmpPanelHash = this._getPanelHash(tmpTarget);
270
+ let tmpNow = Date.now();
271
+
272
+ // Check for double-click on same tether to add a handle
273
+ if (tmpPanelHash && tmpPanelHash === this._LastTetherClickHash
274
+ && (tmpNow - this._LastTetherClickTime) < this._DoubleClickThreshold)
275
+ {
276
+ this._LastTetherClickTime = 0;
277
+ this._LastTetherClickHash = null;
278
+ this._addTetherBezierHandle(tmpTarget, pEvent);
279
+ }
280
+ else
281
+ {
282
+ this._LastTetherClickTime = tmpNow;
283
+ this._LastTetherClickHash = tmpPanelHash;
284
+ this._selectTether(tmpTarget);
285
+ }
256
286
  break;
287
+ }
257
288
 
258
289
  case 'tether-handle':
259
290
  {
@@ -758,6 +789,41 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
758
789
  this._FlowView.removeConnectionHandle(tmpConnectionHash, tmpIndex);
759
790
  }
760
791
 
792
+ /**
793
+ * Add a bezier handle to a tether.
794
+ * @param {Element} pTarget - The tether SVG element that was right-clicked or double-clicked
795
+ * @param {Event} pEvent - The mouse event (for coordinate extraction)
796
+ */
797
+ _addTetherBezierHandle(pTarget, pEvent)
798
+ {
799
+ let tmpPanelHash = this._getPanelHash(pTarget);
800
+ if (!tmpPanelHash) return;
801
+
802
+ // Select the tether so handles render after re-render
803
+ this._FlowView.selectTether(tmpPanelHash);
804
+
805
+ let tmpCoords = this._FlowView.screenToSVGCoords(pEvent.clientX, pEvent.clientY);
806
+ this._FlowView.addTetherHandle(tmpPanelHash, tmpCoords.x, tmpCoords.y);
807
+ }
808
+
809
+ /**
810
+ * Remove a bezier handle from a tether.
811
+ * @param {Element} pTarget - The tether handle SVG element that was right-clicked
812
+ */
813
+ _removeTetherBezierHandle(pTarget)
814
+ {
815
+ let tmpPanelHash = this._getPanelHash(pTarget);
816
+ if (!tmpPanelHash) return;
817
+
818
+ let tmpHandleType = pTarget.getAttribute('data-handle-type');
819
+ if (!tmpHandleType || !tmpHandleType.startsWith('bezier-handle-')) return;
820
+
821
+ let tmpIndex = parseInt(tmpHandleType.replace('bezier-handle-', ''), 10);
822
+ if (isNaN(tmpIndex)) return;
823
+
824
+ this._FlowView.removeTetherHandle(tmpPanelHash, tmpIndex);
825
+ }
826
+
761
827
  // ---- Line Mode Toggling ----
762
828
 
763
829
  _toggleConnectionLineMode(pConnectionHash)
@@ -298,12 +298,74 @@ class PictServiceFlowTether extends libFableServiceProviderBase
298
298
  }
299
299
  else
300
300
  {
301
+ // Check for multi-handle array first
302
+ let tmpHandles = this._getTetherBezierHandles(pPanelData);
303
+ if (tmpHandles.length > 0)
304
+ {
305
+ return this.generateMultiBezierPath(pFrom, pTo, tmpHandles);
306
+ }
307
+
308
+ // Single-handle legacy path
301
309
  let tmpHandleX = (pPanelData.TetherHandleCustomized && pPanelData.TetherBezierHandleX != null) ? pPanelData.TetherBezierHandleX : null;
302
310
  let tmpHandleY = (pPanelData.TetherHandleCustomized && pPanelData.TetherBezierHandleY != null) ? pPanelData.TetherBezierHandleY : null;
303
311
  return this.generateBezierPath(pFrom, pTo, tmpHandleX, tmpHandleY);
304
312
  }
305
313
  }
306
314
 
315
+ /**
316
+ * Get the bezier handles array for a tether, respecting the customized flag.
317
+ * @param {Object} pPanelData
318
+ * @returns {Array<{x: number, y: number}>}
319
+ */
320
+ _getTetherBezierHandles(pPanelData)
321
+ {
322
+ if (!pPanelData || !pPanelData.TetherHandleCustomized)
323
+ {
324
+ return [];
325
+ }
326
+
327
+ // Multi-handle format
328
+ if (Array.isArray(pPanelData.TetherBezierHandles) && pPanelData.TetherBezierHandles.length > 0)
329
+ {
330
+ return pPanelData.TetherBezierHandles;
331
+ }
332
+
333
+ // Legacy single-handle format
334
+ if (pPanelData.TetherBezierHandleX != null && pPanelData.TetherBezierHandleY != null)
335
+ {
336
+ return [{ x: pPanelData.TetherBezierHandleX, y: pPanelData.TetherBezierHandleY }];
337
+ }
338
+
339
+ return [];
340
+ }
341
+
342
+ /**
343
+ * Generate a multi-handle bezier path for a tether.
344
+ * Delegates to PathGenerator.buildMultiBezierPathString.
345
+ * @param {Object} pFrom - {x, y, side}
346
+ * @param {Object} pTo - {x, y, side}
347
+ * @param {Array<{x: number, y: number}>} pHandles
348
+ * @returns {string} SVG path d attribute
349
+ */
350
+ generateMultiBezierPath(pFrom, pTo, pHandles)
351
+ {
352
+ let tmpDepartDist = 20;
353
+ let tmpFromDir = this._FlowView._GeometryProvider.sideDirection(pFrom.side);
354
+ let tmpToDir = this._FlowView._GeometryProvider.sideDirection(pTo.side);
355
+
356
+ let tmpDepart = {
357
+ x: pFrom.x + tmpFromDir.dx * tmpDepartDist,
358
+ y: pFrom.y + tmpFromDir.dy * tmpDepartDist
359
+ };
360
+ let tmpApproach = {
361
+ x: pTo.x + tmpToDir.dx * tmpDepartDist,
362
+ y: pTo.y + tmpToDir.dy * tmpDepartDist
363
+ };
364
+
365
+ return this._FlowView._PathGenerator.buildMultiBezierPathString(
366
+ pFrom, tmpDepart, pFrom.side, tmpApproach, pTo.side, pTo, pHandles);
367
+ }
368
+
307
369
  // ---- Handle State Management ----
308
370
 
309
371
  /**
@@ -317,9 +379,34 @@ class PictServiceFlowTether extends libFableServiceProviderBase
317
379
  {
318
380
  pPanelData.TetherHandleCustomized = true;
319
381
 
382
+ // Multi-handle bezier: handle type is 'bezier-handle-N'
383
+ if (pHandleType && pHandleType.startsWith('bezier-handle-'))
384
+ {
385
+ let tmpIndex = parseInt(pHandleType.replace('bezier-handle-', ''), 10);
386
+ if (!isNaN(tmpIndex) && Array.isArray(pPanelData.TetherBezierHandles)
387
+ && tmpIndex < pPanelData.TetherBezierHandles.length)
388
+ {
389
+ pPanelData.TetherBezierHandles[tmpIndex].x = pX;
390
+ pPanelData.TetherBezierHandles[tmpIndex].y = pY;
391
+ }
392
+ return;
393
+ }
394
+
320
395
  switch (pHandleType)
321
396
  {
322
397
  case 'bezier-midpoint':
398
+ // Migrate to multi-handle array
399
+ if (!Array.isArray(pPanelData.TetherBezierHandles)
400
+ || pPanelData.TetherBezierHandles.length === 0)
401
+ {
402
+ pPanelData.TetherBezierHandles = [{ x: pX, y: pY }];
403
+ }
404
+ else
405
+ {
406
+ pPanelData.TetherBezierHandles[0].x = pX;
407
+ pPanelData.TetherBezierHandles[0].y = pY;
408
+ }
409
+ // Keep legacy fields in sync
323
410
  pPanelData.TetherBezierHandleX = pX;
324
411
  pPanelData.TetherBezierHandleY = pY;
325
412
  break;
@@ -353,6 +440,7 @@ class PictServiceFlowTether extends libFableServiceProviderBase
353
440
  if (pPanelData.TetherHandleCustomized)
354
441
  {
355
442
  pPanelData.TetherHandleCustomized = false;
443
+ pPanelData.TetherBezierHandles = [];
356
444
  pPanelData.TetherBezierHandleX = null;
357
445
  pPanelData.TetherBezierHandleY = null;
358
446
  pPanelData.TetherOrthoCorner1X = null;
@@ -381,6 +469,68 @@ class PictServiceFlowTether extends libFableServiceProviderBase
381
469
  }
382
470
  }
383
471
 
472
+ /**
473
+ * Add a bezier handle to a tether at the specified position.
474
+ * @param {Object} pPanelData - Panel data
475
+ * @param {number} pX
476
+ * @param {number} pY
477
+ * @param {Object} pFrom - Panel anchor {x, y, side}
478
+ * @param {Object} pTo - Node anchor {x, y, side}
479
+ */
480
+ addHandle(pPanelData, pX, pY, pFrom, pTo)
481
+ {
482
+ // Ensure bezier mode and multi-handle array
483
+ pPanelData.TetherLineMode = 'bezier';
484
+
485
+ if (!Array.isArray(pPanelData.TetherBezierHandles))
486
+ {
487
+ pPanelData.TetherBezierHandles = [];
488
+ // Migrate legacy single-handle if present
489
+ if (pPanelData.TetherBezierHandleX != null && pPanelData.TetherBezierHandleY != null)
490
+ {
491
+ pPanelData.TetherBezierHandles.push({
492
+ x: pPanelData.TetherBezierHandleX,
493
+ y: pPanelData.TetherBezierHandleY
494
+ });
495
+ }
496
+ }
497
+
498
+ // Compute insertion index
499
+ let tmpInsertIndex = 0;
500
+ if (this._FlowView._ConnectionRenderer && pFrom && pTo)
501
+ {
502
+ tmpInsertIndex = this._FlowView._ConnectionRenderer.computeInsertionIndex(
503
+ pPanelData.TetherBezierHandles,
504
+ { x: pX, y: pY },
505
+ pFrom,
506
+ pTo
507
+ );
508
+ }
509
+
510
+ pPanelData.TetherBezierHandles.splice(tmpInsertIndex, 0, { x: pX, y: pY });
511
+ pPanelData.TetherHandleCustomized = true;
512
+ }
513
+
514
+ /**
515
+ * Remove a bezier handle from a tether by index.
516
+ * @param {Object} pPanelData - Panel data
517
+ * @param {number} pIndex - Index in TetherBezierHandles array
518
+ */
519
+ removeHandle(pPanelData, pIndex)
520
+ {
521
+ if (!Array.isArray(pPanelData.TetherBezierHandles)) return;
522
+ if (pIndex < 0 || pIndex >= pPanelData.TetherBezierHandles.length) return;
523
+
524
+ pPanelData.TetherBezierHandles.splice(pIndex, 1);
525
+
526
+ if (pPanelData.TetherBezierHandles.length === 0)
527
+ {
528
+ pPanelData.TetherHandleCustomized = false;
529
+ pPanelData.TetherBezierHandleX = null;
530
+ pPanelData.TetherBezierHandleY = null;
531
+ }
532
+ }
533
+
384
534
  /**
385
535
  * Toggle tether line mode between bezier and orthogonal.
386
536
  * Resets handle positions on toggle.
@@ -393,6 +543,7 @@ class PictServiceFlowTether extends libFableServiceProviderBase
393
543
  pPanelData.TetherLineMode = (tmpCurrentMode === 'bezier') ? 'orthogonal' : 'bezier';
394
544
 
395
545
  pPanelData.TetherHandleCustomized = false;
546
+ pPanelData.TetherBezierHandles = [];
396
547
  pPanelData.TetherBezierHandleX = null;
397
548
  pPanelData.TetherBezierHandleY = null;
398
549
  pPanelData.TetherOrthoCorner1X = null;
@@ -491,22 +642,25 @@ class PictServiceFlowTether extends libFableServiceProviderBase
491
642
  }
492
643
  else
493
644
  {
494
- // Bezier: single midpoint handle
495
- let tmpMidX, tmpMidY;
496
- if (pPanelData.TetherHandleCustomized && pPanelData.TetherBezierHandleX != null)
645
+ // Bezier handles
646
+ let tmpHandles = this._getTetherBezierHandles(pPanelData);
647
+
648
+ if (tmpHandles.length > 0)
497
649
  {
498
- tmpMidX = pPanelData.TetherBezierHandleX;
499
- tmpMidY = pPanelData.TetherBezierHandleY;
650
+ // Multi-handle: render each handle as a draggable circle
651
+ for (let i = 0; i < tmpHandles.length; i++)
652
+ {
653
+ this._createHandle(pTethersLayer, pPanelData.Hash, 'bezier-handle-' + i,
654
+ tmpHandles[i].x, tmpHandles[i].y, 'pict-flow-tether-handle');
655
+ }
500
656
  }
501
657
  else
502
658
  {
659
+ // No custom handles: show auto-computed midpoint
503
660
  let tmpMid = this.getAutoMidpoint(pFrom, pTo);
504
- tmpMidX = tmpMid.x;
505
- tmpMidY = tmpMid.y;
661
+ this._createHandle(pTethersLayer, pPanelData.Hash, 'bezier-midpoint',
662
+ tmpMid.x, tmpMid.y, 'pict-flow-tether-handle-midpoint');
506
663
  }
507
-
508
- this._createHandle(pTethersLayer, pPanelData.Hash, 'bezier-midpoint',
509
- tmpMidX, tmpMidY, 'pict-flow-tether-handle-midpoint');
510
664
  }
511
665
  }
512
666
 
@@ -25,7 +25,10 @@ const _DefaultConfiguration =
25
25
  <button class="pict-flow-floating-btn" data-flow-action="add-node" title="Add Node">
26
26
  <span id="Flow-FloatingIcon-plus-{~D:Record.FlowViewIdentifier~}"></span>
27
27
  </button>
28
- <button class="pict-flow-floating-btn danger" data-flow-action="delete-selected" title="Delete Selected">
28
+ <button class="pict-flow-floating-btn" data-flow-action="cards-popup" title="Cards">
29
+ <span id="Flow-FloatingIcon-cards-{~D:Record.FlowViewIdentifier~}"></span>
30
+ </button>
31
+ <button class="pict-flow-floating-btn" data-flow-action="delete-selected" title="Delete Selected">
29
32
  <span id="Flow-FloatingIcon-trash-{~D:Record.FlowViewIdentifier~}"></span>
30
33
  </button>
31
34
  <div class="pict-flow-floating-separator"></div>
@@ -42,9 +45,6 @@ const _DefaultConfiguration =
42
45
  <button class="pict-flow-floating-btn" data-flow-action="auto-layout" title="Auto Layout">
43
46
  <span id="Flow-FloatingIcon-auto-layout-{~D:Record.FlowViewIdentifier~}"></span>
44
47
  </button>
45
- <button class="pict-flow-floating-btn" data-flow-action="cards-popup" title="Cards">
46
- <span id="Flow-FloatingIcon-cards-{~D:Record.FlowViewIdentifier~}"></span>
47
- </button>
48
48
  <button class="pict-flow-floating-btn" data-flow-action="layout-popup" title="Layout">
49
49
  <span id="Flow-FloatingIcon-layout-{~D:Record.FlowViewIdentifier~}"></span>
50
50
  </button>
@@ -155,6 +155,27 @@ class PictViewFlowFloatingToolbar extends libPictView
155
155
  // Populate icons
156
156
  this._populateIcons();
157
157
 
158
+ // Hide buttons based on options
159
+ if (tmpFloatingToolbar.length > 0)
160
+ {
161
+ if (this.options.EnableAddNode === false)
162
+ {
163
+ let tmpAddNodeBtn = tmpFloatingToolbar[0].querySelector('[data-flow-action="add-node"]');
164
+ if (tmpAddNodeBtn)
165
+ {
166
+ tmpAddNodeBtn.style.display = 'none';
167
+ }
168
+ }
169
+ if (this.options.EnableCardPalette === false)
170
+ {
171
+ let tmpCardsBtn = tmpFloatingToolbar[0].querySelector('[data-flow-action="cards-popup"]');
172
+ if (tmpCardsBtn)
173
+ {
174
+ tmpCardsBtn.style.display = 'none';
175
+ }
176
+ }
177
+ }
178
+
158
179
  return super.onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent);
159
180
  }
160
181
 
@@ -12,6 +12,8 @@ const _DefaultConfiguration =
12
12
  FlowViewIdentifier: 'Pict-Flow',
13
13
 
14
14
  EnablePalette: true,
15
+ EnableAddNode: true,
16
+ EnableCardPalette: true,
15
17
 
16
18
  CSS: false,
17
19
 
@@ -26,16 +28,14 @@ const _DefaultConfiguration =
26
28
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-plus-{~D:Record.FlowViewIdentifier~}"></span>
27
29
  <span class="pict-flow-toolbar-btn-text">Node</span>
28
30
  </button>
29
- <button class="pict-flow-toolbar-btn danger" data-flow-action="delete-selected" title="Delete Node">
30
- <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-trash-{~D:Record.FlowViewIdentifier~}"></span>
31
- </button>
32
- </div>
33
- <div class="pict-flow-toolbar-group">
34
31
  <button class="pict-flow-toolbar-btn" data-flow-action="cards-popup" id="Flow-Toolbar-Cards-{~D:Record.FlowViewIdentifier~}" title="Card Palette">
35
32
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-cards-{~D:Record.FlowViewIdentifier~}"></span>
36
33
  <span class="pict-flow-toolbar-btn-text">Cards</span>
37
34
  <span class="pict-flow-toolbar-btn-chevron" id="Flow-Toolbar-CardsChevron-{~D:Record.FlowViewIdentifier~}"></span>
38
35
  </button>
36
+ <button class="pict-flow-toolbar-btn" data-flow-action="delete-selected" title="Delete Node">
37
+ <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-trash-{~D:Record.FlowViewIdentifier~}"></span>
38
+ </button>
39
39
  </div>
40
40
  <div class="pict-flow-toolbar-group">
41
41
  <button class="pict-flow-toolbar-btn" data-flow-action="layout-popup" id="Flow-Toolbar-Layout-{~D:Record.FlowViewIdentifier~}" title="Manage Layouts">
@@ -157,6 +157,24 @@ class PictViewFlowToolbar extends libPictView
157
157
  // Populate SVG icons for toolbar buttons
158
158
  this._populateToolbarIcons();
159
159
 
160
+ // Hide buttons based on options
161
+ if (this.options.EnableAddNode === false)
162
+ {
163
+ let tmpAddNodeBtn = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-AddNode-${tmpFlowViewIdentifier}`);
164
+ if (tmpAddNodeBtn.length > 0)
165
+ {
166
+ tmpAddNodeBtn[0].style.display = 'none';
167
+ }
168
+ }
169
+ if (this.options.EnableCardPalette === false)
170
+ {
171
+ let tmpCardsBtn = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-Cards-${tmpFlowViewIdentifier}`);
172
+ if (tmpCardsBtn.length > 0)
173
+ {
174
+ tmpCardsBtn[0].style.display = 'none';
175
+ }
176
+ }
177
+
160
178
  return super.onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent);
161
179
  }
162
180
 
@@ -508,38 +526,100 @@ class PictViewFlowToolbar extends libPictView
508
526
  // ── Cards Popup ───────────────────────────────────────────────────────
509
527
 
510
528
  /**
511
- * Build the Cards popup content (reuses palette rendering).
529
+ * Build the Cards popup content with search and categorized palette.
512
530
  * @param {HTMLElement} pContainer
513
531
  */
514
532
  _buildCardsPopup(pContainer)
515
533
  {
516
- this._renderPalette(pContainer);
534
+ // Search wrapper
535
+ let tmpSearchWrapper = document.createElement('div');
536
+ tmpSearchWrapper.className = 'pict-flow-popup-search-wrapper';
537
+
538
+ let tmpSearchIcon = document.createElement('span');
539
+ tmpSearchIcon.className = 'pict-flow-popup-search-icon';
540
+ let tmpIconProvider = this._FlowView ? this._FlowView._IconProvider : null;
541
+ if (tmpIconProvider)
542
+ {
543
+ tmpSearchIcon.innerHTML = tmpIconProvider.getIconSVGMarkup('search', 12);
544
+ }
545
+ tmpSearchWrapper.appendChild(tmpSearchIcon);
546
+
547
+ let tmpSearchInput = document.createElement('input');
548
+ tmpSearchInput.className = 'pict-flow-popup-search';
549
+ tmpSearchInput.setAttribute('type', 'text');
550
+ tmpSearchInput.setAttribute('placeholder', 'Search cards...');
551
+ tmpSearchWrapper.appendChild(tmpSearchInput);
552
+ pContainer.appendChild(tmpSearchWrapper);
553
+
554
+ // Palette list container
555
+ let tmpListDiv = document.createElement('div');
556
+ tmpListDiv.className = 'pict-flow-popup-node-list';
557
+ pContainer.appendChild(tmpListDiv);
558
+
559
+ // Initial population
560
+ this._renderPalette(tmpListDiv, '');
561
+
562
+ // Filter on input
563
+ tmpSearchInput.addEventListener('input', () =>
564
+ {
565
+ this._renderPalette(tmpListDiv, tmpSearchInput.value);
566
+ });
567
+
568
+ // Focus search input
569
+ setTimeout(() => { tmpSearchInput.focus(); }, 50);
517
570
  }
518
571
 
519
572
  /**
520
573
  * Render the card palette with categories and card chips into a container.
521
574
  * @param {HTMLElement} pContainer - The target container element
575
+ * @param {string} [pFilter] - Optional search filter text
522
576
  */
523
- _renderPalette(pContainer)
577
+ _renderPalette(pContainer, pFilter)
524
578
  {
525
579
  if (!this._FlowView || !this._FlowView._NodeTypeProvider) return;
526
580
 
527
- let tmpCategories = this._FlowView._NodeTypeProvider.getCardsByCategory();
528
- let tmpCategoryKeys = Object.keys(tmpCategories);
529
-
530
- if (tmpCategoryKeys.length === 0)
581
+ // Clear existing content
582
+ while (pContainer.firstChild)
531
583
  {
532
- let tmpEmpty = document.createElement('div');
533
- tmpEmpty.className = 'pict-flow-popup-list-empty';
534
- tmpEmpty.textContent = 'No card types available';
535
- pContainer.appendChild(tmpEmpty);
536
- return;
584
+ pContainer.removeChild(pContainer.firstChild);
537
585
  }
538
586
 
587
+ let tmpCategories = this._FlowView._NodeTypeProvider.getCardsByCategory();
588
+ let tmpCategoryKeys = Object.keys(tmpCategories);
589
+ let tmpFilter = (pFilter || '').toLowerCase().trim();
590
+ let tmpTotalMatchCount = 0;
591
+
539
592
  for (let i = 0; i < tmpCategoryKeys.length; i++)
540
593
  {
541
594
  let tmpCategoryName = tmpCategoryKeys[i];
542
595
  let tmpCards = tmpCategories[tmpCategoryName];
596
+ let tmpMatchingCards = [];
597
+
598
+ // Filter cards within this category
599
+ for (let j = 0; j < tmpCards.length; j++)
600
+ {
601
+ let tmpCardConfig = tmpCards[j];
602
+ let tmpMeta = tmpCardConfig.CardMetadata || {};
603
+
604
+ if (tmpFilter)
605
+ {
606
+ let tmpLabel = (tmpCardConfig.Label || '').toLowerCase();
607
+ let tmpCode = (tmpMeta.Code || '').toLowerCase();
608
+ let tmpCategory = tmpCategoryName.toLowerCase();
609
+ if (tmpLabel.indexOf(tmpFilter) < 0 &&
610
+ tmpCode.indexOf(tmpFilter) < 0 &&
611
+ tmpCategory.indexOf(tmpFilter) < 0)
612
+ {
613
+ continue;
614
+ }
615
+ }
616
+
617
+ tmpMatchingCards.push(tmpCardConfig);
618
+ }
619
+
620
+ if (tmpMatchingCards.length === 0) continue;
621
+
622
+ tmpTotalMatchCount += tmpMatchingCards.length;
543
623
 
544
624
  let tmpCategoryDiv = document.createElement('div');
545
625
  tmpCategoryDiv.className = 'pict-flow-palette-category';
@@ -553,9 +633,9 @@ class PictViewFlowToolbar extends libPictView
553
633
  let tmpCardsDiv = document.createElement('div');
554
634
  tmpCardsDiv.className = 'pict-flow-palette-cards';
555
635
 
556
- for (let j = 0; j < tmpCards.length; j++)
636
+ for (let j = 0; j < tmpMatchingCards.length; j++)
557
637
  {
558
- let tmpCardConfig = tmpCards[j];
638
+ let tmpCardConfig = tmpMatchingCards[j];
559
639
  let tmpMeta = tmpCardConfig.CardMetadata || {};
560
640
 
561
641
  let tmpCardEl = document.createElement('div');
@@ -635,6 +715,14 @@ class PictViewFlowToolbar extends libPictView
635
715
  tmpCategoryDiv.appendChild(tmpCardsDiv);
636
716
  pContainer.appendChild(tmpCategoryDiv);
637
717
  }
718
+
719
+ if (tmpTotalMatchCount === 0)
720
+ {
721
+ let tmpEmpty = document.createElement('div');
722
+ tmpEmpty.className = 'pict-flow-popup-list-empty';
723
+ tmpEmpty.textContent = tmpFilter ? 'No matching cards' : 'No card types available';
724
+ pContainer.appendChild(tmpEmpty);
725
+ }
638
726
  }
639
727
 
640
728
  // ── Layout Popup ──────────────────────────────────────────────────────
@@ -1018,7 +1106,9 @@ class PictViewFlowToolbar extends libPictView
1018
1106
  'PictViewFlowFloatingToolbar',
1019
1107
  {
1020
1108
  FlowViewIdentifier: tmpFlowViewIdentifier,
1021
- DefaultDestinationAddress: `#Flow-FloatingToolbar-Container-${tmpFlowViewIdentifier}`
1109
+ DefaultDestinationAddress: `#Flow-FloatingToolbar-Container-${tmpFlowViewIdentifier}`,
1110
+ EnableAddNode: this.options.EnableAddNode,
1111
+ EnableCardPalette: this.options.EnableCardPalette
1022
1112
  }
1023
1113
  );
1024
1114
  this._FloatingToolbarView._ToolbarView = this;
@@ -50,6 +50,9 @@ const _DefaultConfiguration =
50
50
  TargetElementAddress: '#Flow-SVG-Container',
51
51
 
52
52
  EnableToolbar: true,
53
+ EnableAddNode: true,
54
+ EnableCardPalette: true,
55
+ IncludeDefaultNodeTypes: true,
53
56
  EnablePanning: true,
54
57
  EnableZooming: true,
55
58
  EnableNodeDragging: true,
@@ -145,7 +148,7 @@ class PictViewFlow extends libPictView
145
148
  { ServiceType: 'PictProviderFlowIcons', Library: libPictProviderFlowIcons, Property: '_IconProvider', PostInit: 'registerIconTemplates' },
146
149
  { ServiceType: 'PictProviderFlowConnectorShapes', Library: libPictProviderFlowConnectorShapes, Property: '_ConnectorShapesProvider' },
147
150
  { ServiceType: 'PictProviderFlowPanelChrome', Library: libPictProviderFlowPanelChrome, Property: '_PanelChromeProvider' },
148
- { ServiceType: 'PictProviderFlowNodeTypes', Library: libPictProviderFlowNodeTypes, Property: '_NodeTypeProvider', ExtraOptions: () => ({ AdditionalNodeTypes: this.options.NodeTypes }) },
151
+ { ServiceType: 'PictProviderFlowNodeTypes', Library: libPictProviderFlowNodeTypes, Property: '_NodeTypeProvider', ExtraOptions: () => ({ AdditionalNodeTypes: this.options.NodeTypes, IncludeDefaultNodeTypes: this.options.IncludeDefaultNodeTypes }) },
149
152
  { ServiceType: 'PictProviderFlowEventHandler', Library: libPictProviderFlowEventHandler, Property: '_EventHandlerProvider' },
150
153
  { ServiceType: 'PictProviderFlowLayouts', Library: libPictProviderFlowLayouts, Property: '_LayoutProvider', PostInit: 'loadPersistedLayouts' },
151
154
 
@@ -410,7 +413,9 @@ class PictViewFlow extends libPictView
410
413
  {
411
414
  ViewIdentifier: `Flow-Toolbar-${tmpViewIdentifier}`,
412
415
  DefaultDestinationAddress: `#Flow-Toolbar-${tmpViewIdentifier}`,
413
- FlowViewIdentifier: tmpViewIdentifier
416
+ FlowViewIdentifier: tmpViewIdentifier,
417
+ EnableAddNode: this.options.EnableAddNode,
418
+ EnableCardPalette: this.options.EnableCardPalette
414
419
  }
415
420
  ));
416
421
  // Use the toolbar's render method after it's set up
@@ -689,6 +694,54 @@ class PictViewFlow extends libPictView
689
694
  _resetHandlesForNode(pNodeHash) { return this._ConnectionHandleManager.resetHandlesForNode(pNodeHash); }
690
695
  _resetHandlesForPanel(pPanelHash) { return this._ConnectionHandleManager.resetHandlesForPanel(pPanelHash); }
691
696
 
697
+ /**
698
+ * Add a bezier handle to a tether at the specified SVG position.
699
+ * @param {string} pPanelHash
700
+ * @param {number} pX
701
+ * @param {number} pY
702
+ */
703
+ addTetherHandle(pPanelHash, pX, pY)
704
+ {
705
+ let tmpPanel = this._FlowData.OpenPanels.find((pPanel) => pPanel.Hash === pPanelHash);
706
+ if (!tmpPanel || !this._TetherService) return;
707
+
708
+ let tmpNode = this.getNode(tmpPanel.NodeHash);
709
+ if (!tmpNode) return;
710
+
711
+ let tmpAnchors = this._TetherService.getSmartAnchors(tmpPanel, tmpNode);
712
+
713
+ this._TetherService.addHandle(tmpPanel, pX, pY, tmpAnchors.panelAnchor, tmpAnchors.nodeAnchor);
714
+
715
+ this.renderFlow();
716
+ this.marshalFromView();
717
+
718
+ if (this._EventHandlerProvider)
719
+ {
720
+ this._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowData);
721
+ }
722
+ }
723
+
724
+ /**
725
+ * Remove a bezier handle from a tether by index.
726
+ * @param {string} pPanelHash
727
+ * @param {number} pIndex
728
+ */
729
+ removeTetherHandle(pPanelHash, pIndex)
730
+ {
731
+ let tmpPanel = this._FlowData.OpenPanels.find((pPanel) => pPanel.Hash === pPanelHash);
732
+ if (!tmpPanel || !this._TetherService) return;
733
+
734
+ this._TetherService.removeHandle(tmpPanel, pIndex);
735
+
736
+ this.renderFlow();
737
+ this.marshalFromView();
738
+
739
+ if (this._EventHandlerProvider)
740
+ {
741
+ this._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowData);
742
+ }
743
+ }
744
+
692
745
  /**
693
746
  * Update a tether handle position during drag (for real-time feedback).
694
747
  * Delegates state update to the TetherService.