pict-section-flow 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -13
- package/docs/Architecture.md +8 -148
- package/docs/Data_Model.md +2 -11
- package/docs/README.md +8 -38
- package/docs/Theme_Integration.md +11 -11
- package/docs/_cover.md +7 -1
- package/docs/_playground.json +24 -0
- package/docs/_sidebar.md +4 -0
- package/docs/_topbar.md +1 -1
- package/docs/_version.json +3 -3
- package/docs/card-help/FREAD.md +1 -1
- package/docs/diagrams/architecture-at-a-glance.excalidraw +4270 -0
- package/docs/diagrams/architecture-at-a-glance.mmd +30 -0
- package/docs/diagrams/architecture-at-a-glance.svg +2 -0
- package/docs/diagrams/data-flow.excalidraw +1451 -0
- package/docs/diagrams/data-flow.mmd +17 -0
- package/docs/diagrams/data-flow.svg +2 -0
- package/docs/diagrams/high-level-design.excalidraw +5767 -0
- package/docs/diagrams/high-level-design.mmd +86 -0
- package/docs/diagrams/high-level-design.svg +2 -0
- package/docs/diagrams/relationships.excalidraw +3852 -0
- package/docs/diagrams/relationships.mmd +9 -0
- package/docs/diagrams/relationships.svg +2 -0
- package/docs/diagrams/service-initialization-sequence.excalidraw +1466 -0
- package/docs/diagrams/service-initialization-sequence.mmd +19 -0
- package/docs/diagrams/service-initialization-sequence.svg +2 -0
- package/docs/diagrams/svg-layer-structure.excalidraw +1060 -0
- package/docs/diagrams/svg-layer-structure.mmd +18 -0
- package/docs/diagrams/svg-layer-structure.svg +2 -0
- package/docs/examples/README.md +9 -0
- package/docs/examples/simple_cards/README.md +677 -0
- package/docs/examples/simple_cards/css/flowexample.css +65 -0
- package/docs/examples/simple_cards/index.html +32 -0
- package/docs/examples/simple_cards/js/pict.min.js +12 -0
- package/docs/examples/simple_cards/pict-section-flow-example-simple-cards.compatible.min.js +1 -0
- package/docs/index.html +6 -5
- package/docs/playground/app.json +6 -0
- package/docs/playground/appdata.json +85 -0
- package/docs/playground/application.js +23 -0
- package/docs/playground/pict.json +17 -0
- package/docs/playground/runtime/pict-application.min.js +2 -0
- package/docs/playground/runtime/pict-section-flow.min.js +2 -0
- package/docs/playground/runtime/pict-section-modal.min.js +2 -0
- package/docs/playground/runtime/pict.min.js +12 -0
- package/docs/retold-catalog.json +241 -166
- package/docs/retold-keyword-index.json +19312 -7226
- package/example_applications/simple_cards/package.json +9 -1
- package/example_applications/simple_cards/source/views/PictView-FlowExample-BottomBar.js +2 -2
- package/package.json +5 -5
- package/source/providers/PictProvider-Flow-PanelChrome.js +2 -1
- package/source/services/PictService-Flow-DataManager.js +6 -0
- package/source/services/PictService-Flow-InteractionManager.js +10 -1
- package/source/services/PictService-Flow-PanelManager.js +106 -2
- package/source/views/PictView-Flow-PropertiesPanel.js +70 -3
- package/source/views/PictView-Flow.js +53 -0
- package/test/PanelManager_tests.js +172 -0
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
"version": "0.0.1",
|
|
4
4
|
"description": "Pict Section Flow - Simple Cards Example Application",
|
|
5
5
|
"main": "source/Pict-Application-FlowExample.js",
|
|
6
|
+
"retold": {
|
|
7
|
+
"ExampleApplication": {
|
|
8
|
+
"Stage": true,
|
|
9
|
+
"Title": "Simple Cards",
|
|
10
|
+
"Summary": "Twelve custom PictFlowCard subclasses across six categories — every panel type (Markdown / Template / Form / View), every BodyContent renderer (SVG / HTML / Canvas), wrapped in a multi-page Pict shell with router-driven navigation and a curated sample-graph catalog.",
|
|
11
|
+
"Complexity": "Basic"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
6
14
|
"scripts": {
|
|
7
15
|
"start": "node source/Pict-Application-FlowExample.js",
|
|
8
16
|
"prebuild": "node ../../scripts/generate-card-help.js",
|
|
@@ -19,7 +27,7 @@
|
|
|
19
27
|
"pict-provider": "^1.0.3",
|
|
20
28
|
"pict-section-content": "^0.0.6",
|
|
21
29
|
"pict-section-form": "^1.0.192",
|
|
22
|
-
"pict-section-flow": "
|
|
30
|
+
"pict-section-flow": "^1.0.1"
|
|
23
31
|
},
|
|
24
32
|
"devDependencies": {
|
|
25
33
|
"quackage": "^1.0.58"
|
|
@@ -44,8 +44,8 @@ const _ViewConfiguration =
|
|
|
44
44
|
<div class="flowexample-bottombar">
|
|
45
45
|
<span>Pict Section Flow Example © 2025</span>
|
|
46
46
|
<div class="flowexample-bottombar-links">
|
|
47
|
-
<a href="https://github.com/
|
|
48
|
-
<a href="https://github.com/
|
|
47
|
+
<a href="https://github.com/fable-retold/pict" target="_blank">Pict</a>
|
|
48
|
+
<a href="https://github.com/fable-retold/fable" target="_blank">Fable</a>
|
|
49
49
|
<a onclick="{~P~}.PictApplication.navigateTo('/About')">About</a>
|
|
50
50
|
</div>
|
|
51
51
|
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pict-section-flow",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Pict Section Flow Diagram",
|
|
5
5
|
"main": "source/Pict-Section-Flow.js",
|
|
6
6
|
"scripts": {
|
|
@@ -15,15 +15,15 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"fable-serviceproviderbase": "^3.0.19",
|
|
17
17
|
"pict-provider": "^1.0.13",
|
|
18
|
-
"pict-section-form": "^1.0.
|
|
18
|
+
"pict-section-form": "^1.0.199",
|
|
19
19
|
"pict-view": "^1.0.68"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"chai": "^6.2.2",
|
|
23
23
|
"mocha": "^11.7.5",
|
|
24
|
-
"pict": "^1.0.
|
|
25
|
-
"pict-docuserve": "^1.
|
|
24
|
+
"pict": "^1.0.372",
|
|
25
|
+
"pict-docuserve": "^1.4.4",
|
|
26
26
|
"pict-router": "^1.0.10",
|
|
27
|
-
"quackage": "^1.
|
|
27
|
+
"quackage": "^1.3.0"
|
|
28
28
|
}
|
|
29
29
|
}
|
|
@@ -40,7 +40,8 @@ class PictProviderFlowPanelChrome extends libFableServiceProviderBase
|
|
|
40
40
|
let tmpFO = tmpSVGHelper.createSVGElement('foreignObject');
|
|
41
41
|
tmpFO.setAttribute('class', 'pict-flow-panel-foreign-object');
|
|
42
42
|
tmpFO.setAttribute('data-panel-hash', pPanelData.Hash);
|
|
43
|
-
tmpFO.setAttribute('data-node-hash', pPanelData.NodeHash);
|
|
43
|
+
tmpFO.setAttribute('data-node-hash', pPanelData.NodeHash || '');
|
|
44
|
+
if (pPanelData.ConnectionHash) { tmpFO.setAttribute('data-connection-hash', pPanelData.ConnectionHash); }
|
|
44
45
|
tmpFO.setAttribute('x', String(pPanelData.X));
|
|
45
46
|
tmpFO.setAttribute('y', String(pPanelData.Y));
|
|
46
47
|
tmpFO.setAttribute('width', String(pPanelData.Width));
|
|
@@ -328,6 +328,12 @@ class PictServiceFlowDataManager extends libFableServiceProviderBase
|
|
|
328
328
|
|
|
329
329
|
let tmpRemovedConnection = this._FlowView._FlowData.Connections.splice(tmpConnectionIndex, 1)[0];
|
|
330
330
|
|
|
331
|
+
// Close any properties panel open for this connection.
|
|
332
|
+
if (typeof this._FlowView.closePanelForConnection === 'function')
|
|
333
|
+
{
|
|
334
|
+
this._FlowView.closePanelForConnection(pConnectionHash);
|
|
335
|
+
}
|
|
336
|
+
|
|
331
337
|
if (this._FlowView._FlowData.ViewState.SelectedConnectionHash === pConnectionHash)
|
|
332
338
|
{
|
|
333
339
|
this._FlowView._FlowData.ViewState.SelectedConnectionHash = null;
|
|
@@ -322,7 +322,16 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
|
|
|
322
322
|
{
|
|
323
323
|
this._LastConnectionClickTime = 0;
|
|
324
324
|
this._LastConnectionClickHash = null;
|
|
325
|
-
|
|
325
|
+
// When the host configured a connection properties panel, double-click opens it;
|
|
326
|
+
// otherwise keep the default behavior of adding a bezier handle.
|
|
327
|
+
if (this._FlowView.options.ConnectionPropertiesPanel && tmpConnectionHash)
|
|
328
|
+
{
|
|
329
|
+
this._FlowView.toggleConnectionPanel(tmpConnectionHash);
|
|
330
|
+
}
|
|
331
|
+
else
|
|
332
|
+
{
|
|
333
|
+
this._addBezierHandle(tmpTarget, pEvent);
|
|
334
|
+
}
|
|
326
335
|
}
|
|
327
336
|
else
|
|
328
337
|
{
|
|
@@ -152,6 +152,98 @@ class PictServiceFlowPanelManager extends libFableServiceProviderBase
|
|
|
152
152
|
return this.openPanel(pNodeHash);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Open a properties panel for a connection (edge). The panel config comes from the FlowView's
|
|
157
|
+
* ConnectionPropertiesPanel option (connections are not typed, so one config serves them all);
|
|
158
|
+
* the panel is placed near the connection's midpoint and tethers to it. Returns false when no
|
|
159
|
+
* ConnectionPropertiesPanel is configured, so a host that has not opted in keeps the default
|
|
160
|
+
* edge behavior (double-click adds a bezier handle).
|
|
161
|
+
* @param {string} pConnectionHash
|
|
162
|
+
* @returns {Object|false} The panel data, or false
|
|
163
|
+
*/
|
|
164
|
+
openConnectionPanel(pConnectionHash)
|
|
165
|
+
{
|
|
166
|
+
let tmpConnection = this._FlowView.getConnection(pConnectionHash);
|
|
167
|
+
if (!tmpConnection) return false;
|
|
168
|
+
|
|
169
|
+
let tmpPanelConfig = this._FlowView.options.ConnectionPropertiesPanel;
|
|
170
|
+
if (!tmpPanelConfig) return false;
|
|
171
|
+
|
|
172
|
+
let tmpExisting = this._FlowView._FlowData.OpenPanels.find((pPanel) => pPanel.ConnectionHash === pConnectionHash);
|
|
173
|
+
if (tmpExisting) return tmpExisting;
|
|
174
|
+
|
|
175
|
+
let tmpMidpoint = this._FlowView.getConnectionMidpoint(pConnectionHash) || { x: 0, y: 0 };
|
|
176
|
+
let tmpWidth = tmpPanelConfig.DefaultWidth || 300;
|
|
177
|
+
let tmpHeight = tmpPanelConfig.DefaultHeight || 200;
|
|
178
|
+
|
|
179
|
+
let tmpPanelData =
|
|
180
|
+
{
|
|
181
|
+
Hash: `panel-${this.fable.getUUID()}`,
|
|
182
|
+
ConnectionHash: pConnectionHash,
|
|
183
|
+
NodeHash: null,
|
|
184
|
+
PanelType: tmpPanelConfig.PanelType || 'Base',
|
|
185
|
+
Title: tmpPanelConfig.Title || 'Connection',
|
|
186
|
+
X: tmpMidpoint.x + 40,
|
|
187
|
+
Y: tmpMidpoint.y + 20,
|
|
188
|
+
Width: tmpWidth,
|
|
189
|
+
Height: tmpHeight
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
this._FlowView._FlowData.OpenPanels.push(tmpPanelData);
|
|
193
|
+
this._FlowView.renderFlow();
|
|
194
|
+
this._FlowView.marshalFromView();
|
|
195
|
+
|
|
196
|
+
if (this._FlowView._EventHandlerProvider)
|
|
197
|
+
{
|
|
198
|
+
this._FlowView._EventHandlerProvider.fireEvent('onPanelOpened', tmpPanelData);
|
|
199
|
+
this._FlowView._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowView._FlowData);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return tmpPanelData;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Toggle a properties panel for a connection (open if closed, close if open).
|
|
207
|
+
* @param {string} pConnectionHash
|
|
208
|
+
* @returns {Object|false}
|
|
209
|
+
*/
|
|
210
|
+
toggleConnectionPanel(pConnectionHash)
|
|
211
|
+
{
|
|
212
|
+
let tmpExisting = this._FlowView._FlowData.OpenPanels.find((pPanel) => pPanel.ConnectionHash === pConnectionHash);
|
|
213
|
+
if (tmpExisting)
|
|
214
|
+
{
|
|
215
|
+
this.closePanel(tmpExisting.Hash);
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
return this.openConnectionPanel(pConnectionHash);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Close all panels for a given connection.
|
|
223
|
+
* @param {string} pConnectionHash
|
|
224
|
+
* @returns {boolean}
|
|
225
|
+
*/
|
|
226
|
+
closePanelForConnection(pConnectionHash)
|
|
227
|
+
{
|
|
228
|
+
let tmpPanelsToClose = this._FlowView._FlowData.OpenPanels.filter((pPanel) => pPanel.ConnectionHash === pConnectionHash);
|
|
229
|
+
if (tmpPanelsToClose.length === 0) return false;
|
|
230
|
+
|
|
231
|
+
for (let i = 0; i < tmpPanelsToClose.length; i++)
|
|
232
|
+
{
|
|
233
|
+
let tmpIndex = this._FlowView._FlowData.OpenPanels.indexOf(tmpPanelsToClose[i]);
|
|
234
|
+
if (tmpIndex >= 0)
|
|
235
|
+
{
|
|
236
|
+
this._FlowView._FlowData.OpenPanels.splice(tmpIndex, 1);
|
|
237
|
+
}
|
|
238
|
+
if (this._FlowView._PropertiesPanelView)
|
|
239
|
+
{
|
|
240
|
+
this._FlowView._PropertiesPanelView.destroyPanel(tmpPanelsToClose[i].Hash);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
155
247
|
/**
|
|
156
248
|
* Update a panel's position (for drag).
|
|
157
249
|
* @param {string} pPanelHash
|
|
@@ -180,8 +272,20 @@ class PictServiceFlowPanelManager extends libFableServiceProviderBase
|
|
|
180
272
|
}
|
|
181
273
|
}
|
|
182
274
|
|
|
183
|
-
// Update the tether for this panel
|
|
184
|
-
|
|
275
|
+
// Update the tether for this panel. A node panel refreshes just its node's tethers; a
|
|
276
|
+
// connection panel has no node, so reconcile the panels layer (redraws all tethers, which
|
|
277
|
+
// is where the connection-midpoint tether is recomputed).
|
|
278
|
+
if (tmpPanel.ConnectionHash)
|
|
279
|
+
{
|
|
280
|
+
if (this._FlowView._PropertiesPanelView && this._FlowView._PanelsLayer && this._FlowView._TethersLayer)
|
|
281
|
+
{
|
|
282
|
+
this._FlowView._PropertiesPanelView.renderPanels(this._FlowView._FlowData.OpenPanels, this._FlowView._PanelsLayer, this._FlowView._TethersLayer, this._FlowView._FlowData.ViewState.SelectedTetherHash);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else
|
|
286
|
+
{
|
|
287
|
+
this._FlowView._renderTethersForNode(tmpPanel.NodeHash);
|
|
288
|
+
}
|
|
185
289
|
}
|
|
186
290
|
}
|
|
187
291
|
|
|
@@ -242,6 +242,13 @@ class PictViewFlowPropertiesPanel extends libPictView
|
|
|
242
242
|
*/
|
|
243
243
|
_renderPanelContent(pPanelData, pBodyContainer)
|
|
244
244
|
{
|
|
245
|
+
// Connection (edge) panels resolve their config and data differently from node panels.
|
|
246
|
+
if (pPanelData.ConnectionHash)
|
|
247
|
+
{
|
|
248
|
+
this._renderConnectionPanelContent(pPanelData, pBodyContainer);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
245
252
|
let tmpNodeData = this._FlowView.getNode(pPanelData.NodeHash);
|
|
246
253
|
if (!tmpNodeData) return;
|
|
247
254
|
|
|
@@ -298,6 +305,54 @@ class PictViewFlowPropertiesPanel extends libPictView
|
|
|
298
305
|
this._renderPortSummary(pBodyContainer, tmpNodeTypeConfig);
|
|
299
306
|
}
|
|
300
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Render the content of a connection (edge) panel. The config is the FlowView's single
|
|
310
|
+
* ConnectionPropertiesPanel (connections are not typed); the panel-type instance renders
|
|
311
|
+
* against the connection object, so a Form panel edits Connection.Data.* and a Template panel
|
|
312
|
+
* renders against the connection.
|
|
313
|
+
*
|
|
314
|
+
* @param {Object} pPanelData
|
|
315
|
+
* @param {HTMLDivElement} pBodyContainer
|
|
316
|
+
*/
|
|
317
|
+
_renderConnectionPanelContent(pPanelData, pBodyContainer)
|
|
318
|
+
{
|
|
319
|
+
let tmpConnectionData = this._FlowView.getConnection(pPanelData.ConnectionHash);
|
|
320
|
+
if (!tmpConnectionData) return;
|
|
321
|
+
|
|
322
|
+
let tmpPanelConfig = this._FlowView.options.ConnectionPropertiesPanel;
|
|
323
|
+
if (!tmpPanelConfig)
|
|
324
|
+
{
|
|
325
|
+
pBodyContainer.innerHTML = '<em>No connection properties panel configured.</em>';
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let tmpPanelType = tmpPanelConfig.PanelType || 'Base';
|
|
330
|
+
let tmpServiceName = `PictFlowCardPropertiesPanel-${tmpPanelType}`;
|
|
331
|
+
let tmpInstance = this._PanelInstances[pPanelData.Hash];
|
|
332
|
+
|
|
333
|
+
if (!tmpInstance)
|
|
334
|
+
{
|
|
335
|
+
if (this.fable.servicesMap.hasOwnProperty(tmpServiceName))
|
|
336
|
+
{
|
|
337
|
+
tmpInstance = this.fable.instantiateServiceProviderWithoutRegistration(tmpServiceName, tmpPanelConfig);
|
|
338
|
+
}
|
|
339
|
+
else if (this.fable.servicesMap.hasOwnProperty('PictFlowCardPropertiesPanel'))
|
|
340
|
+
{
|
|
341
|
+
tmpInstance = this.fable.instantiateServiceProviderWithoutRegistration('PictFlowCardPropertiesPanel', tmpPanelConfig);
|
|
342
|
+
}
|
|
343
|
+
if (tmpInstance)
|
|
344
|
+
{
|
|
345
|
+
tmpInstance._FlowView = this._FlowView;
|
|
346
|
+
this._PanelInstances[pPanelData.Hash] = tmpInstance;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (tmpInstance)
|
|
351
|
+
{
|
|
352
|
+
tmpInstance.render(pBodyContainer, tmpConnectionData);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
301
356
|
/**
|
|
302
357
|
* Render an auto-generated info panel for nodes without a configured PropertiesPanel.
|
|
303
358
|
* Shows the node type, description, and a summary of input/output ports with
|
|
@@ -716,11 +771,23 @@ class PictViewFlowPropertiesPanel extends libPictView
|
|
|
716
771
|
let tmpTetherService = this._FlowView._TetherService;
|
|
717
772
|
if (!tmpTetherService) return;
|
|
718
773
|
|
|
719
|
-
|
|
720
|
-
|
|
774
|
+
// A connection panel tethers to the edge midpoint; model it as a zero-size anchor at that
|
|
775
|
+
// point so the same tether geometry applies. A node panel tethers to its node.
|
|
776
|
+
let tmpAnchorData;
|
|
777
|
+
if (pPanelData.ConnectionHash)
|
|
778
|
+
{
|
|
779
|
+
let tmpMidpoint = this._FlowView.getConnectionMidpoint(pPanelData.ConnectionHash);
|
|
780
|
+
if (!tmpMidpoint) return;
|
|
781
|
+
tmpAnchorData = { X: tmpMidpoint.x, Y: tmpMidpoint.y, Width: 0, Height: 0 };
|
|
782
|
+
}
|
|
783
|
+
else
|
|
784
|
+
{
|
|
785
|
+
tmpAnchorData = this._FlowView.getNode(pPanelData.NodeHash);
|
|
786
|
+
if (!tmpAnchorData) return;
|
|
787
|
+
}
|
|
721
788
|
|
|
722
789
|
let tmpViewIdentifier = this._FlowView.options.ViewIdentifier;
|
|
723
|
-
tmpTetherService.renderTether(pPanelData,
|
|
790
|
+
tmpTetherService.renderTether(pPanelData, tmpAnchorData, pTethersLayer, pIsSelected, tmpViewIdentifier);
|
|
724
791
|
}
|
|
725
792
|
|
|
726
793
|
/**
|
|
@@ -71,6 +71,11 @@ const _DefaultConfiguration =
|
|
|
71
71
|
DefaultNodeWidth: 180,
|
|
72
72
|
DefaultNodeHeight: 80,
|
|
73
73
|
|
|
74
|
+
// Properties panel for connections (edges). Connections are not typed, so one config serves
|
|
75
|
+
// them all: { PanelType, DefaultWidth, DefaultHeight, Title, Configuration }. When set, a
|
|
76
|
+
// double-click on a connection opens this panel; when false, double-click adds a bezier handle.
|
|
77
|
+
ConnectionPropertiesPanel: false,
|
|
78
|
+
|
|
74
79
|
// Layout-algorithm subsystem defaults
|
|
75
80
|
DefaultLayoutAlgorithm: 'Custom',
|
|
76
81
|
DefaultLayoutParameters: {},
|
|
@@ -1328,6 +1333,54 @@ class PictViewFlow extends libPictView
|
|
|
1328
1333
|
return this._PanelManager.togglePanel(pNodeHash);
|
|
1329
1334
|
}
|
|
1330
1335
|
|
|
1336
|
+
/**
|
|
1337
|
+
* Open a properties panel for a connection (edge). Requires the ConnectionPropertiesPanel
|
|
1338
|
+
* option; returns false otherwise.
|
|
1339
|
+
* @param {string} pConnectionHash
|
|
1340
|
+
* @returns {Object|false}
|
|
1341
|
+
*/
|
|
1342
|
+
openConnectionPanel(pConnectionHash)
|
|
1343
|
+
{
|
|
1344
|
+
return this._PanelManager.openConnectionPanel(pConnectionHash);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
/**
|
|
1348
|
+
* Toggle a properties panel for a connection.
|
|
1349
|
+
* @param {string} pConnectionHash
|
|
1350
|
+
* @returns {Object|false}
|
|
1351
|
+
*/
|
|
1352
|
+
toggleConnectionPanel(pConnectionHash)
|
|
1353
|
+
{
|
|
1354
|
+
return this._PanelManager.toggleConnectionPanel(pConnectionHash);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* Close all panels for a given connection.
|
|
1359
|
+
* @param {string} pConnectionHash
|
|
1360
|
+
* @returns {boolean}
|
|
1361
|
+
*/
|
|
1362
|
+
closePanelForConnection(pConnectionHash)
|
|
1363
|
+
{
|
|
1364
|
+
return this._PanelManager.closePanelForConnection(pConnectionHash);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* The midpoint of a connection in SVG coordinates, averaged from its two endpoint ports. Used
|
|
1369
|
+
* to place and tether a connection's properties panel. Returns null if the connection or
|
|
1370
|
+
* either port can not be resolved.
|
|
1371
|
+
* @param {string} pConnectionHash
|
|
1372
|
+
* @returns {{x: number, y: number}|null}
|
|
1373
|
+
*/
|
|
1374
|
+
getConnectionMidpoint(pConnectionHash)
|
|
1375
|
+
{
|
|
1376
|
+
let tmpConnection = this.getConnection(pConnectionHash);
|
|
1377
|
+
if (!tmpConnection) return null;
|
|
1378
|
+
let tmpSource = this.getPortPosition(tmpConnection.SourceNodeHash, tmpConnection.SourcePortHash);
|
|
1379
|
+
let tmpTarget = this.getPortPosition(tmpConnection.TargetNodeHash, tmpConnection.TargetPortHash);
|
|
1380
|
+
if (!tmpSource || !tmpTarget) return null;
|
|
1381
|
+
return { x: (tmpSource.x + tmpTarget.x) / 2, y: (tmpSource.y + tmpTarget.y) / 2 };
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1331
1384
|
/**
|
|
1332
1385
|
* Update a panel's position (for drag).
|
|
1333
1386
|
* @param {string} pPanelHash
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
const libFable = require('fable');
|
|
2
|
+
const libChai = require('chai');
|
|
3
|
+
const libExpect = libChai.expect;
|
|
4
|
+
|
|
5
|
+
const libPanelManager = require('../source/services/PictService-Flow-PanelManager.js');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Connection (edge) properties panels. The node-panel path is well covered through the view; these
|
|
9
|
+
* focus on the connection additions: gating on ConnectionPropertiesPanel, placement near the edge
|
|
10
|
+
* midpoint, the open/toggle/close lifecycle, and that node panels are not disturbed.
|
|
11
|
+
*/
|
|
12
|
+
suite
|
|
13
|
+
(
|
|
14
|
+
'PictService-Flow-PanelManager (connection panels)',
|
|
15
|
+
function ()
|
|
16
|
+
{
|
|
17
|
+
let _Fable;
|
|
18
|
+
let _PanelManager;
|
|
19
|
+
let _MockFlowView;
|
|
20
|
+
|
|
21
|
+
setup
|
|
22
|
+
(
|
|
23
|
+
function ()
|
|
24
|
+
{
|
|
25
|
+
_Fable = new libFable({});
|
|
26
|
+
|
|
27
|
+
_MockFlowView =
|
|
28
|
+
{
|
|
29
|
+
fable: _Fable,
|
|
30
|
+
log: _Fable.log,
|
|
31
|
+
options:
|
|
32
|
+
{
|
|
33
|
+
ViewIdentifier: 'Test-Flow',
|
|
34
|
+
ConnectionPropertiesPanel: false
|
|
35
|
+
},
|
|
36
|
+
_FlowData:
|
|
37
|
+
{
|
|
38
|
+
Nodes:
|
|
39
|
+
[
|
|
40
|
+
{ Hash: 'n1', Type: 'state', X: 0, Y: 0, Width: 100, Height: 60, Ports: [ { Hash: 'n1-out', Direction: 'output' } ] },
|
|
41
|
+
{ Hash: 'n2', Type: 'state', X: 300, Y: 0, Width: 100, Height: 60, Ports: [ { Hash: 'n2-in', Direction: 'input' } ] }
|
|
42
|
+
],
|
|
43
|
+
Connections:
|
|
44
|
+
[
|
|
45
|
+
{ Hash: 'c1', SourceNodeHash: 'n1', SourcePortHash: 'n1-out', TargetNodeHash: 'n2', TargetPortHash: 'n2-in', Data: {} }
|
|
46
|
+
],
|
|
47
|
+
OpenPanels: [],
|
|
48
|
+
ViewState: { SelectedTetherHash: null }
|
|
49
|
+
},
|
|
50
|
+
getConnection: function (pHash) { return this._FlowData.Connections.find((pConn) => pConn.Hash === pHash) || null; },
|
|
51
|
+
getNode: function (pHash) { return this._FlowData.Nodes.find((pNode) => pNode.Hash === pHash) || null; },
|
|
52
|
+
getConnectionMidpoint: function (pHash) { return this.getConnection(pHash) ? { x: 200, y: 30 } : null; },
|
|
53
|
+
_NodeTypeProvider:
|
|
54
|
+
{
|
|
55
|
+
getNodeType: function () { return { Label: 'State', PropertiesPanel: { PanelType: 'Form', DefaultWidth: 300, DefaultHeight: 220, Title: 'State' } }; }
|
|
56
|
+
},
|
|
57
|
+
renderFlow: function () {},
|
|
58
|
+
marshalFromView: function () {},
|
|
59
|
+
_PropertiesPanelView: { destroyPanel: function () {} },
|
|
60
|
+
_EventHandlerProvider: { fireEvent: function () {} }
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
_PanelManager = new libPanelManager(_Fable, { FlowView: _MockFlowView }, 'PM-Test');
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
test
|
|
68
|
+
(
|
|
69
|
+
'openConnectionPanel returns false when no ConnectionPropertiesPanel is configured',
|
|
70
|
+
function ()
|
|
71
|
+
{
|
|
72
|
+
let tmpResult = _PanelManager.openConnectionPanel('c1');
|
|
73
|
+
libExpect(tmpResult).to.equal(false);
|
|
74
|
+
libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(0);
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
test
|
|
79
|
+
(
|
|
80
|
+
'openConnectionPanel returns false for an unknown connection',
|
|
81
|
+
function ()
|
|
82
|
+
{
|
|
83
|
+
_MockFlowView.options.ConnectionPropertiesPanel = { PanelType: 'Form' };
|
|
84
|
+
let tmpResult = _PanelManager.openConnectionPanel('no-such-connection');
|
|
85
|
+
libExpect(tmpResult).to.equal(false);
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
test
|
|
90
|
+
(
|
|
91
|
+
'openConnectionPanel opens a panel carrying the ConnectionHash, placed near the midpoint',
|
|
92
|
+
function ()
|
|
93
|
+
{
|
|
94
|
+
_MockFlowView.options.ConnectionPropertiesPanel = { PanelType: 'Form', DefaultWidth: 320, DefaultHeight: 240, Title: 'Transition' };
|
|
95
|
+
let tmpPanel = _PanelManager.openConnectionPanel('c1');
|
|
96
|
+
|
|
97
|
+
libExpect(tmpPanel).to.be.an('object');
|
|
98
|
+
libExpect(tmpPanel.ConnectionHash).to.equal('c1');
|
|
99
|
+
libExpect(tmpPanel.NodeHash).to.equal(null);
|
|
100
|
+
libExpect(tmpPanel.Title).to.equal('Transition');
|
|
101
|
+
libExpect(tmpPanel.Width).to.equal(320);
|
|
102
|
+
libExpect(tmpPanel.Height).to.equal(240);
|
|
103
|
+
// Midpoint is (200, 30); the panel is offset from it.
|
|
104
|
+
libExpect(tmpPanel.X).to.equal(240);
|
|
105
|
+
libExpect(tmpPanel.Y).to.equal(50);
|
|
106
|
+
libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(1);
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
test
|
|
111
|
+
(
|
|
112
|
+
'openConnectionPanel is idempotent: a second open returns the same panel',
|
|
113
|
+
function ()
|
|
114
|
+
{
|
|
115
|
+
_MockFlowView.options.ConnectionPropertiesPanel = { PanelType: 'Form' };
|
|
116
|
+
let tmpFirst = _PanelManager.openConnectionPanel('c1');
|
|
117
|
+
let tmpSecond = _PanelManager.openConnectionPanel('c1');
|
|
118
|
+
libExpect(tmpSecond.Hash).to.equal(tmpFirst.Hash);
|
|
119
|
+
libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(1);
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
test
|
|
124
|
+
(
|
|
125
|
+
'toggleConnectionPanel opens then closes',
|
|
126
|
+
function ()
|
|
127
|
+
{
|
|
128
|
+
_MockFlowView.options.ConnectionPropertiesPanel = { PanelType: 'Form' };
|
|
129
|
+
let tmpOpened = _PanelManager.toggleConnectionPanel('c1');
|
|
130
|
+
libExpect(tmpOpened).to.be.an('object');
|
|
131
|
+
libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(1);
|
|
132
|
+
|
|
133
|
+
let tmpClosed = _PanelManager.toggleConnectionPanel('c1');
|
|
134
|
+
libExpect(tmpClosed).to.equal(false);
|
|
135
|
+
libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(0);
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
test
|
|
140
|
+
(
|
|
141
|
+
'closePanelForConnection removes the connection panel',
|
|
142
|
+
function ()
|
|
143
|
+
{
|
|
144
|
+
_MockFlowView.options.ConnectionPropertiesPanel = { PanelType: 'Form' };
|
|
145
|
+
_PanelManager.openConnectionPanel('c1');
|
|
146
|
+
let tmpRemoved = _PanelManager.closePanelForConnection('c1');
|
|
147
|
+
libExpect(tmpRemoved).to.equal(true);
|
|
148
|
+
libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(0);
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
test
|
|
153
|
+
(
|
|
154
|
+
'node panels still open alongside connection panels, keyed separately',
|
|
155
|
+
function ()
|
|
156
|
+
{
|
|
157
|
+
_MockFlowView.options.ConnectionPropertiesPanel = { PanelType: 'Form' };
|
|
158
|
+
let tmpNodePanel = _PanelManager.openPanel('n1');
|
|
159
|
+
let tmpConnPanel = _PanelManager.openConnectionPanel('c1');
|
|
160
|
+
|
|
161
|
+
libExpect(tmpNodePanel.NodeHash).to.equal('n1');
|
|
162
|
+
libExpect(tmpConnPanel.ConnectionHash).to.equal('c1');
|
|
163
|
+
libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(2);
|
|
164
|
+
|
|
165
|
+
// Closing the connection panel leaves the node panel intact.
|
|
166
|
+
_PanelManager.closePanelForConnection('c1');
|
|
167
|
+
libExpect(_MockFlowView._FlowData.OpenPanels.length).to.equal(1);
|
|
168
|
+
libExpect(_MockFlowView._FlowData.OpenPanels[0].NodeHash).to.equal('n1');
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
);
|