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.
@@ -11,6 +11,10 @@ const _DefaultConfiguration =
11
11
 
12
12
  FlowViewIdentifier: 'Pict-Flow',
13
13
 
14
+ // Host-supplied buttons (mirrors the docked toolbar's ToolbarExtraButtons), so floating mode keeps
15
+ // the same custom buttons. Each entry is { Hash, Icon, Label?, Tooltip?, Active? }.
16
+ ToolbarExtraButtons: [],
17
+
14
18
  CSS: false,
15
19
 
16
20
  Templates:
@@ -64,6 +68,7 @@ const _DefaultConfiguration =
64
68
  onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._FloatingToolbarView._handleButtonClick('fullscreen')">
65
69
  <span id="Flow-FloatingIcon-fullscreen-{~D:Record.FlowViewIdentifier~}"></span>
66
70
  </button>
71
+ {~TS:Flow-FloatingToolbar-Extra-Button:Record.ToolbarExtraButtons~}
67
72
  <div class="pict-flow-floating-separator"></div>
68
73
  <button class="pict-flow-floating-btn" data-flow-action="dock-toolbar" title="Dock Toolbar"
69
74
  onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._FloatingToolbarView._handleButtonClick('dock-toolbar')">
@@ -71,6 +76,15 @@ const _DefaultConfiguration =
71
76
  </button>
72
77
  </div>
73
78
  `
79
+ },
80
+ {
81
+ Hash: 'Flow-FloatingToolbar-Extra-Button',
82
+ // Icon-only host button (the floating toolbar is compact). Icon span
83
+ // is filled post-render by _populateIcons (keyed by Hash).
84
+ Template: /*html*/`<button class="pict-flow-floating-btn" title="{~D:Record.Tooltip~}" data-flow-action="extra" data-extra-hash="{~D:Record.Hash~}"
85
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._FloatingToolbarView._handleExtraClick('{~D:Record.Hash~}', this)">
86
+ <span id="Flow-FloatingExtraIcon-{~D:Record.Hash~}-{~D:Record.FlowViewIdentifier~}"></span>
87
+ </button>`
74
88
  }
75
89
  ],
76
90
 
@@ -111,9 +125,34 @@ class PictViewFlowFloatingToolbar extends libPictView
111
125
 
112
126
  render(pRenderableHash, pRenderDestinationAddress, pTemplateRecordAddress)
113
127
  {
128
+ // Stamp the owning view onto each host button so its row resolves.
129
+ let tmpExtraButtons = this.options.ToolbarExtraButtons;
130
+ if (Array.isArray(tmpExtraButtons))
131
+ {
132
+ for (let i = 0; i < tmpExtraButtons.length; i++)
133
+ {
134
+ tmpExtraButtons[i].FlowViewIdentifier = this.options.FlowViewIdentifier;
135
+ }
136
+ }
114
137
  return super.render(pRenderableHash, pRenderDestinationAddress, this.options);
115
138
  }
116
139
 
140
+ /**
141
+ * Handle a click on a host-supplied (ToolbarExtraButtons) floating button.
142
+ * Routes to the docked toolbar's _handleExtraAction (the single dispatch
143
+ * point that fires the FlowView's onToolbarButton hook).
144
+ *
145
+ * @param {string} pHash - The button's Hash
146
+ * @param {HTMLElement} pElement - The clicked button element
147
+ */
148
+ _handleExtraClick(pHash, pElement)
149
+ {
150
+ if (this._ToolbarView)
151
+ {
152
+ this._ToolbarView._handleExtraAction(pHash, pElement);
153
+ }
154
+ }
155
+
117
156
  onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent)
118
157
  {
119
158
  let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
@@ -222,6 +261,20 @@ class PictViewFlowFloatingToolbar extends libPictView
222
261
  tmpElements[0].innerHTML = tmpIconProvider.getIconSVGMarkup(tmpIconMap[tmpKeys[i]], 16);
223
262
  }
224
263
  }
264
+
265
+ // Host-supplied extra buttons (keyed by Hash, icon from the button's Icon key).
266
+ let tmpExtraButtons = this.options.ToolbarExtraButtons;
267
+ if (Array.isArray(tmpExtraButtons))
268
+ {
269
+ for (let i = 0; i < tmpExtraButtons.length; i++)
270
+ {
271
+ let tmpExtraIcon = this.pict.ContentAssignment.getElement(`#Flow-FloatingExtraIcon-${tmpExtraButtons[i].Hash}-${tmpFlowViewIdentifier}`);
272
+ if (tmpExtraIcon.length > 0)
273
+ {
274
+ tmpExtraIcon[0].innerHTML = tmpIconProvider.getIconSVGMarkup(tmpExtraButtons[i].Icon, 16);
275
+ }
276
+ }
277
+ }
225
278
  }
