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.
- package/docs/README.md +19 -0
- package/{example_application → example_applications/simple_cards}/html/index.html +2 -2
- package/example_applications/simple_cards/package.json +43 -0
- package/example_applications/simple_cards/source/Pict-Application-FlowExample.js +434 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Each.js +36 -0
- package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +54 -0
- package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +48 -0
- package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +35 -0
- package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +47 -0
- package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +53 -0
- package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +95 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +37 -0
- package/example_applications/simple_cards/source/views/PictView-FlowExample-FileWriteInfo.js +59 -0
- package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-Layout.js +5 -1
- package/example_applications/simple_cards/source/views/PictView-FlowExample-MainWorkspace.js +312 -0
- package/package.json +6 -6
- package/source/Pict-Section-Flow.js +19 -0
- package/source/PictFlowCard.js +207 -0
- package/source/PictFlowCardPropertiesPanel.js +105 -0
- package/source/panels/FlowCardPropertiesPanel-Form.js +174 -0
- package/source/panels/FlowCardPropertiesPanel-Markdown.js +148 -0
- package/source/panels/FlowCardPropertiesPanel-Template.js +88 -0
- package/source/panels/FlowCardPropertiesPanel-View.js +114 -0
- package/source/providers/PictProvider-Flow-EventHandler.js +19 -8
- package/source/providers/PictProvider-Flow-Geometry.js +64 -0
- package/source/providers/PictProvider-Flow-Layouts.js +284 -0
- package/source/providers/PictProvider-Flow-NodeTypes.js +70 -0
- package/source/providers/PictProvider-Flow-PanelChrome.js +72 -0
- package/source/providers/PictProvider-Flow-SVGHelpers.js +30 -0
- package/source/services/PictService-Flow-ConnectionRenderer.js +324 -66
- package/source/services/PictService-Flow-InteractionManager.js +399 -75
- package/source/services/PictService-Flow-Layout.js +159 -0
- package/source/services/PictService-Flow-PathGenerator.js +199 -0
- package/source/services/PictService-Flow-Tether.js +544 -0
- package/source/views/PictView-Flow-Node.js +95 -18
- package/source/views/PictView-Flow-PropertiesPanel.js +435 -0
- package/source/views/PictView-Flow-Toolbar.js +491 -5
- package/source/views/PictView-Flow.js +830 -8
- package/example_application/package.json +0 -41
- package/example_application/source/Pict-Application-FlowExample.js +0 -241
- package/example_application/source/views/PictView-FlowExample-MainWorkspace.js +0 -191
- /package/{example_application → example_applications/simple_cards}/css/flowexample.css +0 -0
- /package/{example_application → example_applications/simple_cards}/source/Pict-Application-FlowExample-Configuration.json +0 -0
- /package/{example_application → example_applications/simple_cards}/source/providers/PictRouter-FlowExample-Configuration.json +0 -0
- /package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-About.js +0 -0
- /package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-BottomBar.js +0 -0
- /package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-Documentation.js +0 -0
- /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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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;
|