pict-section-flow 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/docs/README.md +19 -0
  2. package/{example_application → example_applications/simple_cards}/html/index.html +2 -2
  3. package/example_applications/simple_cards/package.json +43 -0
  4. package/example_applications/simple_cards/source/Pict-Application-FlowExample.js +434 -0
  5. package/example_applications/simple_cards/source/cards/FlowCard-Each.js +36 -0
  6. package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +54 -0
  7. package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +48 -0
  8. package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +35 -0
  9. package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +47 -0
  10. package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +53 -0
  11. package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +95 -0
  12. package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +37 -0
  13. package/example_applications/simple_cards/source/views/PictView-FlowExample-FileWriteInfo.js +59 -0
  14. package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-Layout.js +5 -1
  15. package/example_applications/simple_cards/source/views/PictView-FlowExample-MainWorkspace.js +312 -0
  16. package/package.json +6 -6
  17. package/source/Pict-Section-Flow.js +19 -0
  18. package/source/PictFlowCard.js +207 -0
  19. package/source/PictFlowCardPropertiesPanel.js +105 -0
  20. package/source/panels/FlowCardPropertiesPanel-Form.js +174 -0
  21. package/source/panels/FlowCardPropertiesPanel-Markdown.js +148 -0
  22. package/source/panels/FlowCardPropertiesPanel-Template.js +88 -0
  23. package/source/panels/FlowCardPropertiesPanel-View.js +114 -0
  24. package/source/providers/PictProvider-Flow-EventHandler.js +19 -8
  25. package/source/providers/PictProvider-Flow-Geometry.js +64 -0
  26. package/source/providers/PictProvider-Flow-Layouts.js +284 -0
  27. package/source/providers/PictProvider-Flow-NodeTypes.js +70 -0
  28. package/source/providers/PictProvider-Flow-PanelChrome.js +72 -0
  29. package/source/providers/PictProvider-Flow-SVGHelpers.js +30 -0
  30. package/source/services/PictService-Flow-ConnectionRenderer.js +324 -66
  31. package/source/services/PictService-Flow-InteractionManager.js +399 -75
  32. package/source/services/PictService-Flow-Layout.js +159 -0
  33. package/source/services/PictService-Flow-PathGenerator.js +199 -0
  34. package/source/services/PictService-Flow-Tether.js +544 -0
  35. package/source/views/PictView-Flow-Node.js +95 -18
  36. package/source/views/PictView-Flow-PropertiesPanel.js +435 -0
  37. package/source/views/PictView-Flow-Toolbar.js +491 -5
  38. package/source/views/PictView-Flow.js +830 -8
  39. package/example_application/package.json +0 -41
  40. package/example_application/source/Pict-Application-FlowExample.js +0 -241
  41. package/example_application/source/views/PictView-FlowExample-MainWorkspace.js +0 -191
  42. /package/{example_application → example_applications/simple_cards}/css/flowexample.css +0 -0
  43. /package/{example_application → example_applications/simple_cards}/source/Pict-Application-FlowExample-Configuration.json +0 -0
  44. /package/{example_application → example_applications/simple_cards}/source/providers/PictRouter-FlowExample-Configuration.json +0 -0
  45. /package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-About.js +0 -0
  46. /package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-BottomBar.js +0 -0
  47. /package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-Documentation.js +0 -0
  48. /package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-TopBar.js +0 -0
@@ -22,16 +22,6 @@ class PictViewFlowNode extends libPictView
22
22
  this._FlowView = null;
23
23
  }
24
24
 
25
- /**
26
- * Create the SVG namespace element helper
27
- * @param {string} pTagName
28
- * @returns {SVGElement}
29
- */
30
- _createSVGElement(pTagName)
31
- {
32
- return document.createElementNS('http://www.w3.org/2000/svg', pTagName);
33
- }
34
-
35
25
  /**
36
26
  * Render a node into the nodes SVG layer
37
27
  * @param {Object} pNodeData - The node data object
@@ -41,7 +31,7 @@ class PictViewFlowNode extends libPictView
41
31
  */
42
32
  renderNode(pNodeData, pNodesLayer, pIsSelected, pNodeTypeConfig)