226
279
 
227
280
  /**
@@ -52,6 +52,13 @@ class PictViewFlowNode extends libPictView
52
52
  {
53
53
  tmpClassList += ' pict-flow-node-color-' + tmpColorRole;
54
54
  }
55
+ // A host can stamp an extra CSS class onto a node via node.NodeClass (e.g. a moodboard marks a
56
+ // card whose connection points should stay visible on a read-only board). Survives re-renders
57
+ // because it lives on the node data.
58
+ if (typeof pNodeData.NodeClass === 'string' && pNodeData.NodeClass)
59
+ {
60
+ tmpClassList += ' ' + pNodeData.NodeClass;
61
+ }
55
62
  tmpGroup.setAttribute('class', tmpClassList);
56
63
  tmpGroup.setAttribute('transform', `translate(${pNodeData.X}, ${pNodeData.Y})`);
57
64
  tmpGroup.setAttribute('data-node-hash', pNodeData.Hash);
@@ -86,6 +93,13 @@ class PictViewFlowNode extends libPictView
86
93
  pNodeData.Width = tmpWidth;
87
94
  pNodeData.Height = tmpHeight;
88
95
 
96
+ // Optional rotation (degrees) about the node's center. Applied on top of the position
97
+ // translate so free-form canvases can tilt a card; zero / unset leaves it axis-aligned.
98
+ if (typeof pNodeData.Rotation === 'number' && pNodeData.Rotation)
99
+ {
100
+ tmpGroup.setAttribute('transform', PictViewFlowNode.nodeTransform(pNodeData.X, pNodeData.Y, pNodeData.Rotation, tmpWidth, tmpHeight));
101
+ }
102
+
89
103
  // Determine node body mode from theme (bracket vs rect)
90
104
  let tmpNodeBodyMode = 'rect';
91
105
  if (this._FlowView._ThemeProvider)
@@ -160,8 +174,10 @@ class PictViewFlowNode extends libPictView
160
174
  tmpGroup.appendChild(tmpTypeLabel);
161
175
  }
162
176
 
163
- // FlowCard metadata: icon in title bar, code badge in body (hover-only via CSS)
164
- if (pNodeTypeConfig && pNodeTypeConfig.CardMetadata)
177
+ // FlowCard metadata: icon in title bar, code badge in body (hover-only via CSS). Skip the
178
+ // title icon entirely when there is no title bar (height 0), e.g. edge-to-edge moodboard
179
+ // cards, otherwise the default fallback glyph paints in the card's top-left corner.
180
+ if (pNodeTypeConfig && pNodeTypeConfig.CardMetadata && tmpTitleBarHeight > 0)
165
181
  {
166
182
  let tmpMeta = pNodeTypeConfig.CardMetadata;
167
183
  let tmpIconProvider = this._FlowView._IconProvider;
@@ -312,6 +328,24 @@ class PictViewFlowNode extends libPictView
312
328
  tmpGroup.appendChild(tmpIndicator);
313
329
  }
314
330
 
331
+ // Resize handle: a small grip at the bottom-right corner, shown only when this node is
332
+ // selected and the flow allows node resizing. Its data-element-type routes pointer-down to
333
+ // the InteractionManager's node-resize path. Appended last so it paints over the body.
334
+ if (pIsSelected && this._FlowView.options && this._FlowView.options.EnableNodeResizing)
335
+ {
336
+ let tmpHandleSize = 14;
337
+ let tmpHandle = this._FlowView._SVGHelperProvider.createSVGElement('rect');
338
+ tmpHandle.setAttribute('class', 'pict-flow-node-resize-handle');
339
+ tmpHandle.setAttribute('x', String(tmpWidth - (tmpHandleSize - 4)));
340
+ tmpHandle.setAttribute('y', String(tmpHeight - (tmpHandleSize - 4)));
341
+ tmpHandle.setAttribute('width', String(tmpHandleSize));
342
+ tmpHandle.setAttribute('height', String(tmpHandleSize));
343
+ tmpHandle.setAttribute('rx', '3');
344
+ tmpHandle.setAttribute('data-node-hash', pNodeData.Hash);
345
+ tmpHandle.setAttribute('data-element-type', 'node-resize');
346
+ tmpGroup.appendChild(tmpHandle);
347
+ }
348
+
315
349
  pNodesLayer.appendChild(tmpGroup);
316
350
  }
317
351
 
@@ -556,6 +590,26 @@ class PictViewFlowNode extends libPictView
556
590
  return Math.min(Math.max(8, tmpRadius), Math.floor(pTitleBarHeight / 2));
557
591
  }
558
592
 
593
+ /**
594
+ * The SVG group transform for a node: a position translate, plus a rotation about the node's
595
+ * center when a non-zero rotation (degrees) is given.
596
+ * @param {number} pX
597
+ * @param {number} pY
598
+ * @param {number} pRotation - degrees; 0 / non-number means no rotation
599
+ * @param {number} pWidth
600
+ * @param {number} pHeight
601
+ * @returns {string}
602
+ */
603
+ static nodeTransform(pX, pY, pRotation, pWidth, pHeight)
604
+ {
605
+ let tmpRotation = (typeof pRotation === 'number') ? pRotation : 0;
606
+ if (!tmpRotation)
607
+ {
608
+ return `translate(${pX}, ${pY})`;
609
+ }
610
+ return `translate(${pX}, ${pY}) rotate(${tmpRotation} ${pWidth / 2} ${pHeight / 2})`;
611
+ }
612
+
559
613
  _renderRectNodeBody(pGroup, pNodeData, pWidth, pHeight, pTitleBarHeight, pNodeTypeConfig)
