pict-section-flow 1.3.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.
- package/package.json +1 -1
- package/source/providers/PictProvider-Flow-CSS.js +26 -0
- package/source/providers/PictProvider-Flow-ConnectorShapes.js +8 -0
- package/source/providers/PictProvider-Flow-Icons.js +8 -0
- package/source/services/PictService-Flow-ConnectionRenderer.js +76 -4
- package/source/services/PictService-Flow-InteractionManager.js +4 -2
- package/source/views/PictView-Flow-FloatingToolbar.js +53 -0
- package/source/views/PictView-Flow-Node.js +7 -0
- package/source/views/PictView-Flow-PropertiesPanel.js +27 -5
- package/source/views/PictView-Flow-Toolbar.js +99 -11
- package/source/views/PictView-Flow.js +37 -1
- package/test/CardPalette_tests.js +43 -0
- package/test/ConnectionStyle_tests.js +90 -0
- package/test/ToolbarExtraButtons_tests.js +138 -0
- package/test/UndirectedConnections_tests.js +70 -0
package/package.json
CHANGED
|
@@ -768,6 +768,19 @@ class PictProviderFlowCSS extends libFableServiceProviderBase
|
|
|
768
768
|
cursor: pointer;
|
|
769
769
|
transition: stroke 0.15s;
|
|
770
770
|
}
|
|
771
|
+
/* A connection's label (Data.Label), drawn at the midpoint. The white halo (paint-order) keeps it
|
|
772
|
+
legible where it crosses the line. */
|
|
773
|
+
.pict-flow-connection-label {
|
|
774
|
+
font-size: 12px;
|
|
775
|
+
font-family: inherit;
|
|
776
|
+
fill: var(--pf-text-primary, #2c3e50);
|
|
777
|
+
paint-order: stroke;
|
|
778
|
+
stroke: var(--theme-color-background-primary, #ffffff);
|
|
779
|
+
stroke-width: 3px;
|
|
780
|
+
stroke-linejoin: round;
|
|
781
|
+
pointer-events: none;
|
|
782
|
+
user-select: none;
|
|
783
|
+
}
|
|
771
784
|
.pict-flow-connection:hover {
|
|
772
785
|
stroke: var(--pf-connection-stroke-hover);
|
|
773
786
|
stroke-width: 3;
|
|
@@ -1285,6 +1298,19 @@ class PictProviderFlowCSS extends libFableServiceProviderBase
|
|
|
1285
1298
|
border-right: none;
|
|
1286
1299
|
padding-right: 0;
|
|
1287
1300
|
}
|
|
1301
|
+
/* The host-supplied extra-button group (ToolbarExtraButtons) renders even
|
|
1302
|
+
when empty; collapse it so consumers that pass no buttons see no gap. */
|
|
1303
|
+
.pict-flow-toolbar-group:empty {
|
|
1304
|
+
display: none;
|
|
1305
|
+
}
|
|
1306
|
+
.pict-flow-toolbar-btn-active {
|
|
1307
|
+
background-color: var(--pf-button-active-bg);
|
|
1308
|
+
border-color: var(--pf-button-hover-border);
|
|
1309
|
+
}
|
|
1310
|
+
/* An icon-only host button (ToolbarExtraButtons with no Label) renders an empty text span. */
|
|
1311
|
+
.pict-flow-toolbar-btn-text:empty {
|
|
1312
|
+
display: none;
|
|
1313
|
+
}
|
|
1288
1314
|
.pict-flow-toolbar-btn {
|
|
1289
1315
|
display: inline-flex;
|
|
1290
1316
|
align-items: center;
|
|
@@ -465,6 +465,14 @@ class PictProviderFlowConnectorShapes extends libFableServiceProviderBase
|
|
|
465
465
|
+ '<polygon class="pict-flow-arrowhead pict-flow-arrowhead-tether" points="' + tmpTetherMarker.Points + '" fill="' + tmpTetherMarker.Fill + '" />'
|
|
466
466
|
+ '</marker>';
|
|
467
467
|
|
|
468
|
+
// Generic per-connection end markers, selectable per connection via Data.SourceMarker /
|
|
469
|
+
// Data.TargetMarker (a host like a moodboard styles its own edges). fill="context-stroke" makes
|
|
470
|
+
// each marker take the connection's own stroke color, so a recolored line recolors its markers.
|
|
471
|
+
tmpMarkup += '<marker id="flow-marker-arrow-end-' + pViewIdentifier + '" markerWidth="10" markerHeight="10" refX="8.5" refY="5" orient="auto" markerUnits="strokeWidth"><path d="M1,1 L9,5 L1,9 z" fill="context-stroke" /></marker>';
|
|
472
|
+
tmpMarkup += '<marker id="flow-marker-arrow-start-' + pViewIdentifier + '" markerWidth="10" markerHeight="10" refX="1.5" refY="5" orient="auto-start-reverse" markerUnits="strokeWidth"><path d="M1,1 L9,5 L1,9 z" fill="context-stroke" /></marker>';
|
|
473
|
+
tmpMarkup += '<marker id="flow-marker-dot-' + pViewIdentifier + '" markerWidth="8" markerHeight="8" refX="4" refY="4" orient="auto" markerUnits="strokeWidth"><circle cx="4" cy="4" r="3" fill="context-stroke" /></marker>';
|
|
474
|
+
tmpMarkup += '<marker id="flow-marker-square-' + pViewIdentifier + '" markerWidth="8" markerHeight="8" refX="4" refY="4" orient="auto" markerUnits="strokeWidth"><rect x="1" y="1" width="6" height="6" fill="context-stroke" /></marker>';
|
|
475
|
+
|
|
468
476
|
return tmpMarkup;
|
|
469
477
|
}
|
|
470
478
|
}
|
|
@@ -45,6 +45,14 @@ const _DefaultIcons =
|
|
|
45
45
|
|
|
46
46
|
// ── UI Icons ───────────────────────────────────────────────────────────
|
|
47
47
|
|
|
48
|
+
'edit': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke="var(--theme-color-text-primary, #2c3e50)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4z"/></svg>',
|
|
49
|
+
|
|
50
|
+
'check': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke="var(--theme-color-text-primary, #2c3e50)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>',
|
|
51
|
+
|
|
52
|
+
'background': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z" fill="var(--theme-color-background-secondary, #d5e8f7)" stroke="var(--theme-color-text-primary, #2c3e50)" stroke-width="2"/></svg>',
|
|
53
|
+
|
|
54
|
+
'connect': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M8 8l8 8" stroke="var(--theme-color-text-primary, #2c3e50)" stroke-width="2"/><circle cx="6" cy="6" r="3" fill="var(--theme-color-background-secondary, #d5e8f7)" stroke="var(--theme-color-text-primary, #2c3e50)" stroke-width="2"/><circle cx="18" cy="18" r="3" fill="var(--theme-color-background-secondary, #d5e8f7)" stroke="var(--theme-color-text-primary, #2c3e50)" stroke-width="2"/></svg>',
|
|
55
|
+
|
|
48
56
|
'fullscreen': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke="var(--theme-color-text-primary, #2c3e50)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M9 21H3v-6"/><path d="M21 3l-7 7"/><path d="M3 21l7-7"/></svg>',
|
|
49
57
|
|
|
50
58
|
'exit-fullscreen': '<svg xmlns="http://www.w3.org/2000/svg" width="{FlowIconSize}" height="{FlowIconSize}" viewBox="0 0 24 24" fill="none" stroke="var(--theme-color-text-primary, #2c3e50)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 14h6v6"/><path d="M20 10h-6V4"/><path d="M14 10l7-7"/><path d="M3 21l7-7"/></svg>',
|
|
@@ -116,12 +116,13 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
|
|
|
116
116
|
|
|
117
117
|
// Hit area (wider invisible path for easier selection)
|
|
118
118
|
let tmpShapeProvider = this._FlowView._ConnectorShapesProvider;
|
|
119
|
+
let tmpPathElement = null;
|
|
119
120
|
if (tmpShapeProvider)
|
|
120
121
|
{
|
|
121
122
|
let tmpHitArea = tmpShapeProvider.createConnectionHitAreaElement(tmpPath, pConnection.Hash);
|
|
122
123
|
pConnectionsLayer.appendChild(tmpHitArea);
|
|
123
124
|
|
|
124
|
-
|
|
125
|
+
tmpPathElement = tmpShapeProvider.createConnectionPathElement(
|
|
125
126
|
tmpPath, pConnection.Hash, pIsSelected, tmpViewIdentifier);
|
|
126
127
|
if (tmpConnTypeClass)
|
|
127
128
|
{
|
|
@@ -145,7 +146,7 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
|
|
|
145
146
|
tmpHitArea.setAttribute('data-element-type', 'connection-hitarea');
|
|
146
147
|
pConnectionsLayer.appendChild(tmpHitArea);
|
|
147
148
|
|
|
148
|
-
|
|
149
|
+
tmpPathElement = this._FlowView._SVGHelperProvider.createSVGElement('path');
|
|
149
150
|
tmpPathElement.setAttribute('class', `pict-flow-connection${tmpConnTypeClass} ${pIsSelected ? 'selected' : ''}`);
|
|
150
151
|
tmpPathElement.setAttribute('d', tmpPath);
|
|
151
152
|
tmpPathElement.setAttribute('data-connection-hash', pConnection.Hash);
|
|
@@ -160,14 +161,25 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
|
|
|
160
161
|
pConnectionsLayer.appendChild(tmpPathElement);
|
|
161
162
|
}
|
|
162
163
|
|
|
164
|
+
// Per-connection host styling (a moodboard styles its own edges): stroke color / width / dash and
|
|
165
|
+
// the end markers, applied only when the connection's Data carries them so default (workflow)
|
|
166
|
+
// connections are untouched. A label, when set, is drawn at the midpoint.
|
|
167
|
+
let tmpHasMarkers = (typeof tmpData.SourceMarker !== 'undefined' || typeof tmpData.TargetMarker !== 'undefined');
|
|
168
|
+
this._applyConnectionStyle(tmpPathElement, tmpData, tmpViewIdentifier);
|
|
169
|
+
this._renderConnectionLabel(pConnection, tmpData, pConnectionsLayer, tmpSourcePos, tmpTargetPos);
|
|
170
|
+
|
|
163
171
|
// Render the colored endpoint dot at each end of the connection
|
|
164
172
|
// into the dedicated endpoints layer (sits *above* the nodes
|
|
165
173
|
// layer so the dot doesn't get hidden under the card chrome
|
|
166
174
|
// when an edge theme places it on the node's perimeter).
|
|
167
175
|
// PortRenderer suppresses its own circle for any port that
|
|
168
|
-
// participates in a connection — we own the dot here.
|
|
176
|
+
// participates in a connection — we own the dot here. Skipped when the
|
|
177
|
+
// connection supplies its own end markers (those own the endpoints).
|
|
169
178
|
let tmpEndpointsLayer = this._FlowView._EndpointsLayer || pConnectionsLayer;
|
|
170
|
-
|
|
179
|
+
if (!tmpHasMarkers)
|
|
180
|
+
{
|
|
181
|
+
this._renderEndpointDots(pConnection, tmpEndpointsLayer, tmpSourcePos, tmpTargetPos);
|
|
182
|
+
}
|
|
171
183
|
|
|
172
184
|
// When the resolved attachment differs from the card-defined port
|
|
173
185
|
// position (e.g. a Perimeter theme moved the dot to the edge of the
|
|
@@ -187,6 +199,66 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
|
|
|
187
199
|
}
|
|
188
200
|
}
|
|
189
201
|
|
|
202
|
+
// Apply per-connection host styling to the path element from the connection's Data: StrokeColor,
|
|
203
|
+
// StrokeWidth, StrokeStyle ('solid' | 'dashed' | 'dotted') and SourceMarker / TargetMarker
|
|
204
|
+
// ('none' | 'arrow' | 'dot' | 'square'). Each is applied only when present, so connections that do
|
|
205
|
+
// not carry these keys render exactly as before.
|
|
206
|
+
_applyConnectionStyle(pPathElement, pData, pViewIdentifier)
|
|
207
|
+
{
|
|
208
|
+
if (!pPathElement || !pData || !pPathElement.style) { return; }
|
|
209
|
+
// Stroke color / width / dash go through inline style, NOT SVG presentation attributes: the
|
|
210
|
+
// .pict-flow-connection CSS rule sets stroke + stroke-width, and a stylesheet rule beats a
|
|
211
|
+
// presentation attribute, so an attribute would be silently ignored. Inline style outranks the
|
|
212
|
+
// stylesheet, so the per-connection appearance wins.
|
|
213
|
+
if (pData.StrokeColor) { pPathElement.style.stroke = pData.StrokeColor; }
|
|
214
|
+
if (pData.StrokeWidth) { pPathElement.style.strokeWidth = String(pData.StrokeWidth); }
|
|
215
|
+
if (pData.StrokeStyle)
|
|
216
|
+
{
|
|
217
|
+
pPathElement.style.strokeDasharray = (pData.StrokeStyle === 'dashed') ? '7,5' : ((pData.StrokeStyle === 'dotted') ? '1.5,4' : 'none');
|
|
218
|
+
}
|
|
219
|
+
// Markers are not styled by CSS, so attributes are correct (they reference the marker defs).
|
|
220
|
+
if (typeof pData.TargetMarker !== 'undefined')
|
|
221
|
+
{
|
|
222
|
+
let tmpEnd = this._connectionMarkerId(pData.TargetMarker, 'end', pViewIdentifier);
|
|
223
|
+
if (tmpEnd) { pPathElement.setAttribute('marker-end', 'url(#' + tmpEnd + ')'); }
|
|
224
|
+
else { pPathElement.removeAttribute('marker-end'); }
|
|
225
|
+
}
|
|
226
|
+
if (typeof pData.SourceMarker !== 'undefined')
|
|
227
|
+
{
|
|
228
|
+
let tmpStart = this._connectionMarkerId(pData.SourceMarker, 'start', pViewIdentifier);
|
|
229
|
+
if (tmpStart) { pPathElement.setAttribute('marker-start', 'url(#' + tmpStart + ')'); }
|
|
230
|
+
else { pPathElement.removeAttribute('marker-start'); }
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Resolve a marker name + end ('start' | 'end') to its SVG marker def id; null for 'none'/unknown.
|
|
235
|
+
_connectionMarkerId(pMarker, pEnd, pViewIdentifier)
|
|
236
|
+
{
|
|
237
|
+
if (pMarker === 'arrow') { return 'flow-marker-arrow-' + pEnd + '-' + pViewIdentifier; }
|
|
238
|
+
if (pMarker === 'dot') { return 'flow-marker-dot-' + pViewIdentifier; }
|
|
239
|
+
if (pMarker === 'square') { return 'flow-marker-square-' + pViewIdentifier; }
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Draw a connection's label (Data.Label) at the midpoint of its endpoints. A white halo (paint-order
|
|
244
|
+
// stroke, set in CSS) keeps it legible over the line. Skipped when there is no label.
|
|
245
|
+
_renderConnectionLabel(pConnection, pData, pLayer, pSourcePos, pTargetPos)
|
|
246
|
+
{
|
|
247
|
+
// Render when a Label key is present (even ''), so a host that edits the label in place has an
|
|
248
|
+
// element to update; a connection with no Label key (a default workflow edge) gets none.
|
|
249
|
+
if (!pData || typeof pData.Label === 'undefined' || !pSourcePos || !pTargetPos) { return; }
|
|
250
|
+
if (!this._FlowView._SVGHelperProvider) { return; }
|
|
251
|
+
let tmpText = this._FlowView._SVGHelperProvider.createSVGElement('text');
|
|
252
|
+
tmpText.setAttribute('class', 'pict-flow-connection-label');
|
|
253
|
+
tmpText.setAttribute('x', String((pSourcePos.x + pTargetPos.x) / 2));
|
|
254
|
+
tmpText.setAttribute('y', String((pSourcePos.y + pTargetPos.y) / 2));
|
|
255
|
+
tmpText.setAttribute('text-anchor', 'middle');
|
|
256
|
+
tmpText.setAttribute('dominant-baseline', 'middle');
|
|
257
|
+
tmpText.setAttribute('data-connection-hash', pConnection.Hash);
|
|
258
|
+
tmpText.textContent = pData.Label;
|
|
259
|
+
pLayer.appendChild(tmpText);
|
|
260
|
+
}
|
|
261
|
+
|
|
190
262
|
/**
|
|
191
263
|
* Append the colored endpoint dots for both ends of a connection
|
|
192
264
|
* onto the destination layer (absolute coords). Reuses
|
|
@@ -1163,7 +1163,8 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
|
|
|
1163
1163
|
|
|
1164
1164
|
if (!tmpNodeHash || !tmpPortHash) return;
|
|
1165
1165
|
|
|
1166
|
-
|
|
1166
|
+
// A connection normally starts from an output port; undirected mode lets any port start one.
|
|
1167
|
+
if (!this._FlowView.options.EnableUndirectedConnections && tmpPortDirection !== 'output')
|
|
1167
1168
|
{
|
|
1168
1169
|
return;
|
|
1169
1170
|
}
|
|
@@ -1217,7 +1218,8 @@ class PictServiceFlowInteractionManager extends libFableServiceProviderBase
|
|
|
1217
1218
|
let tmpTargetNodeHash = tmpTarget.getAttribute('data-node-hash');
|
|
1218
1219
|
let tmpTargetPortDirection = tmpTarget.getAttribute('data-port-direction');
|
|
1219
1220
|
|
|
1220
|
-
|
|
1221
|
+
// A connection normally completes on an input port; undirected mode lets any port receive it.
|
|
1222
|
+
if (tmpTargetPortHash && tmpTargetNodeHash && (this._FlowView.options.EnableUndirectedConnections || tmpTargetPortDirection === 'input'))
|
|
1221
1223
|
{
|
|
1222
1224
|
this._FlowView.addConnection(
|
|
1223
1225
|
this._ConnectSourceNodeHash,
|
|
@@ -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);
|
|
@@ -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
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
+ '{~
|
|
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
|
-
+ '{~
|
|
143
|
-
+ '{~
|
|
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
|
-
+ '{~
|
|
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:
|
|
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:
|
|
690
|
-
|
|
691
|
-
|
|
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,6 +59,11 @@ 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,
|
|
62
67
|
// When on, the selected node shows a bottom-right grip that resizes it by drag. Off by default
|
|
63
68
|
// so existing diagrams are unaffected; free-form canvases (moodboards) turn it on.
|
|
64
69
|
EnableNodeResizing: false,
|
|
@@ -73,6 +78,15 @@ const _DefaultConfiguration =
|
|
|
73
78
|
EnableAlignmentGuides: false,
|
|
74
79
|
EnableLayoutMenu: true,
|
|
75
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
|
+
|
|
76
90
|
MinZoom: 0.1,
|
|
77
91
|
MaxZoom: 5.0,
|
|
78
92
|
ZoomStep: 0.1,
|
|
@@ -562,7 +576,8 @@ class PictViewFlow extends libPictView
|
|
|
562
576
|
DefaultDestinationAddress: `#Flow-Toolbar-${tmpViewIdentifier}`,
|
|
563
577
|
FlowViewIdentifier: tmpViewIdentifier,
|
|
564
578
|
EnableAddNode: this.options.EnableAddNode,
|
|
565
|
-
EnableCardPalette: this.options.EnableCardPalette
|
|
579
|
+
EnableCardPalette: this.options.EnableCardPalette,
|
|
580
|
+
ToolbarExtraButtons: this.options.ToolbarExtraButtons
|
|
566
581
|
}
|
|
567
582
|
));
|
|
568
583
|
// Use the toolbar's render method after it's set up
|
|
@@ -1418,6 +1433,27 @@ class PictViewFlow extends libPictView
|
|
|
1418
1433
|
let tmpSource = this.getPortPosition(tmpConnection.SourceNodeHash, tmpConnection.SourcePortHash);
|
|
1419
1434
|
let tmpTarget = this.getPortPosition(tmpConnection.TargetNodeHash, tmpConnection.TargetPortHash);
|
|
1420
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
|
+
}
|
|
1421
1457
|
return { x: (tmpSource.x + tmpTarget.x) / 2, y: (tmpSource.y + tmpTarget.y) / 2 };
|
|
1422
1458
|
}
|
|
1423
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
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const libFable = require('fable');
|
|
2
|
+
const libChai = require('chai');
|
|
3
|
+
const libExpect = libChai.expect;
|
|
4
|
+
|
|
5
|
+
const libConnectorShapes = require('../source/providers/PictProvider-Flow-ConnectorShapes.js');
|
|
6
|
+
const libConnectionRenderer = require('../source/services/PictService-Flow-ConnectionRenderer.js');
|
|
7
|
+
|
|
8
|
+
// Per-connection appearance: a host (a moodboard) styles its own edges by stamping Data on a
|
|
9
|
+
// connection -- stroke color / width / style and the marker at each end ('none' | 'arrow' | 'dot' |
|
|
10
|
+
// 'square'). The renderer applies these only when present, so default (workflow) edges are untouched.
|
|
11
|
+
suite('Flow connection appearance',
|
|
12
|
+
function ()
|
|
13
|
+
{
|
|
14
|
+
suite('marker defs',
|
|
15
|
+
function ()
|
|
16
|
+
{
|
|
17
|
+
test('generateMarkerDefs includes the generic arrow / dot / square markers (color-matched)',
|
|
18
|
+
function ()
|
|
19
|
+
{
|
|
20
|
+
let tmpProvider = new libConnectorShapes(new libFable({}), {}, 'CS-Test');
|
|
21
|
+
let tmpDefs = tmpProvider.generateMarkerDefs('view1');
|
|
22
|
+
libExpect(tmpDefs).to.contain('flow-marker-arrow-end-view1');
|
|
23
|
+
libExpect(tmpDefs).to.contain('flow-marker-arrow-start-view1');
|
|
24
|
+
libExpect(tmpDefs).to.contain('flow-marker-dot-view1');
|
|
25
|
+
libExpect(tmpDefs).to.contain('flow-marker-square-view1');
|
|
26
|
+
libExpect(tmpDefs).to.contain('context-stroke');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
suite('_connectionMarkerId',
|
|
31
|
+
function ()
|
|
32
|
+
{
|
|
33
|
+
let _Resolve = libConnectionRenderer.prototype._connectionMarkerId;
|
|
34
|
+
|
|
35
|
+
test('resolves marker names to def ids per end; none / unknown -> null',
|
|
36
|
+
function ()
|
|
37
|
+
{
|
|
38
|
+
libExpect(_Resolve('arrow', 'end', 'v')).to.equal('flow-marker-arrow-end-v');
|
|
39
|
+
libExpect(_Resolve('arrow', 'start', 'v')).to.equal('flow-marker-arrow-start-v');
|
|
40
|
+
libExpect(_Resolve('dot', 'end', 'v')).to.equal('flow-marker-dot-v');
|
|
41
|
+
libExpect(_Resolve('square', 'end', 'v')).to.equal('flow-marker-square-v');
|
|
42
|
+
libExpect(_Resolve('none', 'end', 'v')).to.equal(null);
|
|
43
|
+
libExpect(_Resolve(undefined, 'end', 'v')).to.equal(null);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
suite('_applyConnectionStyle',
|
|
48
|
+
function ()
|
|
49
|
+
{
|
|
50
|
+
let _Self = { _connectionMarkerId: libConnectionRenderer.prototype._connectionMarkerId };
|
|
51
|
+
function makePath()
|
|
52
|
+
{
|
|
53
|
+
let tmpAttrs = {};
|
|
54
|
+
// Stroke color / width / dash are set via inline style (they must outrank the .pict-flow-connection
|
|
55
|
+
// CSS rule); markers are SVG attributes.
|
|
56
|
+
return { style: {}, setAttribute: function (pK, pV) { tmpAttrs[pK] = pV; }, removeAttribute: function (pK) { delete tmpAttrs[pK]; }, attrs: tmpAttrs };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
test('applies stroke color / width / dash via inline style + markers via attributes',
|
|
60
|
+
function ()
|
|
61
|
+
{
|
|
62
|
+
let tmpPath = makePath();
|
|
63
|
+
libConnectionRenderer.prototype._applyConnectionStyle.call(_Self, tmpPath, { StrokeColor: '#abcdef', StrokeWidth: 4, StrokeStyle: 'dashed', SourceMarker: 'dot', TargetMarker: 'arrow' }, 'v');
|
|
64
|
+
libExpect(tmpPath.style.stroke).to.equal('#abcdef');
|
|
65
|
+
libExpect(tmpPath.style.strokeWidth).to.equal('4');
|
|
66
|
+
libExpect(tmpPath.style.strokeDasharray).to.equal('7,5');
|
|
67
|
+
libExpect(tmpPath.attrs['marker-start']).to.equal('url(#flow-marker-dot-v)');
|
|
68
|
+
libExpect(tmpPath.attrs['marker-end']).to.equal('url(#flow-marker-arrow-end-v)');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('solid sets the dash to none; a none marker clears that end',
|
|
72
|
+
function ()
|
|
73
|
+
{
|
|
74
|
+
let tmpPath = makePath();
|
|
75
|
+
tmpPath.setAttribute('marker-end', 'url(#old)');
|
|
76
|
+
libConnectionRenderer.prototype._applyConnectionStyle.call(_Self, tmpPath, { StrokeStyle: 'solid', TargetMarker: 'none' }, 'v');
|
|
77
|
+
libExpect(tmpPath.style.strokeDasharray).to.equal('none');
|
|
78
|
+
libExpect(tmpPath.attrs['marker-end']).to.equal(undefined);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('Data with no style keys leaves the element untouched',
|
|
82
|
+
function ()
|
|
83
|
+
{
|
|
84
|
+
let tmpPath = makePath();
|
|
85
|
+
libConnectionRenderer.prototype._applyConnectionStyle.call(_Self, tmpPath, {}, 'v');
|
|
86
|
+
libExpect(Object.keys(tmpPath.attrs).length).to.equal(0);
|
|
87
|
+
libExpect(Object.keys(tmpPath.style).length).to.equal(0);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const libChai = require('chai');
|
|
2
|
+
const libExpect = libChai.expect;
|
|
3
|
+
|
|
4
|
+
const libPictViewFlow = require('../source/views/PictView-Flow.js');
|
|
5
|
+
const libPictViewFlowToolbar = require('../source/views/PictView-Flow-Toolbar.js');
|
|
6
|
+
const libPictViewFlowFloatingToolbar = require('../source/views/PictView-Flow-FloatingToolbar.js');
|
|
7
|
+
|
|
8
|
+
// The toolbar custom-button API (ToolbarExtraButtons + onToolbarButton) lets a host add its own buttons
|
|
9
|
+
// to the flow toolbar. They render in both the docked and the floating toolbar (so they survive every
|
|
10
|
+
// toolbar mode) and, on click, fire the FlowView's onToolbarButton hook. These are harness-free unit
|
|
11
|
+
// tests: the render-time stamping and the click dispatch are prototype methods called against light
|
|
12
|
+
// stubs, so no DOM or full Pict app is needed.
|
|
13
|
+
suite('Flow Toolbar custom buttons (ToolbarExtraButtons)',
|
|
14
|
+
function ()
|
|
15
|
+
{
|
|
16
|
+
suite('default_configuration is additive + backward compatible',
|
|
17
|
+
function ()
|
|
18
|
+
{
|
|
19
|
+
test('the flow view defaults to no extra buttons and no hook',
|
|
20
|
+
function ()
|
|
21
|
+
{
|
|
22
|
+
libExpect(libPictViewFlow.default_configuration.ToolbarExtraButtons).to.be.an('array');
|
|
23
|
+
libExpect(libPictViewFlow.default_configuration.ToolbarExtraButtons.length).to.equal(0);
|
|
24
|
+
libExpect(libPictViewFlow.default_configuration.onToolbarButton).to.equal(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('both toolbar views default to an empty extra-button list',
|
|
28
|
+
function ()
|
|
29
|
+
{
|
|
30
|
+
libExpect(libPictViewFlowToolbar.default_configuration.ToolbarExtraButtons).to.be.an('array');
|
|
31
|
+
libExpect(libPictViewFlowToolbar.default_configuration.ToolbarExtraButtons.length).to.equal(0);
|
|
32
|
+
libExpect(libPictViewFlowFloatingToolbar.default_configuration.ToolbarExtraButtons).to.be.an('array');
|
|
33
|
+
libExpect(libPictViewFlowFloatingToolbar.default_configuration.ToolbarExtraButtons.length).to.equal(0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('both toolbar templates render the extra-button group + row template',
|
|
37
|
+
function ()
|
|
38
|
+
{
|
|
39
|
+
let tmpDockedTemplates = libPictViewFlowToolbar.default_configuration.Templates;
|
|
40
|
+
let tmpDockedBar = tmpDockedTemplates.find((pT) => pT.Hash === 'Flow-Toolbar-Template');
|
|
41
|
+
let tmpDockedRow = tmpDockedTemplates.find((pT) => pT.Hash === 'Flow-Toolbar-Extra-Button');
|
|
42
|
+
libExpect(tmpDockedBar.Template).to.contain('{~TS:Flow-Toolbar-Extra-Button:Record.ToolbarExtraButtons~}');
|
|
43
|
+
libExpect(tmpDockedRow).to.be.an('object');
|
|
44
|
+
libExpect(tmpDockedRow.Template).to.contain('_handleExtraAction');
|
|
45
|
+
|
|
46
|
+
let tmpFloatTemplates = libPictViewFlowFloatingToolbar.default_configuration.Templates;
|
|
47
|
+
let tmpFloatBar = tmpFloatTemplates.find((pT) => pT.Hash === 'Flow-FloatingToolbar-Template');
|
|
48
|
+
let tmpFloatRow = tmpFloatTemplates.find((pT) => pT.Hash === 'Flow-FloatingToolbar-Extra-Button');
|
|
49
|
+
libExpect(tmpFloatBar.Template).to.contain('{~TS:Flow-FloatingToolbar-Extra-Button:Record.ToolbarExtraButtons~}');
|
|
50
|
+
libExpect(tmpFloatRow).to.be.an('object');
|
|
51
|
+
libExpect(tmpFloatRow.Template).to.contain('_handleExtraClick');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
suite('_stampExtraButtons (render-time per-row fields)',
|
|
56
|
+
function ()
|
|
57
|
+
{
|
|
58
|
+
test('stamps FlowViewIdentifier and an empty ActiveClass onto each button',
|
|
59
|
+
function ()
|
|
60
|
+
{
|
|
61
|
+
let tmpStub =
|
|
62
|
+
{
|
|
63
|
+
options:
|
|
64
|
+
{
|
|
65
|
+
FlowViewIdentifier: 'MB-FlowView-7',
|
|
66
|
+
ToolbarExtraButtons: [ { Hash: 'background', Icon: 'background' }, { Hash: 'done', Icon: 'check' } ]
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
libPictViewFlowToolbar.prototype._stampExtraButtons.call(tmpStub);
|
|
70
|
+
libExpect(tmpStub.options.ToolbarExtraButtons[0].FlowViewIdentifier).to.equal('MB-FlowView-7');
|
|
71
|
+
libExpect(tmpStub.options.ToolbarExtraButtons[0].ActiveClass).to.equal('');
|
|
72
|
+
libExpect(tmpStub.options.ToolbarExtraButtons[1].FlowViewIdentifier).to.equal('MB-FlowView-7');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('an Active button gets the active class',
|
|
76
|
+
function ()
|
|
77
|
+
{
|
|
78
|
+
let tmpStub =
|
|
79
|
+
{
|
|
80
|
+
options:
|
|
81
|
+
{
|
|
82
|
+
FlowViewIdentifier: 'Pict-Flow',
|
|
83
|
+
ToolbarExtraButtons: [ { Hash: 'edit', Icon: 'edit', Active: true } ]
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
libPictViewFlowToolbar.prototype._stampExtraButtons.call(tmpStub);
|
|
87
|
+
libExpect(tmpStub.options.ToolbarExtraButtons[0].ActiveClass).to.equal(' pict-flow-toolbar-btn-active');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('a non-array extra-button option is tolerated',
|
|
91
|
+
function ()
|
|
92
|
+
{
|
|
93
|
+
let tmpStub = { options: { FlowViewIdentifier: 'Pict-Flow', ToolbarExtraButtons: false } };
|
|
94
|
+
libExpect(function () { libPictViewFlowToolbar.prototype._stampExtraButtons.call(tmpStub); }).to.not.throw();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
suite('click dispatch fires onToolbarButton(hash, element)',
|
|
99
|
+
function ()
|
|
100
|
+
{
|
|
101
|
+
test('the docked toolbar routes a click to the FlowView hook with the element',
|
|
102
|
+
function ()
|
|
103
|
+
{
|
|
104
|
+
let tmpFired = [];
|
|
105
|
+
let tmpElement = { id: 'the-button' };
|
|
106
|
+
let tmpStub =
|
|
107
|
+
{
|
|
108
|
+
_FlowView: { options: { onToolbarButton: (pHash, pEl) => { tmpFired.push({ Hash: pHash, El: pEl }); } } }
|
|
109
|
+
};
|
|
110
|
+
libPictViewFlowToolbar.prototype._handleExtraAction.call(tmpStub, 'background', tmpElement);
|
|
111
|
+
libExpect(tmpFired.length).to.equal(1);
|
|
112
|
+
libExpect(tmpFired[0].Hash).to.equal('background');
|
|
113
|
+
libExpect(tmpFired[0].El).to.equal(tmpElement);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('no hook configured is a no-op (does not throw)',
|
|
117
|
+
function ()
|
|
118
|
+
{
|
|
119
|
+
let tmpStub = { _FlowView: { options: { onToolbarButton: false } } };
|
|
120
|
+
libExpect(function () { libPictViewFlowToolbar.prototype._handleExtraAction.call(tmpStub, 'edit', {}); }).to.not.throw();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('the floating toolbar routes its click through the docked toolbar dispatch',
|
|
124
|
+
function ()
|
|
125
|
+
{
|
|
126
|
+
let tmpSeen = [];
|
|
127
|
+
let tmpElement = { id: 'floating-button' };
|
|
128
|
+
let tmpFloatStub =
|
|
129
|
+
{
|
|
130
|
+
_ToolbarView: { _handleExtraAction: (pHash, pEl) => { tmpSeen.push({ Hash: pHash, El: pEl }); } }
|
|
131
|
+
};
|
|
132
|
+
libPictViewFlowFloatingToolbar.prototype._handleExtraClick.call(tmpFloatStub, 'done', tmpElement);
|
|
133
|
+
libExpect(tmpSeen.length).to.equal(1);
|
|
134
|
+
libExpect(tmpSeen[0].Hash).to.equal('done');
|
|
135
|
+
libExpect(tmpSeen[0].El).to.equal(tmpElement);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const libFable = require('fable');
|
|
2
|
+
const libChai = require('chai');
|
|
3
|
+
const libExpect = libChai.expect;
|
|
4
|
+
|
|
5
|
+
const libInteractionManager = require('../source/services/PictService-Flow-InteractionManager.js');
|
|
6
|
+
const STATES = libInteractionManager.INTERACTION_STATES;
|
|
7
|
+
|
|
8
|
+
// EnableUndirectedConnections lets a connection be drawn between ANY two ports rather than only
|
|
9
|
+
// output -> input. A free-form canvas (a moodboard) turns it on so a card's ports connect in any
|
|
10
|
+
// direction; directed graphs (workflows) leave it off. These tests cover the start side of the drag
|
|
11
|
+
// (the gate that previously hard-required an output port). The completion side uses
|
|
12
|
+
// document.elementFromPoint, so it is exercised in the browser rather than here.
|
|
13
|
+
function makeFlowView(pUndirected)
|
|
14
|
+
{
|
|
15
|
+
return {
|
|
16
|
+
options: { EnableConnectionCreation: true, EnableUndirectedConnections: pUndirected },
|
|
17
|
+
// Returning null skips the drag-line DOM creation, so no document is needed.
|
|
18
|
+
getPortPosition: function () { return null; },
|
|
19
|
+
_ViewportElement: { appendChild: function () {} }
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeManager(pFable, pFlowView)
|
|
24
|
+
{
|
|
25
|
+
let tmpManager = new libInteractionManager(pFable, { FlowView: pFlowView }, 'IM-Test');
|
|
26
|
+
tmpManager._SVGElement = { classList: { add: function () {}, remove: function () {} } };
|
|
27
|
+
return tmpManager;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function makePort(pNodeHash, pPortHash, pDirection)
|
|
31
|
+
{
|
|
32
|
+
let tmpAttrs = { 'data-node-hash': pNodeHash, 'data-port-hash': pPortHash, 'data-port-direction': pDirection };
|
|
33
|
+
return { getAttribute: function (pName) { return Object.prototype.hasOwnProperty.call(tmpAttrs, pName) ? tmpAttrs[pName] : null; } };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
suite('Flow undirected connections (EnableUndirectedConnections)',
|
|
37
|
+
function ()
|
|
38
|
+
{
|
|
39
|
+
let _Fable;
|
|
40
|
+
setup(function () { _Fable = new libFable({}); });
|
|
41
|
+
|
|
42
|
+
test('directed mode (default): an input port does NOT start a connection',
|
|
43
|
+
function ()
|
|
44
|
+
{
|
|
45
|
+
let tmpManager = makeManager(_Fable, makeFlowView(false));
|
|
46
|
+
tmpManager._startConnection({ stopPropagation: function () {} }, makePort('n1', 'p1', 'input'));
|
|
47
|
+
libExpect(tmpManager._State).to.not.equal(STATES.CONNECTING);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('undirected mode: an input port DOES start a connection',
|
|
51
|
+
function ()
|
|
52
|
+
{
|
|
53
|
+
let tmpManager = makeManager(_Fable, makeFlowView(true));
|
|
54
|
+
tmpManager._startConnection({ stopPropagation: function () {} }, makePort('n1', 'p1', 'input'));
|
|
55
|
+
libExpect(tmpManager._State).to.equal(STATES.CONNECTING);
|
|
56
|
+
libExpect(tmpManager._ConnectSourceNodeHash).to.equal('n1');
|
|
57
|
+
libExpect(tmpManager._ConnectSourcePortHash).to.equal('p1');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('an output port starts a connection in either mode',
|
|
61
|
+
function ()
|
|
62
|
+
{
|
|
63
|
+
[ false, true ].forEach(function (pUndirected)
|
|
64
|
+
{
|
|
65
|
+
let tmpManager = makeManager(_Fable, makeFlowView(pUndirected));
|
|
66
|
+
tmpManager._startConnection({ stopPropagation: function () {} }, makePort('n1', 'p1', 'output'));
|
|
67
|
+
libExpect(tmpManager._State).to.equal(STATES.CONNECTING);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|