43
33
  {
44
- let tmpGroup = this._createSVGElement('g');
34
+ let tmpGroup = this._FlowView._SVGHelperProvider.createSVGElement('g');
45
35
  tmpGroup.setAttribute('class', `pict-flow-node ${pIsSelected ? 'selected' : ''} pict-flow-node-${pNodeData.Type || 'default'}`);
46
36
  tmpGroup.setAttribute('transform', `translate(${pNodeData.X}, ${pNodeData.Y})`);
47
37
  tmpGroup.setAttribute('data-node-hash', pNodeData.Hash);
@@ -52,7 +42,7 @@ class PictViewFlowNode extends libPictView
52
42
  let tmpTitleBarHeight = this.options.NodeTitleBarHeight;
53
43
 
54
44
  // Node body (main rectangle)
55
- let tmpBody = this._createSVGElement('rect');
45
+ let tmpBody = this._FlowView._SVGHelperProvider.createSVGElement('rect');
56
46
  tmpBody.setAttribute('class', 'pict-flow-node-body');
57
47
  tmpBody.setAttribute('x', '0');
58
48
  tmpBody.setAttribute('y', '0');
@@ -73,7 +63,7 @@ class PictViewFlowNode extends libPictView
73
63
  tmpGroup.appendChild(tmpBody);
74
64
 
75
65
  // Title bar background (top portion)
76
- let tmpTitleBar = this._createSVGElement('rect');
66
+ let tmpTitleBar = this._FlowView._SVGHelperProvider.createSVGElement('rect');
77
67
  tmpTitleBar.setAttribute('class', 'pict-flow-node-title-bar');
78
68
  tmpTitleBar.setAttribute('x', '0');
79
69
  tmpTitleBar.setAttribute('y', '0');
@@ -91,7 +81,7 @@ class PictViewFlowNode extends libPictView
91
81
  tmpGroup.appendChild(tmpTitleBar);
92
82
 
93
83
  // Title bar bottom fill (to square off the rounded corners at the bottom of the title bar)
94
- let tmpTitleBarBottom = this._createSVGElement('rect');
84
+ let tmpTitleBarBottom = this._FlowView._SVGHelperProvider.createSVGElement('rect');
95
85
  tmpTitleBarBottom.setAttribute('class', 'pict-flow-node-title-bar-bottom');
96
86
  tmpTitleBarBottom.setAttribute('x', '0');
97
87
  tmpTitleBarBottom.setAttribute('y', String(tmpTitleBarHeight - 6));
@@ -108,7 +98,7 @@ class PictViewFlowNode extends libPictView
108
98
  tmpGroup.appendChild(tmpTitleBarBottom);
109
99
 
110
100
  // Title text
111
- let tmpTitle = this._createSVGElement('text');
101
+ let tmpTitle = this._FlowView._SVGHelperProvider.createSVGElement('text');
112
102
  tmpTitle.setAttribute('class', 'pict-flow-node-title');
113
103
  tmpTitle.setAttribute('x', String(tmpWidth / 2));
114
104
  tmpTitle.setAttribute('y', String(tmpTitleBarHeight / 2 + 1));
@@ -120,7 +110,7 @@ class PictViewFlowNode extends libPictView
120
110
  // Type label (below title bar)
121
111
  if (pNodeTypeConfig && pNodeTypeConfig.Label && pNodeTypeConfig.Label !== pNodeData.Title)
122
112
  {
123
- let tmpTypeLabel = this._createSVGElement('text');
113
+ let tmpTypeLabel = this._FlowView._SVGHelperProvider.createSVGElement('text');
124
114
  tmpTypeLabel.setAttribute('class', 'pict-flow-node-type-label');
125
115
  tmpTypeLabel.setAttribute('x', String(tmpWidth / 2));
126
116
  tmpTypeLabel.setAttribute('y', String(tmpTitleBarHeight + 18));
@@ -130,9 +120,96 @@ class PictViewFlowNode extends libPictView
130
120
  tmpGroup.appendChild(tmpTypeLabel);
131
121
  }
132
122
 
123
+ // FlowCard metadata: render icon and code in the node body
124
+ if (pNodeTypeConfig && pNodeTypeConfig.CardMetadata)
125
+ {
126
+ let tmpMeta = pNodeTypeConfig.CardMetadata;
127
+ let tmpBodyCenterY = tmpTitleBarHeight + (tmpHeight - tmpTitleBarHeight) / 2;
128
+
129
+ // Icon (displayed as text, left-of-center or centered if no code)
130
+ if (tmpMeta.Icon)
131
+ {
132
+ let tmpIconText = this._FlowView._SVGHelperProvider.createSVGElement('text');
133
+ tmpIconText.setAttribute('class', 'pict-flow-node-card-icon');
134
+ tmpIconText.setAttribute('font-size', '16');
135
+ tmpIconText.setAttribute('text-anchor', 'middle');
136
+ tmpIconText.setAttribute('dominant-baseline', 'central');
137
+ tmpIconText.setAttribute('pointer-events', 'none');
138
+
139
+ if (tmpMeta.Code)
140
+ {
141
+ // Icon on the left, code on the right
142
+ tmpIconText.setAttribute('x', String(tmpWidth * 0.33));
143
+ }
144
+ else
145
+ {
146
+ tmpIconText.setAttribute('x', String(tmpWidth / 2));
147
+ }
148
+ tmpIconText.setAttribute('y', String(tmpBodyCenterY));
149
+ tmpIconText.textContent = tmpMeta.Icon;
150
+ tmpGroup.appendChild(tmpIconText);
151
+ }
152
+
153
+ // Code badge (displayed as monospace text)
154
+ if (tmpMeta.Code)
155
+ {
156
+ let tmpCodeText = this._FlowView._SVGHelperProvider.createSVGElement('text');
157
+ tmpCodeText.setAttribute('class', 'pict-flow-node-card-code');
158
+ tmpCodeText.setAttribute('font-size', '10');
159
+ tmpCodeText.setAttribute('font-family', 'monospace');
160
+ tmpCodeText.setAttribute('fill', '#7f8c8d');
161
+ tmpCodeText.setAttribute('text-anchor', 'middle');
162
+ tmpCodeText.setAttribute('dominant-baseline', 'central');
163
+ tmpCodeText.setAttribute('pointer-events', 'none');
164
+
165
+ if (tmpMeta.Icon)
166
+ {
167
+ tmpCodeText.setAttribute('x', String(tmpWidth * 0.67));
168
+ }
169
+ else
170
+ {
171
+ tmpCodeText.setAttribute('x', String(tmpWidth / 2));
172
+ }
173
+ tmpCodeText.setAttribute('y', String(tmpBodyCenterY));
174
+ tmpCodeText.textContent = tmpMeta.Code;
175
+ tmpGroup.appendChild(tmpCodeText);
176
+ }
177
+
178
+ // Tooltip via SVG <title> element
179
+ if (tmpMeta.Tooltip || tmpMeta.Description)
180
+ {
181
+ let tmpSVGTitle = this._FlowView._SVGHelperProvider.createSVGElement('title');
182
+ tmpSVGTitle.textContent = tmpMeta.Tooltip || tmpMeta.Description;
183
+ tmpGroup.appendChild(tmpSVGTitle);
184
+ }
185
+ }
186
+
133
187
  // Render ports
134
188
  this._renderPorts(pNodeData, tmpGroup, tmpWidth, tmpHeight);
135
189
 
190
+ // Panel indicator icon (small rect in bottom-right corner)
191
+ if (pNodeTypeConfig && pNodeTypeConfig.PropertiesPanel)
192
+ {
193
+ let tmpIndicatorSize = 10;
194
+ let tmpIndicatorMargin = 4;
195
+ let tmpIndicator = this._FlowView._SVGHelperProvider.createSVGElement('rect');
196
+ tmpIndicator.setAttribute('class', 'pict-flow-node-panel-indicator');
197
+ tmpIndicator.setAttribute('x', String(tmpWidth - tmpIndicatorSize - tmpIndicatorMargin));
198
+ tmpIndicator.setAttribute('y', String(tmpHeight - tmpIndicatorSize - tmpIndicatorMargin));
199
+ tmpIndicator.setAttribute('width', String(tmpIndicatorSize));
200
+ tmpIndicator.setAttribute('height', String(tmpIndicatorSize));
201
+ tmpIndicator.setAttribute('rx', '2');
202
+ tmpIndicator.setAttribute('ry', '2');
203
+ tmpIndicator.setAttribute('data-node-hash', pNodeData.Hash);
204
+ tmpIndicator.setAttribute('data-element-type', 'panel-indicator');
205
+
206
+ let tmpIndicatorTitle = this._FlowView._SVGHelperProvider.createSVGElement('title');
207
+ tmpIndicatorTitle.textContent = 'Double-click to open properties';
208
+ tmpIndicator.appendChild(tmpIndicatorTitle);
209
+
210
+ tmpGroup.appendChild(tmpIndicator);
211
+ }
212
+
136
213
  pNodesLayer.appendChild(tmpGroup);
137
214
  }