560
614
  {
561
615
  // Per-card corner radius (a node-data or node-type override of the theme default), so a card
@@ -226,14 +226,36 @@ class PictViewFlowPropertiesPanel extends libPictView
226
226
  let tmpFO = pPanelsLayer.querySelector(`[data-panel-hash="${pPanelData.Hash}"]`);
227
227
  if (tmpFO)
228
228
  {
229
- // Render appearance and help tabs. Tab-switching click handlers
230
- // are inline `onclick=` attributes in Flow-PanelChrome-Template
231
- // that call FlowView._handlePanelTabClick switchPanelTab.
232
- this._renderAppearanceTab(pPanelData, tmpFO);
233
- this._renderHelpTab(pPanelData, tmpFO);
229
+ // The Appearance tab edits a node's body/title appearance, which a connection (edge) has none
230
+ // of -- so for a connection panel, hide that tab (and the now-single-tab bar) and leave just
231
+ // the connection's own panel. Node panels keep the appearance + help tabs.
232
+ if (pPanelData.ConnectionHash)
233
+ {
234
+ this._hidePanelTabsForConnection(tmpFO);
235
+ }
236
+ else
237
+ {
238
+ // Tab-switching click handlers are inline `onclick=` attributes in Flow-PanelChrome-Template
239
+ // that call FlowView._handlePanelTabClick → switchPanelTab.
240
+ this._renderAppearanceTab(pPanelData, tmpFO);
241
+ this._renderHelpTab(pPanelData, tmpFO);
242
+ }
234
243
  }
235
244
  }
236
245
 
246
+ // Hide the node-oriented Appearance (and Help) tabs plus the tab bar for a connection panel, so it
247
+ // shows only its single Properties pane with no lone tab.
248
+ _hidePanelTabsForConnection(pForeignObject)
249
+ {
250
+ if (!pForeignObject) { return; }
251
+ let tmpAppearanceTab = pForeignObject.querySelector('.pict-flow-panel-tab[data-tab-target="appearance"]');
252
+ if (tmpAppearanceTab) { tmpAppearanceTab.style.display = 'none'; }
253
+ let tmpAppearancePane = pForeignObject.querySelector('.pict-flow-panel-tab-pane[data-tab="appearance"]');
254
+ if (tmpAppearancePane) { tmpAppearancePane.style.display = 'none'; }
255
+ let tmpTabbar = pForeignObject.querySelector('.pict-flow-panel-tabbar');
256
+ if (tmpTabbar) { tmpTabbar.style.display = 'none'; }
257
+ }
258
+
237
259
  /**
238
260
  * Instantiate (or reuse) the panel type implementation and render into the body container.
239
261
  *
@@ -15,6 +15,10 @@ const _DefaultConfiguration =
15
15
  EnableAddNode: true,
16
16
  EnableCardPalette: true,
17
17
 
18
+ // Host-supplied buttons (set by the FlowView from its own ToolbarExtraButtons option). Each entry
19
+ // is { Hash, Icon, Label?, Tooltip?, Active? }. Rendered as a group via Flow-Toolbar-Extra-Button.
20
+ ToolbarExtraButtons: [],
21
+
18
22
  CSS: false,
19
23
 
20
24
  Templates:
@@ -77,6 +81,7 @@ const _DefaultConfiguration =
77
81
  <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-zoom-fit-{~D:Record.FlowViewIdentifier~}"></span>
78
82
  </button>
79
83
  </div>
84
+ <div class="pict-flow-toolbar-group pict-flow-toolbar-extra">{~TS:Flow-Toolbar-Extra-Button:Record.ToolbarExtraButtons~}</div>
80
85
  <div class="pict-flow-toolbar-group pict-flow-toolbar-right">
81
86
  <button class="pict-flow-toolbar-btn" data-flow-action="settings-popup" id="Flow-Toolbar-Settings-{~D:Record.FlowViewIdentifier~}" title="Theme Settings"
82
87
  onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('settings-popup')">
@@ -105,6 +110,18 @@ const _DefaultConfiguration =
105
110
  <div class="pict-flow-toolbar-popup-anchor" id="Flow-Toolbar-PopupAnchor-{~D:Record.FlowViewIdentifier~}">
106
111
  </div>
107
112
  `
113
+ },
114
+ {
115
+ Hash: 'Flow-Toolbar-Extra-Button',
116
+ // Host-supplied button. The icon span is filled post-render by
117
+ // _populateToolbarIcons (keyed by Hash), matching how the built-in
118
+ // button icons are injected. FlowViewIdentifier + ActiveClass are
119
+ // stamped onto each row in render().
120
+ Template: /*html*/`<button class="pict-flow-toolbar-btn{~D:Record.ActiveClass~}" id="Flow-Toolbar-Extra-{~D:Record.Hash~}-{~D:Record.FlowViewIdentifier~}" title="{~D:Record.Tooltip~}" data-flow-action="extra" data-extra-hash="{~D:Record.Hash~}"
121
+ onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleExtraAction('{~D:Record.Hash~}', this)">
122
+ <span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-ExtraIcon-{~D:Record.Hash~}-{~D:Record.FlowViewIdentifier~}"></span>
123
+ <span class="pict-flow-toolbar-btn-text">{~D:Record.Label~}</span>
124
+ </button>`
108
125
  },
