pict-section-flow 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/source/providers/PictProvider-Flow-CSS.js +49 -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-DataManager.js +1 -1
- package/source/services/PictService-Flow-InteractionManager.js +358 -24
- package/source/services/PictService-Flow-RenderManager.js +3 -1
- package/source/services/PictService-Flow-SelectionManager.js +86 -5
- package/source/views/PictView-Flow-FloatingToolbar.js +53 -0
- package/source/views/PictView-Flow-Node.js +56 -2
- package/source/views/PictView-Flow-PropertiesPanel.js +27 -5
- package/source/views/PictView-Flow-Toolbar.js +99 -11
- package/source/views/PictView-Flow.js +85 -9
- package/test/CardPalette_tests.js +43 -0
- package/test/ConnectionStyle_tests.js +90 -0
- package/test/InteractionManager_tests.js +279 -0
- package/test/NodeView_tests.js +17 -0
- package/test/SelectionManager_tests.js +185 -0
- package/test/ToolbarExtraButtons_tests.js +138 -0
- package/test/UndirectedConnections_tests.js +70 -0
|
@@ -11,6 +11,10 @@ const _DefaultConfiguration =
|
|
|
11
11
|
|
|
12
12
|
FlowViewIdentifier: 'Pict-Flow',
|
|
13
13
|
|
|
14
|
+
// Host-supplied buttons (mirrors the docked toolbar's ToolbarExtraButtons), so floating mode keeps
|
|
15
|
+
// the same custom buttons. Each entry is { Hash, Icon, Label?, Tooltip?, Active? }.
|
|
16
|
+
ToolbarExtraButtons: [],
|
|
17
|
+
|
|
14
18
|
CSS: false,
|
|
15
19
|
|
|
16
20
|
Templates:
|
|
@@ -64,6 +68,7 @@ const _DefaultConfiguration =
|
|
|
64
68
|
onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._FloatingToolbarView._handleButtonClick('fullscreen')">
|
|
65
69
|
<span id="Flow-FloatingIcon-fullscreen-{~D:Record.FlowViewIdentifier~}"></span>
|
|
66
70
|
</button>
|
|
71
|
+
{~TS:Flow-FloatingToolbar-Extra-Button:Record.ToolbarExtraButtons~}
|
|
67
72
|
<div class="pict-flow-floating-separator"></div>
|
|
68
73
|
<button class="pict-flow-floating-btn" data-flow-action="dock-toolbar" title="Dock Toolbar"
|
|
69
74
|
onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._FloatingToolbarView._handleButtonClick('dock-toolbar')">
|
|
@@ -71,6 +76,15 @@ const _DefaultConfiguration =
|
|
|
71
76
|
</button>
|
|
72
77
|
</div>
|
|
73
78
|
`
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
Hash: 'Flow-FloatingToolbar-Extra-Button',
|
|
82
|
+
// Icon-only host button (the floating toolbar is compact). Icon span
|
|
83
|
+
// is filled post-render by _populateIcons (keyed by Hash).
|
|
84
|
+
Template: /*html*/`<button class="pict-flow-floating-btn" title="{~D:Record.Tooltip~}" data-flow-action="extra" data-extra-hash="{~D:Record.Hash~}"
|
|
85
|
+
onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._FloatingToolbarView._handleExtraClick('{~D:Record.Hash~}', this)">
|
|
86
|
+
<span id="Flow-FloatingExtraIcon-{~D:Record.Hash~}-{~D:Record.FlowViewIdentifier~}"></span>
|
|
87
|
+
</button>`
|
|
74
88
|
}
|
|
75
89
|
],
|
|
76
90
|
|
|
@@ -111,9 +125,34 @@ class PictViewFlowFloatingToolbar extends libPictView
|
|
|
111
125
|
|
|
112
126
|
render(pRenderableHash, pRenderDestinationAddress, pTemplateRecordAddress)
|
|
113
127
|
{
|
|
128
|
+
// Stamp the owning view onto each host button so its row resolves.
|
|
129
|
+
let tmpExtraButtons = this.options.ToolbarExtraButtons;
|
|
130
|
+
if (Array.isArray(tmpExtraButtons))
|
|
131
|
+
{
|
|
132
|
+
for (let i = 0; i < tmpExtraButtons.length; i++)
|
|
133
|
+
{
|
|
134
|
+
tmpExtraButtons[i].FlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
114
137
|
return super.render(pRenderableHash, pRenderDestinationAddress, this.options);
|
|
115
138
|
}
|
|
116
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Handle a click on a host-supplied (ToolbarExtraButtons) floating button.
|
|
142
|
+
* Routes to the docked toolbar's _handleExtraAction (the single dispatch
|
|
143
|
+
* point that fires the FlowView's onToolbarButton hook).
|
|
144
|
+
*
|
|
145
|
+
* @param {string} pHash - The button's Hash
|
|
146
|
+
* @param {HTMLElement} pElement - The clicked button element
|
|
147
|
+
*/
|
|
148
|
+
_handleExtraClick(pHash, pElement)
|
|
149
|
+
{
|
|
150
|
+
if (this._ToolbarView)
|
|
151
|
+
{
|
|
152
|
+
this._ToolbarView._handleExtraAction(pHash, pElement);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
117
156
|
onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent)
|
|
118
157
|
{
|
|
119
158
|
let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
@@ -222,6 +261,20 @@ class PictViewFlowFloatingToolbar extends libPictView
|
|
|
222
261
|
tmpElements[0].innerHTML = tmpIconProvider.getIconSVGMarkup(tmpIconMap[tmpKeys[i]], 16);
|
|
223
262
|
}
|
|
224
263
|
}
|
|
264
|
+
|
|
265
|
+
// Host-supplied extra buttons (keyed by Hash, icon from the button's Icon key).
|
|
266
|
+
let tmpExtraButtons = this.options.ToolbarExtraButtons;
|
|
267
|
+
if (Array.isArray(tmpExtraButtons))
|
|
268
|
+
{
|
|
269
|
+
for (let i = 0; i < tmpExtraButtons.length; i++)
|
|
270
|
+
{
|
|
271
|
+
let tmpExtraIcon = this.pict.ContentAssignment.getElement(`#Flow-FloatingExtraIcon-${tmpExtraButtons[i].Hash}-${tmpFlowViewIdentifier}`);
|
|
272
|
+
if (tmpExtraIcon.length > 0)
|
|
273
|
+
{
|
|
274
|
+
tmpExtraIcon[0].innerHTML = tmpIconProvider.getIconSVGMarkup(tmpExtraButtons[i].Icon, 16);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
225
278
|
}
|
|
226
279
|
|
|
227
280
|
/**
|
|
@@ -52,6 +52,13 @@ class PictViewFlowNode extends libPictView
|
|
|
52
52
|
{
|
|
53
53
|
tmpClassList += ' pict-flow-node-color-' + tmpColorRole;
|
|
54
54
|
}
|
|
55
|
+
// A host can stamp an extra CSS class onto a node via node.NodeClass (e.g. a moodboard marks a
|
|
56
|
+
// card whose connection points should stay visible on a read-only board). Survives re-renders
|
|
57
|
+
// because it lives on the node data.
|
|
58
|
+
if (typeof pNodeData.NodeClass === 'string' && pNodeData.NodeClass)
|
|
59
|
+
{
|
|
60
|
+
tmpClassList += ' ' + pNodeData.NodeClass;
|
|
61
|
+
}
|
|
55
62
|
tmpGroup.setAttribute('class', tmpClassList);
|
|
56
63
|
tmpGroup.setAttribute('transform', `translate(${pNodeData.X}, ${pNodeData.Y})`);
|
|
57
64
|
tmpGroup.setAttribute('data-node-hash', pNodeData.Hash);
|
|
@@ -86,6 +93,13 @@ class PictViewFlowNode extends libPictView
|
|
|
86
93
|
pNodeData.Width = tmpWidth;
|
|
87
94
|
pNodeData.Height = tmpHeight;
|
|
88
95
|
|
|
96
|
+
// Optional rotation (degrees) about the node's center. Applied on top of the position
|
|
97
|
+
// translate so free-form canvases can tilt a card; zero / unset leaves it axis-aligned.
|
|
98
|
+
if (typeof pNodeData.Rotation === 'number' && pNodeData.Rotation)
|
|
99
|
+
{
|
|
100
|
+
tmpGroup.setAttribute('transform', PictViewFlowNode.nodeTransform(pNodeData.X, pNodeData.Y, pNodeData.Rotation, tmpWidth, tmpHeight));
|
|
101
|
+
}
|
|
102
|
+
|
|
89
103
|
// Determine node body mode from theme (bracket vs rect)
|
|
90
104
|
let tmpNodeBodyMode = 'rect';
|
|
91
105
|
if (this._FlowView._ThemeProvider)
|
|
@@ -160,8 +174,10 @@ class PictViewFlowNode extends libPictView
|
|
|
160
174
|
tmpGroup.appendChild(tmpTypeLabel);
|
|
161
175
|
}
|
|
162
176
|
|
|
163
|
-
// FlowCard metadata: icon in title bar, code badge in body (hover-only via CSS)
|
|
164
|
-
|
|
177
|
+
// FlowCard metadata: icon in title bar, code badge in body (hover-only via CSS). Skip the
|
|
178
|
+
// title icon entirely when there is no title bar (height 0), e.g. edge-to-edge moodboard
|
|
179
|
+
// cards, otherwise the default fallback glyph paints in the card's top-left corner.
|
|
180
|
+
if (pNodeTypeConfig && pNodeTypeConfig.CardMetadata && tmpTitleBarHeight > 0)
|
|
165
181
|
{
|
|
166
182
|
let tmpMeta = pNodeTypeConfig.CardMetadata;
|
|
167
183
|
let tmpIconProvider = this._FlowView._IconProvider;
|
|
@@ -312,6 +328,24 @@ class PictViewFlowNode extends libPictView
|
|
|
312
328
|
tmpGroup.appendChild(tmpIndicator);
|
|
313
329
|
}
|
|
314
330
|
|
|
331
|
+
// Resize handle: a small grip at the bottom-right corner, shown only when this node is
|
|
332
|
+
// selected and the flow allows node resizing. Its data-element-type routes pointer-down to
|
|
333
|
+
// the InteractionManager's node-resize path. Appended last so it paints over the body.
|
|
334
|
+
if (pIsSelected && this._FlowView.options && this._FlowView.options.EnableNodeResizing)
|
|
335
|
+
{
|
|
336
|
+
let tmpHandleSize = 14;
|
|
337
|
+
let tmpHandle = this._FlowView._SVGHelperProvider.createSVGElement('rect');
|
|
338
|
+
tmpHandle.setAttribute('class', 'pict-flow-node-resize-handle');
|
|
339
|
+
tmpHandle.setAttribute('x', String(tmpWidth - (tmpHandleSize - 4)));
|
|
340
|
+
tmpHandle.setAttribute('y', String(tmpHeight - (tmpHandleSize - 4)));
|
|
341
|
+
tmpHandle.setAttribute('width', String(tmpHandleSize));
|
|
342
|
+
tmpHandle.setAttribute('height', String(tmpHandleSize));
|
|
343
|
+
tmpHandle.setAttribute('rx', '3');
|
|
344
|
+
tmpHandle.setAttribute('data-node-hash', pNodeData.Hash);
|
|
345
|
+
tmpHandle.setAttribute('data-element-type', 'node-resize');
|
|
346
|
+
tmpGroup.appendChild(tmpHandle);
|
|
347
|
+
}
|
|
348
|
+
|
|
315
349
|
pNodesLayer.appendChild(tmpGroup);
|
|
316
350
|
}
|
|
317
351
|
|
|
@@ -556,6 +590,26 @@ class PictViewFlowNode extends libPictView
|
|
|
556
590
|
return Math.min(Math.max(8, tmpRadius), Math.floor(pTitleBarHeight / 2));
|
|
557
591
|
}
|
|
558
592
|
|
|
593
|
+
/**
|
|
594
|
+
* The SVG group transform for a node: a position translate, plus a rotation about the node's
|
|
595
|
+
* center when a non-zero rotation (degrees) is given.
|
|
596
|
+
* @param {number} pX
|
|
597
|
+
* @param {number} pY
|
|
598
|
+
* @param {number} pRotation - degrees; 0 / non-number means no rotation
|
|
599
|
+
* @param {number} pWidth
|
|
600
|
+
* @param {number} pHeight
|
|
601
|
+
* @returns {string}
|
|
602
|
+
*/
|
|
603
|
+
static nodeTransform(pX, pY, pRotation, pWidth, pHeight)
|
|
604
|
+
{
|
|
605
|
+
let tmpRotation = (typeof pRotation === 'number') ? pRotation : 0;
|
|
606
|
+
if (!tmpRotation)
|
|
607
|
+
{
|
|
608
|
+
return `translate(${pX}, ${pY})`;
|
|
609
|
+
}
|
|
610
|
+
return `translate(${pX}, ${pY}) rotate(${tmpRotation} ${pWidth / 2} ${pHeight / 2})`;
|
|
611
|
+
}
|
|
612
|
+
|
|
559
613
|
_renderRectNodeBody(pGroup, pNodeData, pWidth, pHeight, pTitleBarHeight, pNodeTypeConfig)
|
|
560
614
|
{
|
|
561
615
|
// Per-card corner radius (a node-data or node-type override of the theme default), so a card
|
|
@@ -226,14 +226,36 @@ class PictViewFlowPropertiesPanel extends libPictView
|
|
|
226
226
|
let tmpFO = pPanelsLayer.querySelector(`[data-panel-hash="${pPanelData.Hash}"]`);
|
|
227
227
|
if (tmpFO)
|
|
228
228
|
{
|
|
229
|
-
//
|
|
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,10 +59,34 @@ const _DefaultConfiguration =
|
|
|
59
59
|
EnableZooming: true,
|
|
60
60
|
EnableNodeDragging: true,
|
|
61
61
|
EnableConnectionCreation: true,
|
|
62
|
+
// When on, a connection can be drawn between ANY two ports (any port can start a drag, any port can
|
|
63
|
+
// receive it), rather than only output -> input. Off by default so directed graphs (workflows) keep
|
|
64
|
+
// their source/target semantics; free-form canvases (a moodboard, whose links are undirected) turn it
|
|
65
|
+
// on so a card's ports connect in any direction.
|
|
66
|
+
EnableUndirectedConnections: false,
|
|
67
|
+
// When on, the selected node shows a bottom-right grip that resizes it by drag. Off by default
|
|
68
|
+
// so existing diagrams are unaffected; free-form canvases (moodboards) turn it on.
|
|
69
|
+
EnableNodeResizing: false,
|
|
62
70
|
EnableGridSnap: false,
|
|
63
71
|
GridSnapSize: 20,
|
|
72
|
+
// When on, several nodes can be selected at once: shift-click a node to toggle it, drag on the
|
|
73
|
+
// empty canvas to marquee-select (shift+drag pans), and dragging any selected node moves them all.
|
|
74
|
+
// Off by default so single-selection diagrams are unaffected; free-form canvases turn it on.
|
|
75
|
+
EnableMultiSelect: false,
|
|
76
|
+
// When on, dragging a single node shows alignment guide lines (and snaps) as its edges or centers
|
|
77
|
+
// line up with other nodes. Off by default; free-form canvases turn it on.
|
|
78
|
+
EnableAlignmentGuides: false,
|
|
64
79
|
EnableLayoutMenu: true,
|
|
65
80
|
|
|
81
|
+
// Host-supplied toolbar buttons. Each entry is { Hash, Icon, Label?, Tooltip?, Active? } where Icon
|
|
82
|
+
// is a flow icon-provider key (edit, check, background, ...). They render in BOTH the docked and the
|
|
83
|
+
// floating toolbar (so they survive every toolbar mode) and, on click, fire onToolbarButton below.
|
|
84
|
+
// Empty by default, so existing consumers are unaffected.
|
|
85
|
+
ToolbarExtraButtons: [],
|
|
86
|
+
// Fired when a ToolbarExtraButtons button is clicked: onToolbarButton(pHash, pElement). The element
|
|
87
|
+
// lets the host anchor a popover next to the button. Off (false) by default.
|
|
88
|
+
onToolbarButton: false,
|
|
89
|
+
|
|
66
90
|
MinZoom: 0.1,
|
|
67
91
|
MaxZoom: 5.0,
|
|
68
92
|
ZoomStep: 0.1,
|
|
@@ -70,6 +94,8 @@ const _DefaultConfiguration =
|
|
|
70
94
|
DefaultNodeType: 'default',
|
|
71
95
|
DefaultNodeWidth: 180,
|
|
72
96
|
DefaultNodeHeight: 80,
|
|
97
|
+
MinimumNodeWidth: 48,
|
|
98
|
+
MinimumNodeHeight: 32,
|
|
73
99
|
|
|
74
100
|
// Properties panel for connections (edges). Connections are not typed, so one config serves
|
|
75
101
|
// them all: { PanelType, DefaultWidth, DefaultHeight, Title, Configuration }. When set, a
|
|
@@ -227,6 +253,9 @@ class PictViewFlow extends libPictView
|
|
|
227
253
|
PanY: 0,
|
|
228
254
|
Zoom: 1,
|
|
229
255
|
SelectedNodeHash: null,
|
|
256
|
+
// The full selection set (multi-select). SelectedNodeHash stays the primary / most
|
|
257
|
+
// recently touched member for backward compatibility; single-select keeps it == [hash].
|
|
258
|
+
SelectedNodeHashes: [],
|
|
230
259
|
SelectedConnectionHash: null,
|
|
231
260
|
SelectedTetherHash: null
|
|
232
261
|
},
|
|
@@ -547,7 +576,8 @@ class PictViewFlow extends libPictView
|
|
|
547
576
|
DefaultDestinationAddress: `#Flow-Toolbar-${tmpViewIdentifier}`,
|
|
548
577
|
FlowViewIdentifier: tmpViewIdentifier,
|
|
549
578
|
EnableAddNode: this.options.EnableAddNode,
|
|
550
|
-
EnableCardPalette: this.options.EnableCardPalette
|
|
579
|
+
EnableCardPalette: this.options.EnableCardPalette,
|
|
580
|
+
ToolbarExtraButtons: this.options.ToolbarExtraButtons
|
|
551
581
|
}
|
|
552
582
|
));
|
|
553
583
|
// Use the toolbar's render method after it's set up
|
|
@@ -558,15 +588,13 @@ class PictViewFlow extends libPictView
|
|
|
558
588
|
}
|
|
559
589
|
}
|
|
560
590
|
|
|
561
|
-
// Setup the node renderer
|
|
591
|
+
// Setup the node renderer. A consumer can override the node title-bar height (e.g. a moodboard
|
|
592
|
+
// sets it to 0 for edge-to-edge image and note cards) via the flow-level NodeTitleBarHeight
|
|
593
|
+
// option; otherwise the renderer keeps its own default.
|
|
594
|
+
let tmpNodeViewOptions = { ViewIdentifier: `Flow-NodeRenderer-${tmpViewIdentifier}`, AutoRender: false };
|
|
595
|
+
if (typeof this.options.NodeTitleBarHeight === 'number') { tmpNodeViewOptions.NodeTitleBarHeight = this.options.NodeTitleBarHeight; }
|
|
562
596
|
this._NodeView = this.fable.instantiateServiceProviderWithoutRegistration('PictViewFlowNode',
|
|
563
|
-
Object.assign({},
|
|
564
|
-
libPictViewFlowNode.default_configuration,
|
|
565
|
-
{
|
|
566
|
-
ViewIdentifier: `Flow-NodeRenderer-${tmpViewIdentifier}`,
|
|
567
|
-
AutoRender: false
|
|
568
|
-
}
|
|
569
|
-
));
|
|
597
|
+
Object.assign({}, libPictViewFlowNode.default_configuration, tmpNodeViewOptions));
|
|
570
598
|
this._NodeView._FlowView = this;
|
|
571
599
|
|
|
572
600
|
// Setup the properties panel renderer
|
|
@@ -616,6 +644,33 @@ class PictViewFlow extends libPictView
|
|
|
616
644
|
return this._SelectionManager.selectNode(pNodeHash);
|
|
617
645
|
}
|
|
618
646
|
|
|
647
|
+
/**
|
|
648
|
+
* Toggle a node's membership in the selection set (multi-select; shift-click).
|
|
649
|
+
* @param {string} pNodeHash
|
|
650
|
+
*/
|
|
651
|
+
toggleNodeSelection(pNodeHash)
|
|
652
|
+
{
|
|
653
|
+
return this._SelectionManager.toggleNodeSelection(pNodeHash);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Replace the selection set with the given node hashes (multi-select; marquee result).
|
|
658
|
+
* @param {Array<string>} pNodeHashes
|
|
659
|
+
*/
|
|
660
|
+
selectNodes(pNodeHashes)
|
|
661
|
+
{
|
|
662
|
+
return this._SelectionManager.selectNodes(pNodeHashes);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* The current selection set as an array of node hashes.
|
|
667
|
+
* @returns {Array<string>}
|
|
668
|
+
*/
|
|
669
|
+
getSelectedNodeHashes()
|
|
670
|
+
{
|
|
671
|
+
return this._SelectionManager.getSelectedNodeHashes();
|
|
672
|
+
}
|
|
673
|
+
|
|
619
674
|
/**
|
|
620
675
|
* Select a connection
|
|
621
676
|
* @param {string|null} pConnectionHash - Hash of the connection to select, or null to deselect
|
|
@@ -1378,6 +1433,27 @@ class PictViewFlow extends libPictView
|
|
|
1378
1433
|
let tmpSource = this.getPortPosition(tmpConnection.SourceNodeHash, tmpConnection.SourcePortHash);
|
|
1379
1434
|
let tmpTarget = this.getPortPosition(tmpConnection.TargetNodeHash, tmpConnection.TargetPortHash);
|
|
1380
1435
|
if (!tmpSource || !tmpTarget) return null;
|
|
1436
|
+
// A connection renders as a curve, so the straight-line average of its endpoints sits OFF the
|
|
1437
|
+
// line (the panel tether would point into empty space). Prefer the true midpoint of the rendered
|
|
1438
|
+
// path -- getPointAtLength at half its length, which is genuinely on the line. Fall back to the
|
|
1439
|
+
// endpoint average when the path element or SVG geometry is unavailable (e.g. server-side render).
|
|
1440
|
+
if (this._SVGElement && typeof this._SVGElement.querySelector === 'function')
|
|
1441
|
+
{
|
|
1442
|
+
let tmpPathElement = this._SVGElement.querySelector('.pict-flow-connection[data-connection-hash="' + pConnectionHash + '"]');
|
|
1443
|
+
if (tmpPathElement && typeof tmpPathElement.getTotalLength === 'function' && typeof tmpPathElement.getPointAtLength === 'function')
|
|
1444
|
+
{
|
|
1445
|
+
try
|
|
1446
|
+
{
|
|
1447
|
+
let tmpLength = tmpPathElement.getTotalLength();
|
|
1448
|
+
if (tmpLength > 0)
|
|
1449
|
+
{
|
|
1450
|
+
let tmpPoint = tmpPathElement.getPointAtLength(tmpLength / 2);
|
|
1451
|
+
return { x: tmpPoint.x, y: tmpPoint.y };
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
catch (pError) { /* fall through to the straight-line midpoint */ }
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1381
1457
|
return { x: (tmpSource.x + tmpTarget.x) / 2, y: (tmpSource.y + tmpTarget.y) / 2 };
|
|
1382
1458
|
}
|
|
1383
1459
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const libChai = require('chai');
|
|
2
|
+
const libExpect = libChai.expect;
|
|
3
|
+
|
|
4
|
+
const libPictViewFlowToolbar = require('../source/views/PictView-Flow-Toolbar.js');
|
|
5
|
+
|
|
6
|
+
// The card palette and the add-node list render one row per card type. Each row's icon / swatch / code
|
|
7
|
+
// pieces are conditional, but the pict template engine does NOT parse a nested {~D:~} inside a {~NE:~}
|
|
8
|
+
// (the NE `~}` terminator collides with the inner tag's), which left the palette showing raw template
|
|
9
|
+
// literals like "{~D:Record.IconHTML ~}~};">~} Image". The fix pre-renders each piece into a complete
|
|
10
|
+
// HTML block in the popup builder and renders it with a plain {~D:~}. These tests guard that the
|
|
11
|
+
// broken pattern does not return.
|
|
12
|
+
suite('Flow card palette + add-node row templates',
|
|
13
|
+
function ()
|
|
14
|
+
{
|
|
15
|
+
function templateByHash(pHash)
|
|
16
|
+
{
|
|
17
|
+
let tmpTemplate = libPictViewFlowToolbar.default_configuration.Templates.find((pT) => pT.Hash === pHash);
|
|
18
|
+
return tmpTemplate ? tmpTemplate.Template : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// A {~NE:Addr^ ... {~ ... ~} ... ~} block: an NE whose content contains a nested template tag.
|
|
22
|
+
const _NESTED_NE = /\{~NE:[^^]+\^[^~]*\{~/;
|
|
23
|
+
|
|
24
|
+
test('the card-palette row renders pre-built blocks, with no nested tag inside an NE',
|
|
25
|
+
function ()
|
|
26
|
+
{
|
|
27
|
+
let tmpTemplate = templateByHash('Flow-Cards-Card');
|
|
28
|
+
libExpect(tmpTemplate, 'Flow-Cards-Card template exists').to.be.a('string');
|
|
29
|
+
libExpect(_NESTED_NE.test(tmpTemplate), 'no nested {~..~} inside an {~NE:~}').to.equal(false);
|
|
30
|
+
libExpect(tmpTemplate).to.contain('{~D:Record.IconBlock~}');
|
|
31
|
+
libExpect(tmpTemplate).to.contain('{~D:Record.SwatchBlock~}');
|
|
32
|
+
libExpect(tmpTemplate).to.contain('{~D:Record.CodeBlock~}');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('the add-node row renders a pre-built code block, with no nested tag inside an NE',
|
|
36
|
+
function ()
|
|
37
|
+
{
|
|
38
|
+
let tmpTemplate = templateByHash('Flow-AddNode-Row');
|
|
39
|
+
libExpect(tmpTemplate, 'Flow-AddNode-Row template exists').to.be.a('string');
|
|
40
|
+
libExpect(_NESTED_NE.test(tmpTemplate), 'no nested {~..~} inside an {~NE:~}').to.equal(false);
|
|
41
|
+
libExpect(tmpTemplate).to.contain('{~D:Record.CodeBlock~}');
|
|
42
|
+
});
|
|
43
|
+
});
|