138
215
 
@@ -168,7 +245,7 @@ class PictViewFlowNode extends libPictView
168
245
  let tmpPosition = this._getPortLocalPosition(tmpSide, i, tmpPorts.length, pWidth, pHeight);
169
246
 
170
247
  // Port circle
171
- let tmpCircle = this._createSVGElement('circle');
248
+ let tmpCircle = this._FlowView._SVGHelperProvider.createSVGElement('circle');
172
249
  tmpCircle.setAttribute('class', `pict-flow-port ${tmpPort.Direction}`);
173
250
  tmpCircle.setAttribute('cx', String(tmpPosition.x));
174
251
  tmpCircle.setAttribute('cy', String(tmpPosition.y));
@@ -182,7 +259,7 @@ class PictViewFlowNode extends libPictView
182
259
  // Port label
183
260
  if (tmpPort.Label)
184
261
  {
185
- let tmpLabel = this._createSVGElement('text');
262
+ let tmpLabel = this._FlowView._SVGHelperProvider.createSVGElement('text');
186
263
  tmpLabel.setAttribute('class', 'pict-flow-port-label');
187
264
  tmpLabel.textContent = tmpPort.Label;
188
265
 
@@ -0,0 +1,435 @@
1
+ const libPictView = require('pict-view');
2
+
3
+ const _DefaultConfiguration =
4
+ {
5
+ ViewIdentifier: 'Flow-PropertiesPanel',
6
+
7
+ AutoRender: false,
8
+
9
+ Templates:
10
+ [
11
+ {
12
+ Hash: 'Flow-InfoPanel-Wrapper',
13
+ Template: '<div class="pict-flow-info-panel">{~D:Record.PanelContent~}</div>'
14
+ },
15
+ {
16
+ Hash: 'Flow-InfoPanel-Header-Icon',
17
+ Template: '<div class="pict-flow-info-panel-header with-icon">{~D:Record.Icon~} {~D:Record.Label~}</div>'
18
+ },
19
+ {
20
+ Hash: 'Flow-InfoPanel-Header',
21
+ Template: '<div class="pict-flow-info-panel-header">{~D:Record.Label~}</div>'
22
+ },
23
+ {
24
+ Hash: 'Flow-InfoPanel-Description',
25
+ Template: '<div class="pict-flow-info-panel-description">{~D:Record.Description~}</div>'
26
+ },
27
+ {
28
+ Hash: 'Flow-InfoPanel-Badges',
29
+ Template: '<div class="pict-flow-info-panel-badges">{~D:Record.BadgesContent~}</div>'
30
+ },
31
+ {
32
+ Hash: 'Flow-InfoPanel-Badge-Category',
33
+ Template: '<span class="pict-flow-info-panel-badge category">{~D:Record.Category~}</span>'
34
+ },
35
+ {
36
+ Hash: 'Flow-InfoPanel-Badge-Code',
37
+ Template: '<span class="pict-flow-info-panel-badge code">{~D:Record.Code~}</span>'
38
+ },
39
+ {
40
+ Hash: 'Flow-InfoPanel-Section-Inputs',
41
+ Template: '<div class="pict-flow-info-panel-section"><div class="pict-flow-info-panel-section-title">Inputs</div>{~D:Record.PortsContent~}</div>'
42
+ },
43
+ {
44
+ Hash: 'Flow-InfoPanel-Section-Outputs',
45
+ Template: '<div class="pict-flow-info-panel-section"><div class="pict-flow-info-panel-section-title">Outputs</div>{~D:Record.PortsContent~}</div>'
46
+ },
47
+ {
48
+ Hash: 'Flow-InfoPanel-Port-Input',
49
+ Template: '<div class="pict-flow-info-panel-port input">{~D:Record.Label~}{~D:Record.Constraint~}</div>'
50
+ },
51
+ {
52
+ Hash: 'Flow-InfoPanel-Port-Output',
53
+ Template: '<div class="pict-flow-info-panel-port output">{~D:Record.Label~}</div>'
54
+ },
55
+ {
56
+ Hash: 'Flow-InfoPanel-Port-Constraint',
57
+ Template: ' <span class="pict-flow-info-panel-port-constraint">{~D:Record.ConstraintText~}</span>'
58
+ }
59
+ ]
60
+ };
61
+
62
+ /**
63
+ * PictView-Flow-PropertiesPanel
64
+ *
65
+ * Renders and manages all open properties panels on the flow graph.
66
+ * Panels are SVG foreignObject elements containing HTML, placed inside
67
+ * the viewport group so they zoom/pan with the graph.
68
+ *
69
+ * Responsibilities:
70
+ * - Reconcile DOM (add new panels, remove closed ones, update positions)
71
+ * - Render tether lines from each panel to its node
72
+ * - Manage panel instance cache (PictFlowCardPropertiesPanel subclasses)
73
+ * - Isolate HTML events from SVG interactions
74
+ */
75
+ class PictViewFlowPropertiesPanel extends libPictView
76
+ {
77
+ constructor(pFable, pOptions, pServiceHash)
78
+ {
79
+ let tmpOptions = Object.assign({}, JSON.parse(JSON.stringify(_DefaultConfiguration)), pOptions);
80
+ super(pFable, tmpOptions, pServiceHash);
81
+
82
+ this.serviceType = 'PictViewFlowPropertiesPanel';
83
+
84
+ this._FlowView = null;
85
+
86
+ // Cache of active panel instances: Map<panelHash, PictFlowCardPropertiesPanel>
87
+ this._PanelInstances = {};
88
+ }
89
+
90
+ /**
91
+ * Render all open panels and their tethers.
92
+ *
93
+ * Uses DOM reconciliation for panels (to preserve live HTML state)
94
+ * and clear-and-rebuild for tethers (trivial SVG lines).
95
+ *
96
+ * @param {Array} pOpenPanels - Array of panel data objects from _FlowData.OpenPanels
97
+ * @param {SVGGElement} pPanelsLayer - The SVG <g> for panel foreignObjects
98
+ * @param {SVGGElement} pTethersLayer - The SVG <g> for tether lines
99
+ * @param {string|null} pSelectedTetherHash - Hash of the selected tether's panel, or null
100
+ */
101
+ renderPanels(pOpenPanels, pPanelsLayer, pTethersLayer, pSelectedTetherHash)
102
+ {
103
+ if (!pPanelsLayer || !pTethersLayer) return;
104
+ if (!this._FlowView) return;
105
+
106
+ let tmpOpenPanels = Array.isArray(pOpenPanels) ? pOpenPanels : [];
107
+
108
+ // --- Reconcile panels layer (add new, remove closed, update positions) ---
109
+ let tmpExistingPanelHashes = new Set();
110
+ let tmpExistingForeignObjects = pPanelsLayer.querySelectorAll('.pict-flow-panel-foreign-object');
111
+ for (let i = 0; i < tmpExistingForeignObjects.length; i++)
112
+ {
113
+ tmpExistingPanelHashes.add(tmpExistingForeignObjects[i].getAttribute('data-panel-hash'));
114
+ }
115
+
116
+ let tmpDesiredPanelHashes = new Set();
117
+ for (let i = 0; i < tmpOpenPanels.length; i++)
118
+ {
119
+ tmpDesiredPanelHashes.add(tmpOpenPanels[i].Hash);
120
+ }
121
+
122
+ // Remove panels that are no longer open
123
+ for (let i = 0; i < tmpExistingForeignObjects.length; i++)
124
+ {
125
+ let tmpHash = tmpExistingForeignObjects[i].getAttribute('data-panel-hash');
126
+ if (!tmpDesiredPanelHashes.has(tmpHash))
127
+ {
128
+ tmpExistingForeignObjects[i].remove();
129
+ // Destroy cached instance
130
+ if (this._PanelInstances[tmpHash])
131
+ {
132
+ this._PanelInstances[tmpHash].destroy();
133
+ delete this._PanelInstances[tmpHash];
134
+ }
135
+ }
136
+ }
137
+
138
+ // Add or update panels
139
+ for (let i = 0; i < tmpOpenPanels.length; i++)
140
+ {
141
+ let tmpPanelData = tmpOpenPanels[i];
142
+
143
+ if (tmpExistingPanelHashes.has(tmpPanelData.Hash))
144
+ {
145
+ // Update position of existing panel
146
+ let tmpFO = pPanelsLayer.querySelector(`[data-panel-hash="${tmpPanelData.Hash}"]`);
147
+ if (tmpFO)
148
+ {
149
+ tmpFO.setAttribute('x', String(tmpPanelData.X));
150
+ tmpFO.setAttribute('y', String(tmpPanelData.Y));
151
+ tmpFO.setAttribute('width', String(tmpPanelData.Width));
152
+ tmpFO.setAttribute('height', String(tmpPanelData.Height));
153
+ }
154
+ }
155
+ else
156
+ {
157
+ // Create new panel
158
+ this._createPanelForeignObject(tmpPanelData, pPanelsLayer);
159
+ }
160
+ }
161
+
162
+ // --- Clear and rebuild tethers ---
163
+ while (pTethersLayer.firstChild)
164
+ {
165
+ pTethersLayer.removeChild(pTethersLayer.firstChild);
166
+ }
167
+
168
+ for (let i = 0; i < tmpOpenPanels.length; i++)
169
+ {
170
+ let tmpIsSelected = (pSelectedTetherHash === tmpOpenPanels[i].Hash);
171
+ this._renderTether(tmpOpenPanels[i], pTethersLayer, tmpIsSelected);
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Create a foreignObject containing the panel chrome and content.
177
+ * Delegates to the PanelChrome provider for template-based chrome creation,
178
+ * then renders panel content into the body container.
179
+ *
180
+ * @param {Object} pPanelData - Panel data from OpenPanels
181
+ * @param {SVGGElement} pPanelsLayer
182
+ */
183
+ _createPanelForeignObject(pPanelData, pPanelsLayer)
184
+ {
185
+ let tmpPanelChromeProvider = this._FlowView._PanelChromeProvider;
186
+ if (!tmpPanelChromeProvider) return;
187
+
188
+ let tmpBody = tmpPanelChromeProvider.createPanelForeignObject(pPanelData, pPanelsLayer);
189
+
190
+ // Render the panel content via the panel type implementation
191
+ if (tmpBody)
192
+ {
193
+ this._renderPanelContent(pPanelData, tmpBody);
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Instantiate (or reuse) the panel type implementation and render into the body container.
199
+ *
200
+ * @param {Object} pPanelData
201
+ * @param {HTMLDivElement} pBodyContainer
202
+ */
203
+ _renderPanelContent(pPanelData, pBodyContainer)
204
+ {
205
+ let tmpNodeData = this._FlowView.getNode(pPanelData.NodeHash);
206
+ if (!tmpNodeData) return;
207
+
208
+ let tmpNodeTypeConfig = this._FlowView._NodeTypeProvider.getNodeType(tmpNodeData.Type);
209
+ if (!tmpNodeTypeConfig) return;
210
+
211
+ // If no PropertiesPanel is configured, render the auto-generated info panel
212
+ if (!tmpNodeTypeConfig.PropertiesPanel)
213
+ {
214
+ this._renderInfoPanelContent(pBodyContainer, tmpNodeData, tmpNodeTypeConfig);
215
+ return;
216
+ }
217
+
218
+ let tmpPanelConfig = tmpNodeTypeConfig.PropertiesPanel;
219
+ let tmpPanelType = tmpPanelConfig.PanelType || 'Base';
220
+
221
+ // Try to get a registered panel type service
222
+ let tmpServiceName = `PictFlowCardPropertiesPanel-${tmpPanelType}`;
223
+ let tmpInstance = null;
224
+
225
+ if (this._PanelInstances[pPanelData.Hash])
226
+ {
227
+ // Reuse existing instance
228
+ tmpInstance = this._PanelInstances[pPanelData.Hash];
229
+ }
230
+ else
231
+ {
232
+ // Create a new instance
233
+ if (this.fable.servicesMap.hasOwnProperty(tmpServiceName))
234
+ {
235
+ tmpInstance = this.fable.instantiateServiceProviderWithoutRegistration(tmpServiceName, tmpPanelConfig);
236
+ }
237
+ else if (this.fable.servicesMap.hasOwnProperty('PictFlowCardPropertiesPanel'))
238
+ {
239
+ // Fall back to base class
240
+ tmpInstance = this.fable.instantiateServiceProviderWithoutRegistration('PictFlowCardPropertiesPanel', tmpPanelConfig);
241
+ }
242
+
243
+ if (tmpInstance)
244
+ {
245
+ tmpInstance._FlowView = this._FlowView;
246
+ this._PanelInstances[pPanelData.Hash] = tmpInstance;
247
+ }
248
+ }
249
+
250
+ if (tmpInstance)
251
+ {
252
+ tmpInstance.render(pBodyContainer, tmpNodeData);
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Render an auto-generated info panel for nodes without a configured PropertiesPanel.
258
+ * Shows the node type, description, and a summary of input/output ports with
259
+ * their connection constraints.
260
+ *
261
+ * Uses configuration-based templates from _DefaultConfiguration.Templates
262
+ * rendered via pict.parseTemplateByHash().
263
+ *
264
+ * @param {HTMLDivElement} pContainer
265
+ * @param {Object} pNodeData
266
+ * @param {Object} pNodeTypeConfig
267
+ */
268
+ _renderInfoPanelContent(pContainer, pNodeData, pNodeTypeConfig)
269
+ {
270
+ let tmpMeta = pNodeTypeConfig.CardMetadata || {};
271
+ let tmpPorts = pNodeTypeConfig.DefaultPorts || [];
272
+
273
+ let tmpInputs = tmpPorts.filter((pPort) => pPort.Direction === 'input');
274
+ let tmpOutputs = tmpPorts.filter((pPort) => pPort.Direction === 'output');
275
+
276
+ let tmpLabel = pNodeTypeConfig.Label || pNodeData.Type;
277
+
278
+ // Build content by rendering configuration-based templates
279
+ let tmpContentParts = [];
280
+
281
+ // Header
282
+ if (tmpMeta.Icon)
283
+ {
284
+ tmpContentParts.push(this.pict.parseTemplateByHash('Flow-InfoPanel-Header-Icon', { Icon: tmpMeta.Icon, Label: tmpLabel }));
285
+ }
286
+ else
287
+ {
288
+ tmpContentParts.push(this.pict.parseTemplateByHash('Flow-InfoPanel-Header', { Label: tmpLabel }));
289
+ }
290
+
291
+ // Description
292
+ if (tmpMeta.Description)
293
+ {
294
+ tmpContentParts.push(this.pict.parseTemplateByHash('Flow-InfoPanel-Description', { Description: tmpMeta.Description }));
295
+ }
296
+
297
+ // Category + Code badges
298
+ if (tmpMeta.Category || tmpMeta.Code)
299
+ {
300
+ let tmpBadgesContent = '';
301
+ if (tmpMeta.Category)
302
+ {
303
+ tmpBadgesContent += this.pict.parseTemplateByHash('Flow-InfoPanel-Badge-Category', { Category: tmpMeta.Category });
304
+ }
305
+ if (tmpMeta.Code)
306
+ {
307
+ tmpBadgesContent += this.pict.parseTemplateByHash('Flow-InfoPanel-Badge-Code', { Code: tmpMeta.Code });
308
+ }
309
+ tmpContentParts.push(this.pict.parseTemplateByHash('Flow-InfoPanel-Badges', { BadgesContent: tmpBadgesContent }));
310
+ }
311
+
312
+ // Inputs
313
+ if (tmpInputs.length > 0)
314
+ {
315
+ let tmpPortsContent = '';
316
+ for (let i = 0; i < tmpInputs.length; i++)
317
+ {
318
+ let tmpPort = tmpInputs[i];
319
+ let tmpConstraint = this._getPortConstraintHTML(tmpPort);
320
+ tmpPortsContent += this.pict.parseTemplateByHash('Flow-InfoPanel-Port-Input', { Label: tmpPort.Label || 'In', Constraint: tmpConstraint });
321
+ }
322
+ tmpContentParts.push(this.pict.parseTemplateByHash('Flow-InfoPanel-Section-Inputs', { PortsContent: tmpPortsContent }));
323
+ }
324
+
325
+ // Outputs
326
+ if (tmpOutputs.length > 0)
327
+ {
328
+ let tmpPortsContent = '';
329
+ for (let i = 0; i < tmpOutputs.length; i++)
330
+ {
331
+ let tmpPort = tmpOutputs[i];
332
+ tmpPortsContent += this.pict.parseTemplateByHash('Flow-InfoPanel-Port-Output', { Label: tmpPort.Label || 'Out' });
333
+ }
334
+ tmpContentParts.push(this.pict.parseTemplateByHash('Flow-InfoPanel-Section-Outputs', { PortsContent: tmpPortsContent }));
335
+ }
336
+
337
+ pContainer.innerHTML = this.pict.parseTemplateByHash('Flow-InfoPanel-Wrapper', { PanelContent: tmpContentParts.join('') });
338
+ }
339
+
340
+ /**
341
+ * Build the constraint markup for a port using configuration templates.
342
+ *
343
+ * @param {Object} pPort
344
+ * @returns {string} Rendered constraint HTML or empty string
345
+ */
346
+ _getPortConstraintHTML(pPort)
347
+ {
348
+ let tmpMin = (typeof pPort.MinimumInputCount === 'number') ? pPort.MinimumInputCount : 0;
349
+ let tmpMax = (typeof pPort.MaximumInputCount === 'number') ? pPort.MaximumInputCount : -1;
350
+
351
+ if (tmpMin > 0 || tmpMax > 0)
352
+ {
353
+ let tmpConstraintText = '';
354
+ if (tmpMax < 0)
355
+ {
356
+ tmpConstraintText = `(min ${tmpMin})`;
357
+ }
358
+ else if (tmpMin === tmpMax)
359
+ {
360
+ tmpConstraintText = `(exactly ${tmpMin})`;
361
+ }
362
+ else
363
+ {
364
+ tmpConstraintText = `(${tmpMin}\u2013${tmpMax})`;
365
+ }
366
+ return this.pict.parseTemplateByHash('Flow-InfoPanel-Port-Constraint', { ConstraintText: tmpConstraintText });
367
+ }
368
+ return '';
369
+ }
370
+
371
+ /**
372
+ * Render a tether from a panel to its node.
373
+ * Delegates to the TetherService for geometry, path generation, and SVG element creation.
374
+ *
375
+ * @param {Object} pPanelData
376
+ * @param {SVGGElement} pTethersLayer
377
+ * @param {boolean} pIsSelected
378
+ */
379
+ _renderTether(pPanelData, pTethersLayer, pIsSelected)
380
+ {
381
+ let tmpTetherService = this._FlowView._TetherService;
382
+ if (!tmpTetherService) return;
383
+
384
+ let tmpNodeData = this._FlowView.getNode(pPanelData.NodeHash);
385
+ if (!tmpNodeData) return;
386
+
387
+ let tmpViewIdentifier = this._FlowView.options.ViewIdentifier;
388
+ tmpTetherService.renderTether(pPanelData, tmpNodeData, pTethersLayer, pIsSelected, tmpViewIdentifier);
389
+ }
390
+
391
+ /**
392
+ * Marshal data from all open panels back into their node Data objects.
393
+ */
394
+ marshalAllFromPanels()
395
+ {
396
+ for (let tmpPanelHash in this._PanelInstances)
397
+ {
398
+ let tmpInstance = this._PanelInstances[tmpPanelHash];
399
+ if (tmpInstance && tmpInstance._NodeData)
400
+ {
401
+ tmpInstance.marshalFromPanel(tmpInstance._NodeData);
402
+ }
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Destroy a specific panel instance and clean up.
408
+ *
409
+ * @param {string} pPanelHash
410
+ */
411
+ destroyPanel(pPanelHash)
412
+ {
413
+ if (this._PanelInstances[pPanelHash])
414
+ {
415
+ this._PanelInstances[pPanelHash].destroy();
416
+ delete this._PanelInstances[pPanelHash];
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Destroy all panel instances.
422
+ */
423
+ destroyAllPanels()
424
+ {
425
+ for (let tmpPanelHash in this._PanelInstances)
426
+ {
427
+ this._PanelInstances[tmpPanelHash].destroy();
428
+ }
429
+ this._PanelInstances = {};
430
+ }
431
+ }
432
+
433
+ module.exports = PictViewFlowPropertiesPanel;
434
+
435
+ module.exports.default_configuration = _DefaultConfiguration;