109
126
  {
110
127
  Hash: 'Flow-AddNode-List',
@@ -121,7 +138,7 @@ const _DefaultConfiguration =
121
138
  + ' onclick="_Pict.views[\'{~D:Record.FlowViewIdentifier~}\']._ToolbarView._addNodeFromPopup(this.getAttribute(\'data-node-type\'))">'
122
139
  + '<span class="pict-flow-popup-list-item-icon">{~D:Record.IconHTML~}</span>'
123
140
  + '<span class="pict-flow-popup-list-item-label">{~D:Record.Label~}</span>'
124
- + '{~NE:Record.Code^<span class="pict-flow-popup-list-item-code">{~D:Record.Code~}</span>~}'
141
+ + '{~D:Record.CodeBlock~}'
125
142
  + '</div>'
126
143
  },
127
144
  {
@@ -137,13 +154,16 @@ const _DefaultConfiguration =
137
154
  },
138
155
  {
139
156
  Hash: 'Flow-Cards-Card',
157
+ // The icon / swatch / code spans are pre-rendered into complete HTML blocks by
158
+ // _buildCardsPopup (a block is '' when its piece is absent). The template can't build them
159
+ // inline: the engine does not parse a nested {~D:~} inside a {~NE:~} (its `~}` terminator
160
+ // collides with the inner tag's), which left the palette showing raw template literals.
140
161
  Template: '<div class="pict-flow-palette-card{~D:Record.DisabledClass~}" data-card-type="{~D:Record.CardType~}" title="{~D:Record.Tooltip~}"'
141
162
  + ' onclick="_Pict.views[\'{~D:Record.FlowViewIdentifier~}\']._ToolbarView._addCardFromPopup(this.getAttribute(\'data-card-type\'))">'
142
- + '{~NE:Record.IconHTML^<span class="pict-flow-palette-card-icon">{~D:Record.IconHTML~}</span>~}'
143
- + '{~NE:Record.IconEmoji^<span class="pict-flow-palette-card-icon">{~D:Record.IconEmoji~}</span>~}'
144
- + '{~NE:Record.SwatchColor^<span class="pict-flow-palette-card-swatch" style="background-color: {~D:Record.SwatchColor~};"></span>~}'
163
+ + '{~D:Record.IconBlock~}'
164
+ + '{~D:Record.SwatchBlock~}'
145
165
  + '<span class="pict-flow-palette-card-title">{~D:Record.Label~}</span>'
146
- + '{~NE:Record.Code^<span class="pict-flow-palette-card-code">{~D:Record.Code~}</span>~}'
166
+ + '{~D:Record.CodeBlock~}'
147
167
  + '</div>'
148
168
  },
149
169
  {
@@ -212,11 +232,32 @@ class PictViewFlowToolbar extends libPictView
212
232
 
213
233
  render(pRenderableHash, pRenderDestinationAddress, pTemplateRecordAddress)
214
234
  {
235
+ // Stamp the per-row render fields onto each host-supplied button so the
236
+ // Flow-Toolbar-Extra-Button rows resolve their owning view and active
237
+ // state (nested {~D:~} addressing inside {~TS:~} is not supported).
238
+ this._stampExtraButtons();
215
239
  // Pass this.options as the template record so {~D:Record.FlowViewIdentifier~}
216
240
  // resolves correctly in the toolbar template.
217
241
  return super.render(pRenderableHash, pRenderDestinationAddress, this.options);
218
242
  }
219
243
 
244
+ /**
245
+ * Stamp FlowViewIdentifier + ActiveClass onto each ToolbarExtraButtons entry
246
+ * so the row template can address them.
247
+ */
248
+ _stampExtraButtons()
249
+ {
250
+ let tmpExtraButtons = this.options.ToolbarExtraButtons;
251
+ if (!Array.isArray(tmpExtraButtons)) return;
252
+ for (let i = 0; i < tmpExtraButtons.length; i++)
253
+ {
254
+ tmpExtraButtons[i].FlowViewIdentifier = this.options.FlowViewIdentifier;
255
+ tmpExtraButtons[i].ActiveClass = tmpExtraButtons[i].Active ? ' pict-flow-toolbar-btn-active' : '';
256
+ // A label-less (icon-only) button renders an empty text span; CSS (:empty) collapses it.
257
+ if (typeof tmpExtraButtons[i].Label !== 'string') { tmpExtraButtons[i].Label = ''; }
258
+ }
259
+ }
260
+
220
261
  onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent)
221
262
  {
222
263
  let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
@@ -321,6 +362,20 @@ class PictViewFlowToolbar extends libPictView
321
362
  {
322
363
  tmpAutoChevron[0].innerHTML = tmpIconProvider.getIconSVGMarkup('chevron-down', 8);
323
364
  }
365
+
366
+ // Host-supplied extra buttons (keyed by Hash, icon from the button's Icon key).
367
+ let tmpExtraButtons = this.options.ToolbarExtraButtons;
368
+ if (Array.isArray(tmpExtraButtons))
369
+ {
370
+ for (let i = 0; i < tmpExtraButtons.length; i++)
371
+ {
372
+ let tmpExtraIcon = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-ExtraIcon-${tmpExtraButtons[i].Hash}-${tmpFlowViewIdentifier}`);
373
+ if (tmpExtraIcon.length > 0)
374
+ {
375
+ tmpExtraIcon[0].innerHTML = tmpIconProvider.getIconSVGMarkup(tmpExtraButtons[i].Icon, 14);
376
+ }
377
+ }
378
+ }
324
379
  }
325
380
 
326
381
  // ── Popup Management ──────────────────────────────────────────────────
@@ -548,12 +603,16 @@ class PictViewFlowToolbar extends libPictView
548
603
  let tmpResolvedKey = tmpIconProvider.resolveIconKey(tmpMeta);
549
604
  tmpIconHTML = tmpIconProvider.getIconSVGMarkup(tmpResolvedKey, 16);
550
605
  }
606
+ let tmpRowCode = tmpMeta.Code || '';
551
607
  tmpRows.push(
552
608
  {
553
609
  NodeType: tmpTypeKeys[i],
554
610
  Label: tmpTypeConfig.Label || '',
555
611
  IconHTML: tmpIconHTML,
556
- Code: tmpMeta.Code || '',
612
+ Code: tmpRowCode,
613
+ // Pre-rendered so the template renders it with {~D:~} (a nested {~D:~} inside {~NE:~} is
614
+ // not parsed by the engine).
615
+ CodeBlock: tmpRowCode ? ('<span class="pict-flow-popup-list-item-code">' + tmpRowCode + '</span>') : '',
557
616
  FlowViewIdentifier: tmpFlowViewIdentifier
558
617
  });
559
618
  }
@@ -682,16 +741,27 @@ class PictViewFlowToolbar extends libPictView
682
741
  tmpIconHTML = tmpIconProvider.getIconSVGMarkup('default', 14);
683
742
  }
684
743
 
744
+ // Pre-render each conditional span into a complete HTML block ('' when absent). The
745
+ // template renders these directly with {~D:~}; it cannot wrap them inline because the
746
+ // engine does not parse a nested {~D:~} inside a {~NE:~}.
747
+ let tmpCode = tmpMeta.Code || '';
748
+ let tmpSwatchColor = (!tmpIconHTML && !tmpIsEmoji && tmpCardConfig.TitleBarColor) ? tmpCardConfig.TitleBarColor : '';
749
+ let tmpIconBlock = '';
750
+ if (tmpIconHTML) { tmpIconBlock = '<span class="pict-flow-palette-card-icon">' + tmpIconHTML + '</span>'; }
751
+ else if (tmpIsEmoji) { tmpIconBlock = '<span class="pict-flow-palette-card-icon">' + tmpMeta.Icon + '</span>'; }
752
+ let tmpSwatchBlock = tmpSwatchColor ? ('<span class="pict-flow-palette-card-swatch" style="background-color: ' + tmpSwatchColor + ';"></span>') : '';
753
+ let tmpCodeBlock = tmpCode ? ('<span class="pict-flow-palette-card-code">' + tmpCode + '</span>') : '';
754
+
685
755
  tmpMatching.push(
686
756
  {
687
757
  CardType: tmpCardConfig.Hash,
688
758
  Label: tmpCardConfig.Label || '',
689
- Code: tmpMeta.Code || '',
690
- IconHTML: tmpIconHTML,
691
- IconEmoji: tmpIsEmoji ? tmpMeta.Icon : '',
759
+ Code: tmpCode,
760
+ IconBlock: tmpIconBlock,
761
+ SwatchBlock: tmpSwatchBlock,
762
+ CodeBlock: tmpCodeBlock,
692
763
  DisabledClass: (tmpMeta.Enabled === false) ? ' disabled' : '',
693
764
  Tooltip: tmpMeta.Tooltip || tmpMeta.Description || '',
694
- SwatchColor: (!tmpIconHTML && !tmpIsEmoji && tmpCardConfig.TitleBarColor) ? tmpCardConfig.TitleBarColor : '',
695
765
  FlowViewIdentifier: tmpFlowViewIdentifier
696
766
  });
697
767
  }
@@ -1677,7 +1747,8 @@ class PictViewFlowToolbar extends libPictView
1677
1747
  FlowViewIdentifier: tmpFlowViewIdentifier,
1678
1748
  DefaultDestinationAddress: `#Flow-FloatingToolbar-Container-${tmpFlowViewIdentifier}`,
1679
1749
  EnableAddNode: this.options.EnableAddNode,
1680
- EnableCardPalette: this.options.EnableCardPalette
1750
+ EnableCardPalette: this.options.EnableCardPalette,
1751
+ ToolbarExtraButtons: this.options.ToolbarExtraButtons
1681
1752
  }
1682
1753
  );
1683
1754
  this._FloatingToolbarView._ToolbarView = this;
@@ -1742,6 +1813,23 @@ class PictViewFlowToolbar extends libPictView
1742
1813
  * Handle a toolbar action
1743
1814
  * @param {string} pAction
1744
1815
  */
1816
+ /**
1817
+ * Handle a click on a host-supplied (ToolbarExtraButtons) button. Routes to
1818
+ * the FlowView's onToolbarButton hook with the button hash and the clicked
1819
+ * element (so the host can anchor a popover next to it).
1820
+ *
1821
+ * @param {string} pHash - The button's Hash
1822
+ * @param {HTMLElement} pElement - The clicked button element
1823
+ */
1824
+ _handleExtraAction(pHash, pElement)
1825
+ {
1826
+ if (!this._FlowView) return;
1827
+ if (typeof this._FlowView.options.onToolbarButton === 'function')
1828
+ {
1829
+ this._FlowView.options.onToolbarButton(pHash, pElement);
1830
+ }
1831
+ }
1832
+
1745
1833
  _handleToolbarAction(pAction)
1746
1834
  {
1747
1835
  if (!this._FlowView) return;
@@ -59,10 +59,34 @@ const _DefaultConfiguration =
59
59
  EnableZooming: true,
60
60
  EnableNodeDragging: true,
61
61
  EnableConnectionCreation: true,
62
+ // When on, a connection can be drawn between ANY two ports (any port can start a drag, any port can
63
+ // receive it), rather than only output -> input. Off by default so directed graphs (workflows) keep
64
+ // their source/target semantics; free-form canvases (a moodboard, whose links are undirected) turn it
65
+ // on so a card's ports connect in any direction.
66
+ EnableUndirectedConnections: false,
67
+ // When on, the selected node shows a bottom-right grip that resizes it by drag. Off by default
68
+ // so existing diagrams are unaffected; free-form canvases (moodboards) turn it on.
69
+ EnableNodeResizing: false,
62
70
  EnableGridSnap: false,
63
71
  GridSnapSize: 20,
72
+ // When on, several nodes can be selected at once: shift-click a node to toggle it, drag on the
73
+ // empty canvas to marquee-select (shift+drag pans), and dragging any selected node moves them all.
74
+ // Off by default so single-selection diagrams are unaffected; free-form canvases turn it on.
75
+ EnableMultiSelect: false,
76
+ // When on, dragging a single node shows alignment guide lines (and snaps) as its edges or centers
77
+ // line up with other nodes. Off by default; free-form canvases turn it on.
78
+ EnableAlignmentGuides: false,
64
79
  EnableLayoutMenu: true,
65
80
 
81
+ // Host-supplied toolbar buttons. Each entry is { Hash, Icon, Label?, Tooltip?, Active? } where Icon
82
+ // is a flow icon-provider key (edit, check, background, ...). They render in BOTH the docked and the
83
+ // floating toolbar (so they survive every toolbar mode) and, on click, fire onToolbarButton below.
84
+ // Empty by default, so existing consumers are unaffected.
85
+ ToolbarExtraButtons: [],
86
+ // Fired when a ToolbarExtraButtons button is clicked: onToolbarButton(pHash, pElement). The element
87
+ // lets the host anchor a popover next to the button. Off (false) by default.
88
+ onToolbarButton: false,
89
+
66
90
  MinZoom: 0.1,
67
91
  MaxZoom: 5.0,
68
92
  ZoomStep: 0.1,
@@ -70,6 +94,8 @@ const _DefaultConfiguration =
70
94
  DefaultNodeType: 'default',
71
95
  DefaultNodeWidth: 180,
72
96
  DefaultNodeHeight: 80,
97
+ MinimumNodeWidth: 48,
98
+ MinimumNodeHeight: 32,
73
99
 
74
100
  // Properties panel for connections (edges). Connections are not typed, so one config serves
75
101
  // them all: { PanelType, DefaultWidth, DefaultHeight, Title, Configuration }. When set, a
@@ -227,6 +253,9 @@ class PictViewFlow extends libPictView
227
253
  PanY: 0,
228
254
  Zoom: 1,
229
255
  SelectedNodeHash: null,
256
+ // The full selection set (multi-select). SelectedNodeHash stays the primary / most
257
+ // recently touched member for backward compatibility; single-select keeps it == [hash].
258
+ SelectedNodeHashes: [],
230
259
  SelectedConnectionHash: null,
231
260
  SelectedTetherHash: null
232
261
  },
@@ -547,7 +576,8 @@ class PictViewFlow extends libPictView
547
576
  DefaultDestinationAddress: `#Flow-Toolbar-${tmpViewIdentifier}`,
548
577
  FlowViewIdentifier: tmpViewIdentifier,
549
578
  EnableAddNode: this.options.EnableAddNode,
550
- EnableCardPalette: this.options.EnableCardPalette
579
+ EnableCardPalette: this.options.EnableCardPalette,
580
+ ToolbarExtraButtons: this.options.ToolbarExtraButtons
551
581
  }
552
582
  ));
553
583
  // Use the toolbar's render method after it's set up
@@ -558,15 +588,13 @@ class PictViewFlow extends libPictView
558
588
  }
559
589
  }
560
590
 
561
- // Setup the node renderer
591
+ // Setup the node renderer. A consumer can override the node title-bar height (e.g. a moodboard
592
+ // sets it to 0 for edge-to-edge image and note cards) via the flow-level NodeTitleBarHeight
593
+ // option; otherwise the renderer keeps its own default.
594
+ let tmpNodeViewOptions = { ViewIdentifier: `Flow-NodeRenderer-${tmpViewIdentifier}`, AutoRender: false };
595
+ if (typeof this.options.NodeTitleBarHeight === 'number') { tmpNodeViewOptions.NodeTitleBarHeight = this.options.NodeTitleBarHeight; }
562
596
  this._NodeView = this.fable.instantiateServiceProviderWithoutRegistration('PictViewFlowNode',
563
- Object.assign({},
564
- libPictViewFlowNode.default_configuration,
565
- {
566
- ViewIdentifier: `Flow-NodeRenderer-${tmpViewIdentifier}`,
567
- AutoRender: false
568
- }
569
- ));
597
+ Object.assign({}, libPictViewFlowNode.default_configuration, tmpNodeViewOptions));
570
598
  this._NodeView._FlowView = this;
571
599
 
572
600
  // Setup the properties panel renderer
@@ -616,6 +644,33 @@ class PictViewFlow extends libPictView
616
644
  return this._SelectionManager.selectNode(pNodeHash);
617
645
  }
618
646
 
647
+ /**
648
+ * Toggle a node's membership in the selection set (multi-select; shift-click).
649
+ * @param {string} pNodeHash
650
+ */
651
+ toggleNodeSelection(pNodeHash)
652
+ {
653
+ return this._SelectionManager.toggleNodeSelection(pNodeHash);
654
+ }
655
+
656
+ /**
657
+ * Replace the selection set with the given node hashes (multi-select; marquee result).
658
+ * @param {Array<string>} pNodeHashes
659
+ */
660
+ selectNodes(pNodeHashes)
661
+ {
662
+ return this._SelectionManager.selectNodes(pNodeHashes);
663
+ }
664
+
665
+ /**
666
+ * The current selection set as an array of node hashes.
667
+ * @returns {Array<string>}
668
+ */
669
+ getSelectedNodeHashes()
670
+ {
671
+ return this._SelectionManager.getSelectedNodeHashes();
672
+ }
673
+
619
674
  /**
620
675
  * Select a connection
621
676
  * @param {string|null} pConnectionHash - Hash of the connection to select, or null to deselect
@@ -1378,6 +1433,27 @@ class PictViewFlow extends libPictView
1378
1433
  let tmpSource = this.getPortPosition(tmpConnection.SourceNodeHash, tmpConnection.SourcePortHash);
1379
1434
  let tmpTarget = this.getPortPosition(tmpConnection.TargetNodeHash, tmpConnection.TargetPortHash);
1380
1435
  if (!tmpSource || !tmpTarget) return null;
1436
+ // A connection renders as a curve, so the straight-line average of its endpoints sits OFF the
1437
+ // line (the panel tether would point into empty space). Prefer the true midpoint of the rendered
1438
+ // path -- getPointAtLength at half its length, which is genuinely on the line. Fall back to the
1439
+ // endpoint average when the path element or SVG geometry is unavailable (e.g. server-side render).
1440
+ if (this._SVGElement && typeof this._SVGElement.querySelector === 'function')
1441
+ {
1442
+ let tmpPathElement = this._SVGElement.querySelector('.pict-flow-connection[data-connection-hash="' + pConnectionHash + '"]');
1443
+ if (tmpPathElement && typeof tmpPathElement.getTotalLength === 'function' && typeof tmpPathElement.getPointAtLength === 'function')
1444
+ {
1445
+ try
1446
+ {
1447
+ let tmpLength = tmpPathElement.getTotalLength();
1448
+ if (tmpLength > 0)
1449
+ {
1450
+ let tmpPoint = tmpPathElement.getPointAtLength(tmpLength / 2);
1451
+ return { x: tmpPoint.x, y: tmpPoint.y };
1452
+ }
1453
+ }
1454
+ catch (pError) { /* fall through to the straight-line midpoint */ }
1455
+ }
1456
+ }
1381
1457
  return { x: (tmpSource.x + tmpTarget.x) / 2, y: (tmpSource.y + tmpTarget.y) / 2 };
1382
1458
  }
1383
1459
 
@@ -0,0 +1,43 @@
1
+ const libChai = require('chai');
2
+ const libExpect = libChai.expect;
3
+
4
+ const libPictViewFlowToolbar = require('../source/views/PictView-Flow-Toolbar.js');
5
+
6
+ // The card palette and the add-node list render one row per card type. Each row's icon / swatch / code
7
+ // pieces are conditional, but the pict template engine does NOT parse a nested {~D:~} inside a {~NE:~}
8
+ // (the NE `~}` terminator collides with the inner tag's), which left the palette showing raw template
9
+ // literals like "{~D:Record.IconHTML ~}~};">~} Image". The fix pre-renders each piece into a complete
10
+ // HTML block in the popup builder and renders it with a plain {~D:~}. These tests guard that the
11
+ // broken pattern does not return.
12
+ suite('Flow card palette + add-node row templates',
13
+ function ()
14
+ {
15
+ function templateByHash(pHash)
16
+ {
17
+ let tmpTemplate = libPictViewFlowToolbar.default_configuration.Templates.find((pT) => pT.Hash === pHash);
18
+ return tmpTemplate ? tmpTemplate.Template : null;
19
+ }
20
+
21
+ // A {~NE:Addr^ ... {~ ... ~} ... ~} block: an NE whose content contains a nested template tag.
22
+ const _NESTED_NE = /\{~NE:[^^]+\^[^~]*\{~/;
23
+
24
+ test('the card-palette row renders pre-built blocks, with no nested tag inside an NE',
25
+ function ()
26
+ {
27
+ let tmpTemplate = templateByHash('Flow-Cards-Card');
28
+ libExpect(tmpTemplate, 'Flow-Cards-Card template exists').to.be.a('string');
29
+ libExpect(_NESTED_NE.test(tmpTemplate), 'no nested {~..~} inside an {~NE:~}').to.equal(false);
30
+ libExpect(tmpTemplate).to.contain('{~D:Record.IconBlock~}');
31
+ libExpect(tmpTemplate).to.contain('{~D:Record.SwatchBlock~}');
32
+ libExpect(tmpTemplate).to.contain('{~D:Record.CodeBlock~}');
33
+ });
34
+
35
+ test('the add-node row renders a pre-built code block, with no nested tag inside an NE',
36
+ function ()
37
+ {
38
+ let tmpTemplate = templateByHash('Flow-AddNode-Row');
39
+ libExpect(tmpTemplate, 'Flow-AddNode-Row template exists').to.be.a('string');
40
+ libExpect(_NESTED_NE.test(tmpTemplate), 'no nested {~..~} inside an {~NE:~}').to.equal(false);
41
+ libExpect(tmpTemplate).to.contain('{~D:Record.CodeBlock~}');
42
+ });
43
+ });