pict-section-flow 0.0.17 → 0.0.19
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 +18 -18
- package/docs/Architecture.md +1 -1
- package/docs/Data_Model.md +2 -2
- package/docs/Getting_Started.md +5 -5
- package/docs/Implementation_Reference.md +6 -6
- package/docs/Layout_Persistence.md +3 -3
- package/docs/README.md +12 -12
- package/docs/Theme_Integration.md +150 -0
- package/docs/_cover.md +1 -1
- package/docs/_sidebar.md +7 -6
- package/docs/_version.json +7 -0
- package/docs/api/PictFlowCard.md +6 -6
- package/docs/api/PictFlowCardPropertiesPanel.md +2 -2
- package/docs/api/addConnection.md +4 -4
- package/docs/api/addNode.md +6 -6
- package/docs/api/autoLayout.md +2 -2
- package/docs/api/getFlowData.md +5 -5
- package/docs/api/marshalToView.md +3 -3
- package/docs/api/openPanel.md +2 -2
- package/docs/api/registerHandler.md +3 -3
- package/docs/api/registerNodeType.md +3 -3
- package/docs/api/removeConnection.md +5 -5
- package/docs/api/removeNode.md +6 -6
- package/docs/api/saveLayout.md +2 -2
- package/docs/api/screenToSVGCoords.md +2 -2
- package/docs/api/selectNode.md +3 -3
- package/docs/api/setTheme.md +2 -2
- package/docs/api/setZoom.md +3 -3
- package/docs/api/toggleFullscreen.md +2 -2
- package/docs/card-help/EACH.md +3 -3
- package/docs/card-help/FREAD.md +5 -5
- package/docs/card-help/FWRITE.md +5 -5
- package/docs/card-help/GET.md +2 -2
- package/docs/card-help/ITE.md +3 -3
- package/docs/card-help/LOG.md +4 -4
- package/docs/card-help/NOTE.md +1 -1
- package/docs/card-help/PREV.md +2 -2
- package/docs/card-help/SET.md +5 -5
- package/docs/card-help/SPKL.md +2 -2
- package/docs/card-help/STAT.md +3 -3
- package/docs/card-help/SW.md +4 -4
- package/docs/css/docuserve.css +277 -23
- package/docs/index.html +2 -2
- package/docs/retold-catalog.json +1 -1
- package/docs/retold-keyword-index.json +1 -1
- package/example_applications/simple_cards/css/flowexample.css +2 -2
- package/example_applications/simple_cards/source/card-help-content.js +12 -12
- package/example_applications/simple_cards/source/cards/FlowCard-DataPreview.js +1 -1
- package/example_applications/simple_cards/source/sample-flows.js +410 -0
- package/example_applications/simple_cards/source/views/PictView-FlowExample-About.js +5 -5
- package/example_applications/simple_cards/source/views/PictView-FlowExample-Documentation.js +5 -5
- package/example_applications/simple_cards/source/views/PictView-FlowExample-FileWriteInfo.js +4 -4
- package/example_applications/simple_cards/source/views/PictView-FlowExample-MainWorkspace.js +141 -8
- package/example_applications/simple_cards/source/views/PictView-FlowExample-TopBar.js +2 -2
- package/package.json +8 -7
- package/source/Pict-Section-Flow.js +26 -0
- package/source/providers/PictProvider-Flow-CSS.js +435 -61
- package/source/providers/PictProvider-Flow-ConnectorShapes.js +9 -5
- package/source/providers/PictProvider-Flow-NodeTypes.js +10 -0
- package/source/providers/PictProvider-Flow-PanelChrome.js +7 -17
- package/source/providers/PictProvider-Flow-Theme.js +7 -7
- package/source/providers/edges/Edge-Bezier.js +41 -0
- package/source/providers/edges/Edge-Orthogonal.js +37 -0
- package/source/providers/edges/Edge-OrthogonalSnap.js +72 -0
- package/source/providers/edges/Edge-Perimeter-Linear.js +31 -0
- package/source/providers/edges/Edge-Perimeter-Orthogonal.js +39 -0
- package/source/providers/edges/Edge-Perimeter.js +48 -0
- package/source/providers/edges/Edge-PerimeterMath.js +92 -0
- package/source/providers/edges/Edge-Straight.js +24 -0
- package/source/providers/layouts/Layout-Circular.js +203 -0
- package/source/providers/layouts/Layout-Coerce.js +40 -0
- package/source/providers/layouts/Layout-Columnar.js +134 -0
- package/source/providers/layouts/Layout-Custom.js +27 -0
- package/source/providers/layouts/Layout-ForcedFromCenter.js +256 -0
- package/source/providers/layouts/Layout-Grid.js +134 -0
- package/source/providers/layouts/Layout-Layered.js +209 -0
- package/source/providers/layouts/Layout-Tabular.js +94 -0
- package/source/services/PictService-Flow-ConnectionRenderer.js +532 -28
- package/source/services/PictService-Flow-DataManager.js +12 -1
- package/source/services/PictService-Flow-InteractionManager.js +39 -42
- package/source/services/PictService-Flow-Layout.js +305 -121
- package/source/services/PictService-Flow-PortRenderer.js +108 -26
- package/source/services/PictService-Flow-RenderManager.js +41 -11
- package/source/views/PictView-Flow-FloatingToolbar.js +69 -61
- package/source/views/PictView-Flow-Node.js +47 -6
- package/source/views/PictView-Flow-PropertiesPanel.js +46 -53
- package/source/views/PictView-Flow-Toolbar.js +1065 -485
- package/source/views/PictView-Flow.js +455 -7
- package/test/Layout_tests.js +1400 -0
- package/test/PortRenderer_tests.js +11 -2
|
@@ -21,67 +21,152 @@ const _DefaultConfiguration =
|
|
|
21
21
|
[
|
|
22
22
|
{
|
|
23
23
|
Hash: 'Flow-Toolbar-Template',
|
|
24
|
+
// Inline onclick handlers route to the toolbar via the FlowView
|
|
25
|
+
// (the flow toolbar is reachable as ._ToolbarView from the
|
|
26
|
+
// registered FlowView). Each button is responsible for its own
|
|
27
|
+
// action — no event delegation.
|
|
24
28
|
Template: /*html*/`
|
|
25
29
|
<div class="pict-flow-toolbar" id="Flow-Toolbar-Bar-{~D:Record.FlowViewIdentifier~}">
|
|
26
30
|
<div class="pict-flow-toolbar-group">
|
|
27
|
-
<button class="pict-flow-toolbar-btn" data-flow-action="add-node" id="Flow-Toolbar-AddNode-{~D:Record.FlowViewIdentifier~}" title="Add Node"
|
|
31
|
+
<button class="pict-flow-toolbar-btn" data-flow-action="add-node" id="Flow-Toolbar-AddNode-{~D:Record.FlowViewIdentifier~}" title="Add Node"
|
|
32
|
+
onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('add-node')">
|
|
28
33
|
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-plus-{~D:Record.FlowViewIdentifier~}"></span>
|
|
29
34
|
<span class="pict-flow-toolbar-btn-text">Node</span>
|
|
30
35
|
</button>
|
|
31
|
-
<button class="pict-flow-toolbar-btn" data-flow-action="cards-popup" id="Flow-Toolbar-Cards-{~D:Record.FlowViewIdentifier~}" title="Card Palette"
|
|
36
|
+
<button class="pict-flow-toolbar-btn" data-flow-action="cards-popup" id="Flow-Toolbar-Cards-{~D:Record.FlowViewIdentifier~}" title="Card Palette"
|
|
37
|
+
onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('cards-popup')">
|
|
32
38
|
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-cards-{~D:Record.FlowViewIdentifier~}"></span>
|
|
33
39
|
<span class="pict-flow-toolbar-btn-text">Cards</span>
|
|
34
40
|
<span class="pict-flow-toolbar-btn-chevron" id="Flow-Toolbar-CardsChevron-{~D:Record.FlowViewIdentifier~}"></span>
|
|
35
41
|
</button>
|
|
36
|
-
<button class="pict-flow-toolbar-btn" data-flow-action="delete-selected" title="Delete Node"
|
|
42
|
+
<button class="pict-flow-toolbar-btn" data-flow-action="delete-selected" title="Delete Node"
|
|
43
|
+
onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('delete-selected')">
|
|
37
44
|
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-trash-{~D:Record.FlowViewIdentifier~}"></span>
|
|
38
45
|
</button>
|
|
39
46
|
</div>
|
|
40
47
|
<div class="pict-flow-toolbar-group">
|
|
41
|
-
<button class="pict-flow-toolbar-btn" data-flow-action="layout-popup" id="Flow-Toolbar-Layout-{~D:Record.FlowViewIdentifier~}" title="Manage
|
|
48
|
+
<button class="pict-flow-toolbar-btn" data-flow-action="layout-popup" id="Flow-Toolbar-Layout-{~D:Record.FlowViewIdentifier~}" title="Manage saved layouts"
|
|
49
|
+
onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('layout-popup')">
|
|
42
50
|
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-layout-{~D:Record.FlowViewIdentifier~}"></span>
|
|
43
|
-
<span class="pict-flow-toolbar-btn-text">
|
|
51
|
+
<span class="pict-flow-toolbar-btn-text">Layouts</span>
|
|
44
52
|
<span class="pict-flow-toolbar-btn-chevron" id="Flow-Toolbar-LayoutChevron-{~D:Record.FlowViewIdentifier~}"></span>
|
|
45
53
|
</button>
|
|
46
|
-
<
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
<div class="pict-flow-toolbar-btn-split" id="Flow-Toolbar-Auto-{~D:Record.FlowViewIdentifier~}">
|
|
55
|
+
<button class="pict-flow-toolbar-btn pict-flow-toolbar-btn-split-main" data-flow-action="apply-current-layout" title="Apply current layout algorithm"
|
|
56
|
+
onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('apply-current-layout')">
|
|
57
|
+
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-auto-{~D:Record.FlowViewIdentifier~}"></span>
|
|
58
|
+
<span class="pict-flow-toolbar-btn-text">Auto</span>
|
|
59
|
+
</button>
|
|
60
|
+
<button class="pict-flow-toolbar-btn pict-flow-toolbar-btn-split-chevron" data-flow-action="layout-algorithm-popup" title="Choose layout algorithm"
|
|
61
|
+
onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('layout-algorithm-popup')">
|
|
62
|
+
<span class="pict-flow-toolbar-btn-chevron" id="Flow-Toolbar-AutoChevron-{~D:Record.FlowViewIdentifier~}"></span>
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
50
65
|
</div>
|
|
51
66
|
<div class="pict-flow-toolbar-group">
|
|
52
|
-
<button class="pict-flow-toolbar-btn" data-flow-action="zoom-in" title="Zoom In"
|
|
67
|
+
<button class="pict-flow-toolbar-btn" data-flow-action="zoom-in" title="Zoom In"
|
|
68
|
+
onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('zoom-in')">
|
|
53
69
|
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-zoom-in-{~D:Record.FlowViewIdentifier~}"></span>
|
|
54
70
|
</button>
|
|
55
|
-
<button class="pict-flow-toolbar-btn" data-flow-action="zoom-out" title="Zoom Out"
|
|
71
|
+
<button class="pict-flow-toolbar-btn" data-flow-action="zoom-out" title="Zoom Out"
|
|
72
|
+
onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('zoom-out')">
|
|
56
73
|
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-zoom-out-{~D:Record.FlowViewIdentifier~}"></span>
|
|
57
74
|
</button>
|
|
58
|
-
<button class="pict-flow-toolbar-btn" data-flow-action="zoom-fit" title="Fit to View"
|
|
75
|
+
<button class="pict-flow-toolbar-btn" data-flow-action="zoom-fit" title="Fit to View"
|
|
76
|
+
onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('zoom-fit')">
|
|
59
77
|
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-zoom-fit-{~D:Record.FlowViewIdentifier~}"></span>
|
|
60
78
|
</button>
|
|
61
79
|
</div>
|
|
62
80
|
<div class="pict-flow-toolbar-group pict-flow-toolbar-right">
|
|
63
|
-
<button class="pict-flow-toolbar-btn" data-flow-action="settings-popup" id="Flow-Toolbar-Settings-{~D:Record.FlowViewIdentifier~}" title="Theme Settings"
|
|
81
|
+
<button class="pict-flow-toolbar-btn" data-flow-action="settings-popup" id="Flow-Toolbar-Settings-{~D:Record.FlowViewIdentifier~}" title="Theme Settings"
|
|
82
|
+
onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('settings-popup')">
|
|
64
83
|
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-settings-{~D:Record.FlowViewIdentifier~}"></span>
|
|
65
84
|
</button>
|
|
66
|
-
<button class="pict-flow-toolbar-btn" data-flow-action="fullscreen" id="Flow-Toolbar-Fullscreen-{~D:Record.FlowViewIdentifier~}" title="Toggle Fullscreen"
|
|
85
|
+
<button class="pict-flow-toolbar-btn" data-flow-action="fullscreen" id="Flow-Toolbar-Fullscreen-{~D:Record.FlowViewIdentifier~}" title="Toggle Fullscreen"
|
|
86
|
+
onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('fullscreen')">
|
|
67
87
|
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Fullscreen-Icon-{~D:Record.FlowViewIdentifier~}"></span>
|
|
68
88
|
</button>
|
|
69
|
-
<button class="pict-flow-toolbar-btn" data-flow-action="toggle-floating" title="Float"
|
|
89
|
+
<button class="pict-flow-toolbar-btn" data-flow-action="toggle-floating" title="Float"
|
|
90
|
+
onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('toggle-floating')">
|
|
70
91
|
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-grip-{~D:Record.FlowViewIdentifier~}"></span>
|
|
71
92
|
</button>
|
|
72
|
-
<button class="pict-flow-toolbar-btn" data-flow-action="collapse-toolbar" title="Collapse Toolbar"
|
|
93
|
+
<button class="pict-flow-toolbar-btn" data-flow-action="collapse-toolbar" title="Collapse Toolbar"
|
|
94
|
+
onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('collapse-toolbar')">
|
|
73
95
|
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-collapse-{~D:Record.FlowViewIdentifier~}"></span>
|
|
74
96
|
</button>
|
|
75
97
|
</div>
|
|
76
98
|
</div>
|
|
77
99
|
<div class="pict-flow-toolbar-collapsed" id="Flow-Toolbar-Collapsed-{~D:Record.FlowViewIdentifier~}">
|
|
78
|
-
<button class="pict-flow-toolbar-expand-btn" data-flow-action="expand-toolbar" title="Expand Toolbar" id="Flow-Toolbar-ExpandBtn-{~D:Record.FlowViewIdentifier~}"
|
|
100
|
+
<button class="pict-flow-toolbar-expand-btn" data-flow-action="expand-toolbar" title="Expand Toolbar" id="Flow-Toolbar-ExpandBtn-{~D:Record.FlowViewIdentifier~}"
|
|
101
|
+
onclick="_Pict.views['{~D:Record.FlowViewIdentifier~}']._ToolbarView._handleToolbarAction('expand-toolbar')">
|
|
79
102
|
<span id="Flow-Toolbar-Icon-expand-{~D:Record.FlowViewIdentifier~}"></span>
|
|
80
103
|
</button>
|
|
81
104
|
</div>
|
|
82
105
|
<div class="pict-flow-toolbar-popup-anchor" id="Flow-Toolbar-PopupAnchor-{~D:Record.FlowViewIdentifier~}">
|
|
83
106
|
</div>
|
|
84
107
|
`
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
Hash: 'Flow-AddNode-List',
|
|
111
|
+
// Iteration source is `Record.Rows` — the outer call sets the
|
|
112
|
+
// FlowViewIdentifier on each row so inline handlers resolve their
|
|
113
|
+
// owning view independently. Nested {~D:~} inside {~TS:~}
|
|
114
|
+
// addresses isn't supported by the template engine, so we keep
|
|
115
|
+
// addresses static.
|
|
116
|
+
Template: '{~TS:Flow-AddNode-Row:Record.Rows~}'
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
Hash: 'Flow-AddNode-Row',
|
|
120
|
+
Template: '<div class="pict-flow-popup-list-item" data-node-type="{~D:Record.NodeType~}"'
|
|
121
|
+
+ ' onclick="_Pict.views[\'{~D:Record.FlowViewIdentifier~}\']._ToolbarView._addNodeFromPopup(this.getAttribute(\'data-node-type\'))">'
|
|
122
|
+
+ '<span class="pict-flow-popup-list-item-icon">{~D:Record.IconHTML~}</span>'
|
|
123
|
+
+ '<span class="pict-flow-popup-list-item-label">{~D:Record.Label~}</span>'
|
|
124
|
+
+ '{~NE:Record.Code^<span class="pict-flow-popup-list-item-code">{~D:Record.Code~}</span>~}'
|
|
125
|
+
+ '</div>'
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
Hash: 'Flow-Cards-List',
|
|
129
|
+
Template: '{~TS:Flow-Cards-Category:Record.Categories~}'
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
Hash: 'Flow-Cards-Category',
|
|
133
|
+
Template: '<div class="pict-flow-palette-category" style="padding:0.35em 0.5em;">'
|
|
134
|
+
+ '<div class="pict-flow-palette-category-label">{~D:Record.Name~}</div>'
|
|
135
|
+
+ '<div class="pict-flow-palette-cards">{~TS:Flow-Cards-Card:Record.Cards~}</div>'
|
|
136
|
+
+ '</div>'
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
Hash: 'Flow-Cards-Card',
|
|
140
|
+
Template: '<div class="pict-flow-palette-card{~D:Record.DisabledClass~}" data-card-type="{~D:Record.CardType~}" title="{~D:Record.Tooltip~}"'
|
|
141
|
+
+ ' onclick="_Pict.views[\'{~D:Record.FlowViewIdentifier~}\']._ToolbarView._addCardFromPopup(this.getAttribute(\'data-card-type\'))">'
|
|
142
|
+
+ '{~NE:Record.IconHTML^<span class="pict-flow-palette-card-icon">{~D:Record.IconHTML~}</span>~}'
|
|
143
|
+
+ '{~NE:Record.IconEmoji^<span class="pict-flow-palette-card-icon">{~D:Record.IconEmoji~}</span>~}'
|
|
144
|
+
+ '{~NE:Record.SwatchColor^<span class="pict-flow-palette-card-swatch" style="background-color: {~D:Record.SwatchColor~};"></span>~}'
|
|
145
|
+
+ '<span class="pict-flow-palette-card-title">{~D:Record.Label~}</span>'
|
|
146
|
+
+ '{~NE:Record.Code^<span class="pict-flow-palette-card-code">{~D:Record.Code~}</span>~}'
|
|
147
|
+
+ '</div>'
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
Hash: 'Flow-Layout-List',
|
|
151
|
+
Template: '{~TS:Flow-Layout-Row:Record.Rows~}'
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
Hash: 'Flow-Layout-Row',
|
|
155
|
+
Template: '<div class="pict-flow-popup-layout-row" data-layout-hash="{~D:Record.LayoutHash~}"'
|
|
156
|
+
+ ' onclick="_Pict.views[\'{~D:Record.FlowViewIdentifier~}\']._ToolbarView._restoreLayoutFromPopup(\'{~D:Record.LayoutHash~}\', event)">'
|
|
157
|
+
+ '<span class="pict-flow-popup-layout-name">{~D:Record.Name~}</span>'
|
|
158
|
+
+ '<button class="pict-flow-popup-layout-delete" title="Delete layout"'
|
|
159
|
+
+ ' onclick="_Pict.views[\'{~D:Record.FlowViewIdentifier~}\']._ToolbarView._deleteLayoutFromPopup(\'{~D:Record.LayoutHash~}\', event)">'
|
|
160
|
+
+ '{~D:Record.DeleteIconHTML~}</button>'
|
|
161
|
+
+ '</div>'
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
Hash: 'Flow-Layout-OptionList',
|
|
165
|
+
Template: '{~TS:Flow-Layout-Option:Record.Options~}'
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
Hash: 'Flow-Layout-Option',
|
|
169
|
+
Template: '<option value="{~D:Record.Value~}"{~D:Record.SelectedAttr~}>{~D:Record.Label~}</option>'
|
|
85
170
|
}
|
|
86
171
|
],
|
|
87
172
|
|
|
@@ -113,6 +198,16 @@ class PictViewFlowToolbar extends libPictView
|
|
|
113
198
|
this._FloatingPosition = { X: 80, Y: 80 };
|
|
114
199
|
this._DocumentClickHandler = null;
|
|
115
200
|
this._FloatingToolbarView = null;
|
|
201
|
+
|
|
202
|
+
// Layout-algorithm parameter form (pict-section-form metacontroller).
|
|
203
|
+
// Lazily created on first popup open; null until then. Falls back to
|
|
204
|
+
// hand-rolled inputs when PictFormMetacontroller is not registered.
|
|
205
|
+
this._LayoutFormMetacontroller = null;
|
|
206
|
+
this._LayoutFormHostID = null;
|
|
207
|
+
// Whether the parameter form is expanded inside the popup. Persists
|
|
208
|
+
// across popup opens so a user who collapsed it doesn't have to
|
|
209
|
+
// re-collapse on every reopen. Defaults to expanded.
|
|
210
|
+
this._LayoutFormExpanded = true;
|
|
116
211
|
}
|
|
117
212
|
|
|
118
213
|
render(pRenderableHash, pRenderDestinationAddress, pTemplateRecordAddress)
|
|
@@ -126,33 +221,8 @@ class PictViewFlowToolbar extends libPictView
|
|
|
126
221
|
{
|
|
127
222
|
let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
128
223
|
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
if (tmpToolbarBar.length > 0)
|
|
132
|
-
{
|
|
133
|
-
tmpToolbarBar[0].addEventListener('click', (pEvent) =>
|
|
134
|
-
{
|
|
135
|
-
let tmpTarget = pEvent.target;
|
|
136
|
-
if (!tmpTarget) return;
|
|
137
|
-
|
|
138
|
-
// Walk up to find the button with the action
|
|
139
|
-
let tmpButton = tmpTarget.closest('[data-flow-action]');
|
|
140
|
-
if (!tmpButton) return;
|
|
141
|
-
|
|
142
|
-
let tmpAction = tmpButton.getAttribute('data-flow-action');
|
|
143
|
-
this._handleToolbarAction(tmpAction);
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Bind expand button click (it's outside the main toolbar bar)
|
|
148
|
-
let tmpExpandBtn = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-ExpandBtn-${tmpFlowViewIdentifier}`);
|
|
149
|
-
if (tmpExpandBtn.length > 0)
|
|
150
|
-
{
|
|
151
|
-
tmpExpandBtn[0].addEventListener('click', () =>
|
|
152
|
-
{
|
|
153
|
-
this._setToolbarMode('docked');
|
|
154
|
-
});
|
|
155
|
-
}
|
|
224
|
+
// Click handlers live on each button as inline `onclick` attributes
|
|
225
|
+
// in Flow-Toolbar-Template — they call _handleToolbarAction directly.
|
|
156
226
|
|
|
157
227
|
// Populate SVG icons for toolbar buttons
|
|
158
228
|
this._populateToolbarIcons();
|
|
@@ -174,6 +244,14 @@ class PictViewFlowToolbar extends libPictView
|
|
|
174
244
|
tmpCardsBtn[0].remove();
|
|
175
245
|
}
|
|
176
246
|
}
|
|
247
|
+
if (this.options.EnableLayoutMenu === false)
|
|
248
|
+
{
|
|
249
|
+
let tmpAutoBtn = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-Auto-${tmpFlowViewIdentifier}`);
|
|
250
|
+
if (tmpAutoBtn.length > 0)
|
|
251
|
+
{
|
|
252
|
+
tmpAutoBtn[0].remove();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
177
255
|
|
|
178
256
|
return super.onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent);
|
|
179
257
|
}
|
|
@@ -198,7 +276,7 @@ class PictViewFlowToolbar extends libPictView
|
|
|
198
276
|
'zoom-in': 'zoom-in',
|
|
199
277
|
'zoom-out': 'zoom-out',
|
|
200
278
|
'zoom-fit': 'zoom-fit',
|
|
201
|
-
'auto
|
|
279
|
+
'auto': 'auto-layout',
|
|
202
280
|
'cards': 'cards',
|
|
203
281
|
'layout': 'layout',
|
|
204
282
|
'settings': 'settings',
|
|
@@ -237,6 +315,12 @@ class PictViewFlowToolbar extends libPictView
|
|
|
237
315
|
{
|
|
238
316
|
tmpLayoutChevron[0].innerHTML = tmpIconProvider.getIconSVGMarkup('chevron-down', 8);
|
|
239
317
|
}
|
|
318
|
+
|
|
319
|
+
let tmpAutoChevron = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-AutoChevron-${tmpFlowViewIdentifier}`);
|
|
320
|
+
if (tmpAutoChevron.length > 0)
|
|
321
|
+
{
|
|
322
|
+
tmpAutoChevron[0].innerHTML = tmpIconProvider.getIconSVGMarkup('chevron-down', 8);
|
|
323
|
+
}
|
|
240
324
|
}
|
|
241
325
|
|
|
242
326
|
// ── Popup Management ──────────────────────────────────────────────────
|
|
@@ -278,6 +362,9 @@ class PictViewFlowToolbar extends libPictView
|
|
|
278
362
|
case 'layout':
|
|
279
363
|
this._buildLayoutPopup(tmpPopup);
|
|
280
364
|
break;
|
|
365
|
+
case 'layout-algorithm':
|
|
366
|
+
this._buildLayoutAlgorithmPopup(tmpPopup);
|
|
367
|
+
break;
|
|
281
368
|
case 'settings':
|
|
282
369
|
this._buildSettingsPopup(tmpPopup);
|
|
283
370
|
break;
|
|
@@ -366,6 +453,9 @@ class PictViewFlowToolbar extends libPictView
|
|
|
366
453
|
case 'layout':
|
|
367
454
|
tmpTriggerSelector = `#Flow-Toolbar-Layout-${tmpFlowViewIdentifier}`;
|
|
368
455
|
break;
|
|
456
|
+
case 'layout-algorithm':
|
|
457
|
+
tmpTriggerSelector = `#Flow-Toolbar-Auto-${tmpFlowViewIdentifier}`;
|
|
458
|
+
break;
|
|
369
459
|
case 'settings':
|
|
370
460
|
tmpTriggerSelector = `#Flow-Toolbar-Settings-${tmpFlowViewIdentifier}`;
|
|
371
461
|
break;
|
|
@@ -390,212 +480,180 @@ class PictViewFlowToolbar extends libPictView
|
|
|
390
480
|
// ── Add Node Popup ────────────────────────────────────────────────────
|
|
391
481
|
|
|
392
482
|
/**
|
|
393
|
-
* Build the searchable Add Node popup content
|
|
483
|
+
* Build the searchable Add Node popup content via templates with inline
|
|
484
|
+
* handlers. Search input fires `_filterNodeList` which re-renders the
|
|
485
|
+
* list section in place.
|
|
486
|
+
*
|
|
394
487
|
* @param {HTMLElement} pContainer
|
|
395
488
|
*/
|
|
396
489
|
_buildAddNodePopup(pContainer)
|
|
397
490
|
{
|
|
398
|
-
|
|
399
|
-
let tmpSearchWrapper = document.createElement('div');
|
|
400
|
-
tmpSearchWrapper.className = 'pict-flow-popup-search-wrapper';
|
|
401
|
-
|
|
402
|
-
let tmpSearchIcon = document.createElement('span');
|
|
403
|
-
tmpSearchIcon.className = 'pict-flow-popup-search-icon';
|
|
491
|
+
let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
404
492
|
let tmpIconProvider = this._FlowView ? this._FlowView._IconProvider : null;
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
// Initial population
|
|
424
|
-
this._populateNodeList(tmpListDiv, '');
|
|
425
|
-
|
|
426
|
-
// Filter on input
|
|
427
|
-
tmpSearchInput.addEventListener('input', () =>
|
|
428
|
-
{
|
|
429
|
-
this._populateNodeList(tmpListDiv, tmpSearchInput.value);
|
|
430
|
-
});
|
|
493
|
+
let tmpSearchIconHTML = tmpIconProvider ? tmpIconProvider.getIconSVGMarkup('search', 12) : '';
|
|
494
|
+
|
|
495
|
+
let tmpListID = `Flow-Toolbar-AddNodeList-${tmpFlowViewIdentifier}`;
|
|
496
|
+
let tmpSearchID = `Flow-Toolbar-AddNodeSearch-${tmpFlowViewIdentifier}`;
|
|
497
|
+
|
|
498
|
+
// Stage AppData and render the initial list HTML inline — the popup
|
|
499
|
+
// element isn't in the DOM yet, so we can't look it up via
|
|
500
|
+
// getElementById here. _filterNodeList handles updates after open.
|
|
501
|
+
let tmpInitialListHTML = this._renderNodeListHTML('');
|
|
502
|
+
|
|
503
|
+
this.pict.ContentAssignment.assignContent(pContainer,
|
|
504
|
+
'<div class="pict-flow-popup-search-wrapper">'
|
|
505
|
+
+ '<span class="pict-flow-popup-search-icon">' + tmpSearchIconHTML + '</span>'
|
|
506
|
+
+ '<input id="' + tmpSearchID + '" class="pict-flow-popup-search" type="text" placeholder="Search node types..." '
|
|
507
|
+
+ 'oninput="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._filterNodeList(\'' + tmpListID + '\', this.value)" />'
|
|
508
|
+
+ '</div>'
|
|
509
|
+
+ '<div id="' + tmpListID + '" class="pict-flow-popup-node-list">' + tmpInitialListHTML + '</div>');
|
|
431
510
|
}
|
|
432
511
|
|
|
433
512
|
/**
|
|
434
|
-
*
|
|
435
|
-
*
|
|
513
|
+
* Build the HTML string for the Add Node list filtered by the given
|
|
514
|
+
* text. Stages AppData and renders the {~TS:~} template into a string;
|
|
515
|
+
* returns "no matches" markup when nothing matches.
|
|
516
|
+
*
|
|
436
517
|
* @param {string} pFilter
|
|
518
|
+
* @returns {string}
|
|
437
519
|
*/
|
|
438
|
-
|
|
520
|
+
_renderNodeListHTML(pFilter)
|
|
439
521
|
{
|
|
440
|
-
if (!this._FlowView || !this._FlowView._NodeTypeProvider) return;
|
|
441
|
-
|
|
442
|
-
// Clear
|
|
443
|
-
while (pListDiv.firstChild)
|
|
444
|
-
{
|
|
445
|
-
pListDiv.removeChild(pListDiv.firstChild);
|
|
446
|
-
}
|
|
522
|
+
if (!this._FlowView || !this._FlowView._NodeTypeProvider) return '';
|
|
447
523
|
|
|
524
|
+
let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
448
525
|
let tmpTypes = this._FlowView._NodeTypeProvider.getNodeTypes();
|
|
449
526
|
let tmpTypeKeys = Object.keys(tmpTypes);
|
|
450
527
|
let tmpFilter = (pFilter || '').toLowerCase().trim();
|
|
451
528
|
let tmpIconProvider = this._FlowView._IconProvider;
|
|
452
|
-
let tmpMatchCount = 0;
|
|
453
529
|
|
|
530
|
+
let tmpRows = [];
|
|
454
531
|
for (let i = 0; i < tmpTypeKeys.length; i++)
|
|
455
532
|
{
|
|
456
533
|
let tmpTypeConfig = tmpTypes[tmpTypeKeys[i]];
|
|
457
534
|
let tmpMeta = tmpTypeConfig.CardMetadata || {};
|
|
458
|
-
|
|
459
|
-
// Skip disabled cards
|
|
460
535
|
if (tmpMeta.Enabled === false) continue;
|
|
461
536
|
|
|
462
|
-
// Filter match: label, code, or category
|
|
463
537
|
if (tmpFilter)
|
|
464
538
|
{
|
|
465
539
|
let tmpLabel = (tmpTypeConfig.Label || '').toLowerCase();
|
|
466
540
|
let tmpCode = (tmpMeta.Code || '').toLowerCase();
|
|
467
541
|
let tmpCategory = (tmpMeta.Category || '').toLowerCase();
|
|
468
|
-
if (tmpLabel.indexOf(tmpFilter) < 0 &&
|
|
469
|
-
tmpCode.indexOf(tmpFilter) < 0 &&
|
|
470
|
-
tmpCategory.indexOf(tmpFilter) < 0)
|
|
471
|
-
{
|
|
472
|
-
continue;
|
|
473
|
-
}
|
|
542
|
+
if (tmpLabel.indexOf(tmpFilter) < 0 && tmpCode.indexOf(tmpFilter) < 0 && tmpCategory.indexOf(tmpFilter) < 0) continue;
|
|
474
543
|
}
|
|
475
544
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
let tmpRow = document.createElement('div');
|
|
479
|
-
tmpRow.className = 'pict-flow-popup-list-item';
|
|
480
|
-
tmpRow.setAttribute('data-node-type', tmpTypeKeys[i]);
|
|
481
|
-
|
|
482
|
-
// Icon
|
|
483
|
-
let tmpIconSpan = document.createElement('span');
|
|
484
|
-
tmpIconSpan.className = 'pict-flow-popup-list-item-icon';
|
|
545
|
+
let tmpIconHTML = '';
|
|
485
546
|
if (tmpIconProvider)
|
|
486
547
|
{
|
|
487
548
|
let tmpResolvedKey = tmpIconProvider.resolveIconKey(tmpMeta);
|
|
488
|
-
|
|
489
|
-
}
|
|
490
|
-
tmpRow.appendChild(tmpIconSpan);
|
|
491
|
-
|
|
492
|
-
// Label
|
|
493
|
-
let tmpLabelSpan = document.createElement('span');
|
|
494
|
-
tmpLabelSpan.className = 'pict-flow-popup-list-item-label';
|
|
495
|
-
tmpLabelSpan.textContent = tmpTypeConfig.Label;
|
|
496
|
-
tmpRow.appendChild(tmpLabelSpan);
|
|
497
|
-
|
|
498
|
-
// Code badge
|
|
499
|
-
if (tmpMeta.Code)
|
|
500
|
-
{
|
|
501
|
-
let tmpCodeSpan = document.createElement('span');
|
|
502
|
-
tmpCodeSpan.className = 'pict-flow-popup-list-item-code';
|
|
503
|
-
tmpCodeSpan.textContent = tmpMeta.Code;
|
|
504
|
-
tmpRow.appendChild(tmpCodeSpan);
|
|
549
|
+
tmpIconHTML = tmpIconProvider.getIconSVGMarkup(tmpResolvedKey, 16);
|
|
505
550
|
}
|
|
506
|
-
|
|
507
|
-
// Click handler
|
|
508
|
-
tmpRow.addEventListener('click', () =>
|
|
551
|
+
tmpRows.push(
|
|
509
552
|
{
|
|
510
|
-
|
|
511
|
-
|
|
553
|
+
NodeType: tmpTypeKeys[i],
|
|
554
|
+
Label: tmpTypeConfig.Label || '',
|
|
555
|
+
IconHTML: tmpIconHTML,
|
|
556
|
+
Code: tmpMeta.Code || '',
|
|
557
|
+
FlowViewIdentifier: tmpFlowViewIdentifier
|
|
512
558
|
});
|
|
513
|
-
|
|
514
|
-
pListDiv.appendChild(tmpRow);
|
|
515
559
|
}
|
|
516
560
|
|
|
517
|
-
if (
|
|
561
|
+
if (tmpRows.length === 0)
|
|
518
562
|
{
|
|
519
|
-
|
|
520
|
-
tmpEmpty.className = 'pict-flow-popup-list-empty';
|
|
521
|
-
tmpEmpty.textContent = 'No matching node types';
|
|
522
|
-
pListDiv.appendChild(tmpEmpty);
|
|
563
|
+
return '<div class="pict-flow-popup-list-empty">No matching node types</div>';
|
|
523
564
|
}
|
|
565
|
+
return this.pict.parseTemplateByHash('Flow-AddNode-List', { Rows: tmpRows });
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Update the Add Node list contents in response to a search input
|
|
570
|
+
* change. Called from the inline `oninput` handler. The list element
|
|
571
|
+
* is already in the DOM by the time this fires.
|
|
572
|
+
*
|
|
573
|
+
* @param {string} pListID
|
|
574
|
+
* @param {string} pFilter
|
|
575
|
+
*/
|
|
576
|
+
_filterNodeList(pListID, pFilter)
|
|
577
|
+
{
|
|
578
|
+
let tmpListEl = document.getElementById(pListID);
|
|
579
|
+
if (!tmpListEl) return;
|
|
580
|
+
this.pict.ContentAssignment.assignContent(tmpListEl, this._renderNodeListHTML(pFilter));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Inline handler for an Add Node row click. Adds the node at viewport
|
|
585
|
+
* center and closes the popup.
|
|
586
|
+
*
|
|
587
|
+
* @param {string} pNodeType
|
|
588
|
+
*/
|
|
589
|
+
_addNodeFromPopup(pNodeType)
|
|
590
|
+
{
|
|
591
|
+
this._addNodeAtCenter(pNodeType);
|
|
592
|
+
this._closePopup();
|
|
524
593
|
}
|
|
525
594
|
|
|
526
595
|
// ── Cards Popup ───────────────────────────────────────────────────────
|
|
527
596
|
|
|
528
597
|
/**
|
|
529
|
-
* Build the Cards popup
|
|
598
|
+
* Build the Cards popup using templates with inline handlers. Search
|
|
599
|
+
* input fires `_filterCardsList` which re-renders the categorized list.
|
|
600
|
+
*
|
|
530
601
|
* @param {HTMLElement} pContainer
|
|
531
602
|
*/
|
|
532
603
|
_buildCardsPopup(pContainer)
|
|
533
604
|
{
|
|
534
|
-
|
|
535
|
-
let tmpSearchWrapper = document.createElement('div');
|
|
536
|
-
tmpSearchWrapper.className = 'pict-flow-popup-search-wrapper';
|
|
537
|
-
|
|
538
|
-
let tmpSearchIcon = document.createElement('span');
|
|
539
|
-
tmpSearchIcon.className = 'pict-flow-popup-search-icon';
|
|
605
|
+
let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
540
606
|
let tmpIconProvider = this._FlowView ? this._FlowView._IconProvider : null;
|
|
541
|
-
|
|
542
|
-
{
|
|
543
|
-
tmpSearchIcon.innerHTML = tmpIconProvider.getIconSVGMarkup('search', 12);
|
|
544
|
-
}
|
|
545
|
-
tmpSearchWrapper.appendChild(tmpSearchIcon);
|
|
607
|
+
let tmpSearchIconHTML = tmpIconProvider ? tmpIconProvider.getIconSVGMarkup('search', 12) : '';
|
|
546
608
|
|
|
547
|
-
let
|
|
548
|
-
|
|
549
|
-
tmpSearchInput.setAttribute('type', 'text');
|
|
550
|
-
tmpSearchInput.setAttribute('placeholder', 'Search cards...');
|
|
551
|
-
tmpSearchWrapper.appendChild(tmpSearchInput);
|
|
552
|
-
pContainer.appendChild(tmpSearchWrapper);
|
|
609
|
+
let tmpListID = `Flow-Toolbar-CardsList-${tmpFlowViewIdentifier}`;
|
|
610
|
+
let tmpSearchID = `Flow-Toolbar-CardsSearch-${tmpFlowViewIdentifier}`;
|
|
553
611
|
|
|
554
|
-
//
|
|
555
|
-
let
|
|
556
|
-
tmpListDiv.className = 'pict-flow-popup-node-list';
|
|
557
|
-
pContainer.appendChild(tmpListDiv);
|
|
612
|
+
// Initial render inline — popup not yet in DOM.
|
|
613
|
+
let tmpInitialListHTML = this._renderCardsListHTML('');
|
|
558
614
|
|
|
559
|
-
|
|
560
|
-
|
|
615
|
+
this.pict.ContentAssignment.assignContent(pContainer,
|
|
616
|
+
'<div class="pict-flow-popup-search-wrapper">'
|
|
617
|
+
+ '<span class="pict-flow-popup-search-icon">' + tmpSearchIconHTML + '</span>'
|
|
618
|
+
+ '<input id="' + tmpSearchID + '" class="pict-flow-popup-search" type="text" placeholder="Search cards..." '
|
|
619
|
+
+ 'oninput="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._filterCardsList(\'' + tmpListID + '\', this.value)" />'
|
|
620
|
+
+ '</div>'
|
|
621
|
+
+ '<div id="' + tmpListID + '" class="pict-flow-popup-node-list">' + tmpInitialListHTML + '</div>');
|
|
561
622
|
|
|
562
|
-
//
|
|
563
|
-
|
|
623
|
+
// Focus search input — defer past the popup append.
|
|
624
|
+
setTimeout(() =>
|
|
564
625
|
{
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
// Focus search input
|
|
569
|
-
setTimeout(() => { tmpSearchInput.focus(); }, 50);
|
|
626
|
+
let tmpSearch = document.getElementById(tmpSearchID);
|
|
627
|
+
if (tmpSearch) tmpSearch.focus();
|
|
628
|
+
}, 50);
|
|
570
629
|
}
|
|
571
630
|
|
|
572
631
|
/**
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
632
|
+
* Build the cards-list HTML string for the given filter. Stages
|
|
633
|
+
* AppData and renders via the {~TS:~} templates; returns "no matches"
|
|
634
|
+
* markup when nothing matches.
|
|
635
|
+
*
|
|
636
|
+
* @param {string} pFilter
|
|
637
|
+
* @returns {string}
|
|
576
638
|
*/
|
|
577
|
-
|
|
639
|
+
_renderCardsListHTML(pFilter)
|
|
578
640
|
{
|
|
579
|
-
if (!this._FlowView || !this._FlowView._NodeTypeProvider) return;
|
|
580
|
-
|
|
581
|
-
// Clear existing content
|
|
582
|
-
while (pContainer.firstChild)
|
|
583
|
-
{
|
|
584
|
-
pContainer.removeChild(pContainer.firstChild);
|
|
585
|
-
}
|
|
641
|
+
if (!this._FlowView || !this._FlowView._NodeTypeProvider) return '';
|
|
586
642
|
|
|
643
|
+
let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
587
644
|
let tmpCategories = this._FlowView._NodeTypeProvider.getCardsByCategory();
|
|
588
645
|
let tmpCategoryKeys = Object.keys(tmpCategories);
|
|
589
646
|
let tmpFilter = (pFilter || '').toLowerCase().trim();
|
|
647
|
+
let tmpIconProvider = this._FlowView._IconProvider;
|
|
590
648
|
let tmpTotalMatchCount = 0;
|
|
591
649
|
|
|
650
|
+
let tmpCategoryRecords = [];
|
|
592
651
|
for (let i = 0; i < tmpCategoryKeys.length; i++)
|
|
593
652
|
{
|
|
594
653
|
let tmpCategoryName = tmpCategoryKeys[i];
|
|
595
654
|
let tmpCards = tmpCategories[tmpCategoryName];
|
|
596
|
-
let
|
|
655
|
+
let tmpMatching = [];
|
|
597
656
|
|
|
598
|
-
// Filter cards within this category
|
|
599
657
|
for (let j = 0; j < tmpCards.length; j++)
|
|
600
658
|
{
|
|
601
659
|
let tmpCardConfig = tmpCards[j];
|
|
@@ -606,413 +664,916 @@ class PictViewFlowToolbar extends libPictView
|
|
|
606
664
|
let tmpLabel = (tmpCardConfig.Label || '').toLowerCase();
|
|
607
665
|
let tmpCode = (tmpMeta.Code || '').toLowerCase();
|
|
608
666
|
let tmpCategory = tmpCategoryName.toLowerCase();
|
|
609
|
-
if (tmpLabel.indexOf(tmpFilter) < 0 &&
|
|
610
|
-
tmpCode.indexOf(tmpFilter) < 0 &&
|
|
611
|
-
tmpCategory.indexOf(tmpFilter) < 0)
|
|
612
|
-
{
|
|
613
|
-
continue;
|
|
614
|
-
}
|
|
667
|
+
if (tmpLabel.indexOf(tmpFilter) < 0 && tmpCode.indexOf(tmpFilter) < 0 && tmpCategory.indexOf(tmpFilter) < 0) continue;
|
|
615
668
|
}
|
|
616
669
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
if (tmpMatchingCards.length === 0) continue;
|
|
621
|
-
|
|
622
|
-
tmpTotalMatchCount += tmpMatchingCards.length;
|
|
623
|
-
|
|
624
|
-
let tmpCategoryDiv = document.createElement('div');
|
|
625
|
-
tmpCategoryDiv.className = 'pict-flow-palette-category';
|
|
626
|
-
tmpCategoryDiv.style.padding = '0.35em 0.5em';
|
|
627
|
-
|
|
628
|
-
let tmpCategoryLabel = document.createElement('div');
|
|
629
|
-
tmpCategoryLabel.className = 'pict-flow-palette-category-label';
|
|
630
|
-
tmpCategoryLabel.textContent = tmpCategoryName;
|
|
631
|
-
tmpCategoryDiv.appendChild(tmpCategoryLabel);
|
|
632
|
-
|
|
633
|
-
let tmpCardsDiv = document.createElement('div');
|
|
634
|
-
tmpCardsDiv.className = 'pict-flow-palette-cards';
|
|
635
|
-
|
|
636
|
-
for (let j = 0; j < tmpMatchingCards.length; j++)
|
|
637
|
-
{
|
|
638
|
-
let tmpCardConfig = tmpMatchingCards[j];
|
|
639
|
-
let tmpMeta = tmpCardConfig.CardMetadata || {};
|
|
640
|
-
|
|
641
|
-
let tmpCardEl = document.createElement('div');
|
|
642
|
-
tmpCardEl.className = 'pict-flow-palette-card';
|
|
643
|
-
if (tmpMeta.Enabled === false)
|
|
644
|
-
{
|
|
645
|
-
tmpCardEl.classList.add('disabled');
|
|
646
|
-
}
|
|
647
|
-
tmpCardEl.setAttribute('data-card-type', tmpCardConfig.Hash);
|
|
648
|
-
|
|
649
|
-
if (tmpMeta.Tooltip)
|
|
650
|
-
{
|
|
651
|
-
tmpCardEl.setAttribute('title', tmpMeta.Tooltip);
|
|
652
|
-
}
|
|
653
|
-
else if (tmpMeta.Description)
|
|
654
|
-
{
|
|
655
|
-
tmpCardEl.setAttribute('title', tmpMeta.Description);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// Icon or color swatch
|
|
659
|
-
if (tmpMeta.Icon)
|
|
660
|
-
{
|
|
661
|
-
let tmpIconSpan = document.createElement('span');
|
|
662
|
-
tmpIconSpan.className = 'pict-flow-palette-card-icon';
|
|
663
|
-
let tmpIconProvider = this._FlowView._IconProvider;
|
|
664
|
-
if (tmpIconProvider && !tmpIconProvider.isEmojiIcon(tmpMeta.Icon))
|
|
665
|
-
{
|
|
666
|
-
let tmpResolvedKey = tmpIconProvider.resolveIconKey(tmpMeta);
|
|
667
|
-
tmpIconSpan.innerHTML = tmpIconProvider.getIconSVGMarkup(tmpResolvedKey, 14);
|
|
668
|
-
}
|
|
669
|
-
else
|
|
670
|
-
{
|
|
671
|
-
tmpIconSpan.textContent = tmpMeta.Icon;
|
|
672
|
-
}
|
|
673
|
-
tmpCardEl.appendChild(tmpIconSpan);
|
|
674
|
-
}
|
|
675
|
-
else if (this._FlowView._IconProvider)
|
|
670
|
+
let tmpIconHTML = '';
|
|
671
|
+
let tmpIsEmoji = false;
|
|
672
|
+
if (tmpMeta.Icon && tmpIconProvider && !tmpIconProvider.isEmojiIcon(tmpMeta.Icon))
|
|
676
673
|
{
|
|
677
|
-
|
|
678
|
-
tmpIconSpan.className = 'pict-flow-palette-card-icon';
|
|
679
|
-
tmpIconSpan.innerHTML = this._FlowView._IconProvider.getIconSVGMarkup('default', 14);
|
|
680
|
-
tmpCardEl.appendChild(tmpIconSpan);
|
|
674
|
+
tmpIconHTML = tmpIconProvider.getIconSVGMarkup(tmpIconProvider.resolveIconKey(tmpMeta), 14);
|
|
681
675
|
}
|
|
682
|
-
else if (
|
|
676
|
+
else if (tmpMeta.Icon)
|
|
683
677
|
{
|
|
684
|
-
|
|
685
|
-
tmpSwatch.className = 'pict-flow-palette-card-swatch';
|
|
686
|
-
tmpSwatch.style.backgroundColor = tmpCardConfig.TitleBarColor;
|
|
687
|
-
tmpCardEl.appendChild(tmpSwatch);
|
|
678
|
+
tmpIsEmoji = true;
|
|
688
679
|
}
|
|
689
|
-
|
|
690
|
-
// Title
|
|
691
|
-
let tmpTitleSpan = document.createElement('span');
|
|
692
|
-
tmpTitleSpan.className = 'pict-flow-palette-card-title';
|
|
693
|
-
tmpTitleSpan.textContent = tmpCardConfig.Label;
|
|
694
|
-
tmpCardEl.appendChild(tmpTitleSpan);
|
|
695
|
-
|
|
696
|
-
// Code badge
|
|
697
|
-
if (tmpMeta.Code)
|
|
680
|
+
else if (tmpIconProvider)
|
|
698
681
|
{
|
|
699
|
-
|
|
700
|
-
tmpCodeSpan.className = 'pict-flow-palette-card-code';
|
|
701
|
-
tmpCodeSpan.textContent = tmpMeta.Code;
|
|
702
|
-
tmpCardEl.appendChild(tmpCodeSpan);
|
|
682
|
+
tmpIconHTML = tmpIconProvider.getIconSVGMarkup('default', 14);
|
|
703
683
|
}
|
|
704
684
|
|
|
705
|
-
|
|
706
|
-
tmpCardEl.addEventListener('click', () =>
|
|
685
|
+
tmpMatching.push(
|
|
707
686
|
{
|
|
708
|
-
|
|
709
|
-
|
|
687
|
+
CardType: tmpCardConfig.Hash,
|
|
688
|
+
Label: tmpCardConfig.Label || '',
|
|
689
|
+
Code: tmpMeta.Code || '',
|
|
690
|
+
IconHTML: tmpIconHTML,
|
|
691
|
+
IconEmoji: tmpIsEmoji ? tmpMeta.Icon : '',
|
|
692
|
+
DisabledClass: (tmpMeta.Enabled === false) ? ' disabled' : '',
|
|
693
|
+
Tooltip: tmpMeta.Tooltip || tmpMeta.Description || '',
|
|
694
|
+
SwatchColor: (!tmpIconHTML && !tmpIsEmoji && tmpCardConfig.TitleBarColor) ? tmpCardConfig.TitleBarColor : '',
|
|
695
|
+
FlowViewIdentifier: tmpFlowViewIdentifier
|
|
710
696
|
});
|
|
711
|
-
|
|
712
|
-
tmpCardsDiv.appendChild(tmpCardEl);
|
|
713
697
|
}
|
|
714
698
|
|
|
715
|
-
|
|
716
|
-
|
|
699
|
+
if (tmpMatching.length === 0) continue;
|
|
700
|
+
tmpTotalMatchCount += tmpMatching.length;
|
|
701
|
+
tmpCategoryRecords.push(
|
|
702
|
+
{
|
|
703
|
+
Name: tmpCategoryName,
|
|
704
|
+
Cards: tmpMatching,
|
|
705
|
+
FlowViewIdentifier: tmpFlowViewIdentifier
|
|
706
|
+
});
|
|
717
707
|
}
|
|
718
708
|
|
|
719
709
|
if (tmpTotalMatchCount === 0)
|
|
720
710
|
{
|
|
721
|
-
|
|
722
|
-
tmpEmpty.className = 'pict-flow-popup-list-empty';
|
|
723
|
-
tmpEmpty.textContent = tmpFilter ? 'No matching cards' : 'No card types available';
|
|
724
|
-
pContainer.appendChild(tmpEmpty);
|
|
711
|
+
return '<div class="pict-flow-popup-list-empty">' + (tmpFilter ? 'No matching cards' : 'No card types available') + '</div>';
|
|
725
712
|
}
|
|
713
|
+
return this.pict.parseTemplateByHash('Flow-Cards-List', { Categories: tmpCategoryRecords });
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Inline `oninput` handler — refresh card list with the new filter.
|
|
718
|
+
*/
|
|
719
|
+
_filterCardsList(pListID, pFilter)
|
|
720
|
+
{
|
|
721
|
+
let tmpListEl = document.getElementById(pListID);
|
|
722
|
+
if (!tmpListEl) return;
|
|
723
|
+
this.pict.ContentAssignment.assignContent(tmpListEl, this._renderCardsListHTML(pFilter));
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Inline handler for a card click. Adds the node and closes the popup.
|
|
728
|
+
*
|
|
729
|
+
* @param {string} pCardType
|
|
730
|
+
*/
|
|
731
|
+
_addCardFromPopup(pCardType)
|
|
732
|
+
{
|
|
733
|
+
this._addCardFromPalette(pCardType);
|
|
734
|
+
this._closePopup();
|
|
726
735
|
}
|
|
727
736
|
|
|
728
737
|
// ── Layout Popup ──────────────────────────────────────────────────────
|
|
729
738
|
|
|
730
739
|
/**
|
|
731
|
-
* Build the Layout popup
|
|
740
|
+
* Build the Layout popup using templates with inline handlers.
|
|
741
|
+
*
|
|
742
|
+
* Layout records (name + hash) are pushed into AppData and iterated via
|
|
743
|
+
* a {~TS:~} template. The save section is a static fragment with inline
|
|
744
|
+
* handlers that call the toolbar's helper methods.
|
|
745
|
+
*
|
|
732
746
|
* @param {HTMLElement} pContainer
|
|
733
747
|
*/
|
|
734
748
|
_buildLayoutPopup(pContainer)
|
|
735
749
|
{
|
|
750
|
+
let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
736
751
|
let tmpIconProvider = this._FlowView ? this._FlowView._IconProvider : null;
|
|
752
|
+
let tmpSaveIconHTML = tmpIconProvider ? tmpIconProvider.getIconSVGMarkup('save', 14) : '✓';
|
|
753
|
+
let tmpDeleteIconHTML = tmpIconProvider ? tmpIconProvider.getIconSVGMarkup('trash', 12) : '×';
|
|
754
|
+
|
|
755
|
+
let tmpSaveRowID = `Flow-Toolbar-LayoutSaveRow-${tmpFlowViewIdentifier}`;
|
|
756
|
+
let tmpSaveInputRowID = `Flow-Toolbar-LayoutSaveInputRow-${tmpFlowViewIdentifier}`;
|
|
757
|
+
let tmpSaveInputID = `Flow-Toolbar-LayoutSaveInput-${tmpFlowViewIdentifier}`;
|
|
758
|
+
let tmpListID = `Flow-Toolbar-LayoutList-${tmpFlowViewIdentifier}`;
|
|
759
|
+
|
|
760
|
+
// Build layout records for the {~TS:~} iteration
|
|
761
|
+
let tmpLayouts = (this._FlowView && this._FlowView._LayoutProvider)
|
|
762
|
+
? this._FlowView._LayoutProvider.getLayouts() : [];
|
|
763
|
+
let tmpLayoutRecords = [];
|
|
764
|
+
for (let i = 0; i < tmpLayouts.length; i++)
|
|
765
|
+
{
|
|
766
|
+
tmpLayoutRecords.push(
|
|
767
|
+
{
|
|
768
|
+
LayoutHash: tmpLayouts[i].Hash,
|
|
769
|
+
Name: tmpLayouts[i].Name || '',
|
|
770
|
+
DeleteIconHTML: tmpDeleteIconHTML,
|
|
771
|
+
FlowViewIdentifier: tmpFlowViewIdentifier
|
|
772
|
+
});
|
|
773
|
+
}
|
|
737
774
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
775
|
+
let tmpListHTML = (tmpLayoutRecords.length === 0)
|
|
776
|
+
? '<div class="pict-flow-popup-list-empty">No saved layouts</div>'
|
|
777
|
+
: this.pict.parseTemplateByHash('Flow-Layout-List', { Rows: tmpLayoutRecords });
|
|
778
|
+
|
|
779
|
+
this.pict.ContentAssignment.assignContent(pContainer,
|
|
780
|
+
'<div class="pict-flow-popup-layout-save-section">'
|
|
781
|
+
+ '<div id="' + tmpSaveRowID + '" class="pict-flow-popup-layout-save"'
|
|
782
|
+
+ ' onclick="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._showLayoutSaveInput(\'' + tmpSaveRowID + '\', \'' + tmpSaveInputRowID + '\', \'' + tmpSaveInputID + '\')">'
|
|
783
|
+
+ '<span class="pict-flow-popup-layout-save-icon">' + tmpSaveIconHTML + '</span>'
|
|
784
|
+
+ '<span>Save Current Layout</span>'
|
|
785
|
+
+ '</div>'
|
|
786
|
+
+ '<div id="' + tmpSaveInputRowID + '" class="pict-flow-popup-layout-save-input-row" style="display:none;">'
|
|
787
|
+
+ '<input id="' + tmpSaveInputID + '" class="pict-flow-popup-layout-save-input" type="text" placeholder="Layout name..."'
|
|
788
|
+
+ ' onclick="event.stopPropagation()"'
|
|
789
|
+
+ ' onkeydown="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._handleLayoutSaveKey(event, \'' + tmpSaveInputID + '\', \'' + tmpSaveRowID + '\', \'' + tmpSaveInputRowID + '\')" />'
|
|
790
|
+
+ '<button class="pict-flow-popup-layout-save-confirm" title="Save"'
|
|
791
|
+
+ ' onclick="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._confirmLayoutSave(\'' + tmpSaveInputID + '\')">'
|
|
792
|
+
+ tmpSaveIconHTML + '</button>'
|
|
793
|
+
+ '</div>'
|
|
794
|
+
+ '</div>'
|
|
795
|
+
+ '<div class="pict-flow-popup-divider"></div>'
|
|
796
|
+
+ '<div id="' + tmpListID + '">' + tmpListHTML + '</div>');
|
|
797
|
+
}
|
|
746
798
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
799
|
+
/**
|
|
800
|
+
* Inline handler — reveal the layout-name input and focus it.
|
|
801
|
+
*/
|
|
802
|
+
_showLayoutSaveInput(pSaveRowID, pInputRowID, pInputID)
|
|
803
|
+
{
|
|
804
|
+
let tmpRow = document.getElementById(pSaveRowID);
|
|
805
|
+
let tmpInputRow = document.getElementById(pInputRowID);
|
|
806
|
+
let tmpInput = document.getElementById(pInputID);
|
|
807
|
+
if (tmpRow) tmpRow.style.display = 'none';
|
|
808
|
+
if (tmpInputRow) tmpInputRow.style.display = '';
|
|
809
|
+
if (tmpInput)
|
|
810
|
+
{
|
|
811
|
+
tmpInput.value = '';
|
|
812
|
+
setTimeout(() => { tmpInput.focus(); }, 50);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
752
815
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
816
|
+
/**
|
|
817
|
+
* Inline handler — Enter saves, Escape cancels.
|
|
818
|
+
*/
|
|
819
|
+
_handleLayoutSaveKey(pEvent, pInputID, pSaveRowID, pInputRowID)
|
|
820
|
+
{
|
|
821
|
+
if (pEvent.key === 'Enter')
|
|
757
822
|
{
|
|
758
|
-
|
|
823
|
+
pEvent.preventDefault();
|
|
824
|
+
this._confirmLayoutSave(pInputID);
|
|
759
825
|
}
|
|
760
|
-
else
|
|
826
|
+
else if (pEvent.key === 'Escape')
|
|
761
827
|
{
|
|
762
|
-
|
|
828
|
+
let tmpRow = document.getElementById(pSaveRowID);
|
|
829
|
+
let tmpInputRow = document.getElementById(pInputRowID);
|
|
830
|
+
if (tmpInputRow) tmpInputRow.style.display = 'none';
|
|
831
|
+
if (tmpRow) tmpRow.style.display = '';
|
|
763
832
|
}
|
|
764
|
-
|
|
833
|
+
}
|
|
765
834
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
835
|
+
/**
|
|
836
|
+
* Inline handler — save the layout under the entered name and rebuild
|
|
837
|
+
* the popup so the new entry appears.
|
|
838
|
+
*/
|
|
839
|
+
_confirmLayoutSave(pInputID)
|
|
840
|
+
{
|
|
841
|
+
let tmpInput = document.getElementById(pInputID);
|
|
842
|
+
if (!tmpInput) return;
|
|
843
|
+
let tmpName = (tmpInput.value || '').trim();
|
|
844
|
+
if (tmpName === '') return;
|
|
845
|
+
this._FlowView._LayoutProvider.saveLayout(tmpName);
|
|
769
846
|
|
|
770
|
-
let
|
|
771
|
-
|
|
772
|
-
if (
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
847
|
+
let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
848
|
+
let tmpPopupEl = document.getElementById(`Flow-Toolbar-Popup-${tmpFlowViewIdentifier}`);
|
|
849
|
+
if (tmpPopupEl) this._buildLayoutPopup(tmpPopupEl);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Inline handler — restore layout when the row (not its delete button)
|
|
854
|
+
* is clicked.
|
|
855
|
+
*/
|
|
856
|
+
_restoreLayoutFromPopup(pLayoutHash, pEvent)
|
|
857
|
+
{
|
|
858
|
+
if (pEvent && pEvent.target && pEvent.target.closest('.pict-flow-popup-layout-delete')) return;
|
|
859
|
+
this._FlowView._LayoutProvider.restoreLayout(pLayoutHash);
|
|
860
|
+
this._closePopup();
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Inline handler — delete a saved layout and refresh the popup.
|
|
865
|
+
*/
|
|
866
|
+
_deleteLayoutFromPopup(pLayoutHash, pEvent)
|
|
867
|
+
{
|
|
868
|
+
if (pEvent && typeof pEvent.stopPropagation === 'function') pEvent.stopPropagation();
|
|
869
|
+
this._FlowView._LayoutProvider.deleteLayout(pLayoutHash);
|
|
870
|
+
|
|
871
|
+
let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
872
|
+
let tmpPopupEl = document.getElementById(`Flow-Toolbar-Popup-${tmpFlowViewIdentifier}`);
|
|
873
|
+
if (tmpPopupEl) this._buildLayoutPopup(tmpPopupEl);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// ── Layout Algorithm Popup ────────────────────────────────────────────
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Build the Layout Algorithm popup using templates with inline handlers.
|
|
880
|
+
* The popup has three logical sections (algorithm chooser, edge-theme
|
|
881
|
+
* chooser, auto-apply toggle + apply button). Each control's behavior
|
|
882
|
+
* lives on the toolbar as a helper method called from inline handlers.
|
|
883
|
+
*
|
|
884
|
+
* Iterations (algorithm options, edge themes) are template-driven via
|
|
885
|
+
* AppData arrays.
|
|
886
|
+
*
|
|
887
|
+
* @param {HTMLElement} pContainer
|
|
888
|
+
*/
|
|
889
|
+
_buildLayoutAlgorithmPopup(pContainer)
|
|
890
|
+
{
|
|
891
|
+
if (!this._FlowView || !this._FlowView._LayoutService) return;
|
|
777
892
|
|
|
778
|
-
let
|
|
779
|
-
|
|
780
|
-
|
|
893
|
+
let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
894
|
+
let tmpLayoutService = this._FlowView._LayoutService;
|
|
895
|
+
let tmpCurrentSettings = this._FlowView.getLayoutAlgorithm();
|
|
896
|
+
let tmpAlgoDescriptor = tmpLayoutService.getAlgorithm(tmpCurrentSettings.Algorithm);
|
|
897
|
+
let tmpEdgeSettings = this._FlowView.getEdgeTheme();
|
|
898
|
+
let tmpResolvedEdge = tmpEdgeSettings.Theme;
|
|
899
|
+
let tmpExplicitEdgeOverride = tmpEdgeSettings.Override;
|
|
900
|
+
let tmpDefaultThemeName = (tmpAlgoDescriptor && tmpAlgoDescriptor.DefaultEdgeTheme) || 'Bezier';
|
|
901
|
+
let tmpIconProvider = this._FlowView ? this._FlowView._IconProvider : null;
|
|
902
|
+
let tmpSettingsIconHTML = tmpIconProvider ? tmpIconProvider.getIconSVGMarkup('settings', 13) : '⚙';
|
|
781
903
|
|
|
782
|
-
//
|
|
783
|
-
|
|
904
|
+
// Build option records for the dropdowns
|
|
905
|
+
let tmpAlgoOptions = tmpLayoutService.listAlgorithms().map((pAlgo) => (
|
|
784
906
|
{
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
});
|
|
907
|
+
Value: pAlgo.Name,
|
|
908
|
+
Label: pAlgo.Label || pAlgo.Name,
|
|
909
|
+
SelectedAttr: (pAlgo.Name === tmpCurrentSettings.Algorithm) ? ' selected="selected"' : ''
|
|
910
|
+
}));
|
|
790
911
|
|
|
791
|
-
|
|
792
|
-
|
|
912
|
+
let tmpEdgeOptionRecords = [];
|
|
913
|
+
tmpEdgeOptionRecords.push(
|
|
793
914
|
{
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
915
|
+
Value: '__inherit__',
|
|
916
|
+
Label: `Inherit from layout (${tmpDefaultThemeName})`,
|
|
917
|
+
SelectedAttr: !tmpExplicitEdgeOverride ? ' selected="selected"' : ''
|
|
918
|
+
});
|
|
919
|
+
let tmpEdgeThemes = tmpLayoutService.listEdgeThemes();
|
|
920
|
+
for (let i = 0; i < tmpEdgeThemes.length; i++)
|
|
921
|
+
{
|
|
922
|
+
tmpEdgeOptionRecords.push(
|
|
799
923
|
{
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
924
|
+
Value: tmpEdgeThemes[i].Name,
|
|
925
|
+
Label: tmpEdgeThemes[i].Label || tmpEdgeThemes[i].Name,
|
|
926
|
+
SelectedAttr: (tmpEdgeThemes[i].Name === tmpExplicitEdgeOverride) ? ' selected="selected"' : ''
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
let tmpAlgoOptionsHTML = this.pict.parseTemplateByHash('Flow-Layout-OptionList', { Options: tmpAlgoOptions });
|
|
931
|
+
let tmpEdgeOptionsHTML = this.pict.parseTemplateByHash('Flow-Layout-OptionList', { Options: tmpEdgeOptionRecords });
|
|
804
932
|
|
|
805
|
-
|
|
933
|
+
let tmpHasParameters = !!(tmpAlgoDescriptor && (
|
|
934
|
+
(tmpAlgoDescriptor.ParameterManifest && tmpAlgoDescriptor.ParameterManifest.Descriptors) ||
|
|
935
|
+
(tmpAlgoDescriptor.ParameterSchema && Object.keys(tmpAlgoDescriptor.ParameterSchema).length > 0)
|
|
936
|
+
));
|
|
806
937
|
|
|
807
|
-
|
|
808
|
-
|
|
938
|
+
let tmpAlgoDescription = (tmpAlgoDescriptor && tmpAlgoDescriptor.Description)
|
|
939
|
+
? '<div class="pict-flow-popup-control-description">' + tmpAlgoDescriptor.Description + '</div>'
|
|
940
|
+
: '';
|
|
941
|
+
|
|
942
|
+
let tmpFormToggleHTML = '';
|
|
943
|
+
if (tmpHasParameters)
|
|
809
944
|
{
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
945
|
+
tmpFormToggleHTML = '<button type="button" class="pict-flow-popup-collapse-toggle"'
|
|
946
|
+
+ ' aria-expanded="' + (this._LayoutFormExpanded ? 'true' : 'false') + '"'
|
|
947
|
+
+ ' title="' + (this._LayoutFormExpanded ? 'Hide parameters' : 'Show parameters') + '"'
|
|
948
|
+
+ ' onclick="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._toggleLayoutFormCollapsed(this)">'
|
|
949
|
+
+ tmpSettingsIconHTML
|
|
950
|
+
+ '</button>';
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
let tmpAutoApplyID = `Flow-Toolbar-AutoApply-${tmpFlowViewIdentifier}`;
|
|
954
|
+
let tmpApplyDisabled = (tmpCurrentSettings.Algorithm === 'Custom') ? ' disabled' : '';
|
|
955
|
+
let tmpApplyTitle = (tmpCurrentSettings.Algorithm === 'Custom') ? ' title="Custom does not auto-position nodes"' : '';
|
|
956
|
+
|
|
957
|
+
let tmpEdgeDescriptionHTML = '';
|
|
958
|
+
if (tmpResolvedEdge)
|
|
959
|
+
{
|
|
960
|
+
let tmpResolvedDescriptor = tmpLayoutService.getEdgeTheme(tmpResolvedEdge);
|
|
961
|
+
if (tmpResolvedDescriptor && tmpResolvedDescriptor.Description)
|
|
816
962
|
{
|
|
817
|
-
|
|
818
|
-
tmpSaveInputRow.style.display = 'none';
|
|
819
|
-
tmpSaveRow.style.display = '';
|
|
963
|
+
tmpEdgeDescriptionHTML = '<div class="pict-flow-popup-control-description">' + tmpResolvedDescriptor.Description + '</div>';
|
|
820
964
|
}
|
|
821
|
-
}
|
|
965
|
+
}
|
|
822
966
|
|
|
823
|
-
//
|
|
824
|
-
|
|
967
|
+
// Place a host div for the parameter form; mount the form into it
|
|
968
|
+
// after the popup is in the DOM (the metacontroller resolves
|
|
969
|
+
// destinations via document.querySelector).
|
|
970
|
+
let tmpParamFormHostID = `Flow-Toolbar-LayoutParamFormHost-${tmpFlowViewIdentifier}`;
|
|
971
|
+
|
|
972
|
+
this.pict.ContentAssignment.assignContent(pContainer,
|
|
973
|
+
'<div class="pict-flow-popup-settings-section pict-flow-popup-layout-algorithm-row">'
|
|
974
|
+
+ '<label class="pict-flow-popup-settings-label">Algorithm</label>'
|
|
975
|
+
+ '<div class="pict-flow-popup-layout-algorithm-controls">'
|
|
976
|
+
+ '<select class="pict-flow-popup-settings-select pict-flow-popup-layout-algorithm-select" data-layout-control="algorithm"'
|
|
977
|
+
+ ' onclick="event.stopPropagation()"'
|
|
978
|
+
+ ' onchange="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._handleLayoutAlgoChange(this.value)">'
|
|
979
|
+
+ tmpAlgoOptionsHTML
|
|
980
|
+
+ '</select>'
|
|
981
|
+
+ tmpFormToggleHTML
|
|
982
|
+
+ '</div>'
|
|
983
|
+
+ '</div>'
|
|
984
|
+
+ tmpAlgoDescription
|
|
985
|
+
+ '<div id="' + tmpParamFormHostID + '"></div>'
|
|
986
|
+
+ '<div class="pict-flow-popup-divider"></div>'
|
|
987
|
+
+ '<div class="pict-flow-popup-settings-section">'
|
|
988
|
+
+ '<label class="pict-flow-popup-settings-label">Edge theme</label>'
|
|
989
|
+
+ '<select class="pict-flow-popup-settings-select" data-layout-control="edge-theme"'
|
|
990
|
+
+ ' onclick="event.stopPropagation()"'
|
|
991
|
+
+ ' onchange="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._handleEdgeThemeChange(this.value)">'
|
|
992
|
+
+ tmpEdgeOptionsHTML
|
|
993
|
+
+ '</select>'
|
|
994
|
+
+ '</div>'
|
|
995
|
+
+ tmpEdgeDescriptionHTML
|
|
996
|
+
+ '<div class="pict-flow-popup-divider"></div>'
|
|
997
|
+
+ '<div class="pict-flow-popup-settings-section" style="display:flex;align-items:center;gap:8px;">'
|
|
998
|
+
+ '<input type="checkbox" id="' + tmpAutoApplyID + '"' + (tmpCurrentSettings.AutoApply ? ' checked="checked"' : '') + ''
|
|
999
|
+
+ ' onclick="event.stopPropagation()"'
|
|
1000
|
+
+ ' onchange="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._handleAutoApplyChange(this.checked)" />'
|
|
1001
|
+
+ '<label class="pict-flow-popup-settings-label" for="' + tmpAutoApplyID + '" style="cursor:pointer;">Auto-apply on changes</label>'
|
|
1002
|
+
+ '</div>'
|
|
1003
|
+
+ '<div class="pict-flow-popup-divider"></div>'
|
|
1004
|
+
+ '<div class="pict-flow-popup-settings-section" style="padding:4px 8px;">'
|
|
1005
|
+
+ '<button class="pict-flow-popup-layout-save-confirm" style="width:100%;padding:6px;"' + tmpApplyDisabled + tmpApplyTitle
|
|
1006
|
+
+ ' onclick="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._handleApplyLayoutNow()">Apply Now</button>'
|
|
1007
|
+
+ '</div>');
|
|
1008
|
+
|
|
1009
|
+
// Mount the parameter form (handles its own DOM building /
|
|
1010
|
+
// metacontroller). Use pContainer.querySelector — the popup may
|
|
1011
|
+
// not be in the document yet, so document.getElementById would miss.
|
|
1012
|
+
let tmpFormHostEl = pContainer.querySelector('#' + tmpParamFormHostID);
|
|
1013
|
+
if (tmpFormHostEl)
|
|
825
1014
|
{
|
|
826
|
-
|
|
827
|
-
}
|
|
1015
|
+
this._buildLayoutParameterFormSection(tmpFormHostEl, tmpAlgoDescriptor, tmpCurrentSettings);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
828
1018
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1019
|
+
/**
|
|
1020
|
+
* Inline handler — switch layout algorithm, refresh popup.
|
|
1021
|
+
*/
|
|
1022
|
+
_handleLayoutAlgoChange(pName)
|
|
1023
|
+
{
|
|
1024
|
+
if (!this._FlowView || !this._FlowView._LayoutService) return;
|
|
1025
|
+
let tmpAlgo = this._FlowView._LayoutService.getAlgorithm(pName);
|
|
1026
|
+
let tmpDefaults = (tmpAlgo && tmpAlgo.DefaultParameters)
|
|
1027
|
+
? JSON.parse(JSON.stringify(tmpAlgo.DefaultParameters)) : {};
|
|
1028
|
+
this._FlowView.setLayoutAlgorithm(pName, tmpDefaults);
|
|
832
1029
|
|
|
833
|
-
|
|
834
|
-
let
|
|
835
|
-
|
|
836
|
-
|
|
1030
|
+
let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
1031
|
+
let tmpPopupEl = document.getElementById(`Flow-Toolbar-Popup-${tmpFlowViewIdentifier}`);
|
|
1032
|
+
if (tmpPopupEl) this._buildLayoutAlgorithmPopup(tmpPopupEl);
|
|
1033
|
+
}
|
|
837
1034
|
|
|
838
|
-
|
|
839
|
-
|
|
1035
|
+
/**
|
|
1036
|
+
* Inline handler — switch edge theme, refresh popup so descriptions
|
|
1037
|
+
* track the new theme.
|
|
1038
|
+
*/
|
|
1039
|
+
_handleEdgeThemeChange(pValue)
|
|
1040
|
+
{
|
|
1041
|
+
if (!this._FlowView) return;
|
|
1042
|
+
if (pValue === '__inherit__')
|
|
840
1043
|
{
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1044
|
+
this._FlowView.setEdgeTheme(null);
|
|
1045
|
+
}
|
|
1046
|
+
else
|
|
1047
|
+
{
|
|
1048
|
+
this._FlowView.setEdgeTheme(pValue);
|
|
846
1049
|
}
|
|
847
1050
|
|
|
848
|
-
let
|
|
1051
|
+
let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
1052
|
+
let tmpPopupEl = document.getElementById(`Flow-Toolbar-Popup-${tmpFlowViewIdentifier}`);
|
|
1053
|
+
if (tmpPopupEl) this._buildLayoutAlgorithmPopup(tmpPopupEl);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* Inline handler — toggle auto-apply.
|
|
1058
|
+
*/
|
|
1059
|
+
_handleAutoApplyChange(pChecked)
|
|
1060
|
+
{
|
|
1061
|
+
if (!this._FlowView) return;
|
|
1062
|
+
this._FlowView.setLayoutAutoApply(pChecked);
|
|
1063
|
+
}
|
|
849
1064
|
|
|
850
|
-
|
|
1065
|
+
/**
|
|
1066
|
+
* Inline handler — fire the layout (when not Custom).
|
|
1067
|
+
*/
|
|
1068
|
+
_handleApplyLayoutNow()
|
|
1069
|
+
{
|
|
1070
|
+
if (!this._FlowView) return;
|
|
1071
|
+
let tmpSettings = this._FlowView.getLayoutAlgorithm();
|
|
1072
|
+
if (tmpSettings.Algorithm === 'Custom') return;
|
|
1073
|
+
this._FlowView.applyCurrentLayout();
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Inline handler — toggle the parameter-form collapse state.
|
|
1078
|
+
*/
|
|
1079
|
+
_toggleLayoutFormCollapsed(pToggleButton)
|
|
1080
|
+
{
|
|
1081
|
+
this._LayoutFormExpanded = !this._LayoutFormExpanded;
|
|
1082
|
+
pToggleButton.setAttribute('aria-expanded', this._LayoutFormExpanded ? 'true' : 'false');
|
|
1083
|
+
pToggleButton.title = this._LayoutFormExpanded ? 'Hide parameters' : 'Show parameters';
|
|
1084
|
+
let tmpHost = this._LayoutFormHostID ? document.getElementById(this._LayoutFormHostID) : null;
|
|
1085
|
+
if (tmpHost)
|
|
851
1086
|
{
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1087
|
+
tmpHost.setAttribute('data-collapsed', this._LayoutFormExpanded ? 'false' : 'true');
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Render the parameter form section. When the host app has registered
|
|
1093
|
+
* `PictFormMetacontroller` and the algorithm provides a
|
|
1094
|
+
* `ParameterManifest`, we inject that manifest into a section-form so
|
|
1095
|
+
* the user gets the same Manyfest-driven UX as the rest of the Pict
|
|
1096
|
+
* ecosystem. Otherwise we fall back to schema-driven hand-rolled inputs.
|
|
1097
|
+
*
|
|
1098
|
+
* Data binding: `pict.AppData.PictFlowLayoutEditor.Parameters` is a
|
|
1099
|
+
* mirror of `_FlowData.LayoutParameters`. The form's Informary writes
|
|
1100
|
+
* directly into AppData; a `change`/`input` listener on the popup
|
|
1101
|
+
* pushes those edits back into `_FlowData.LayoutParameters` and (when
|
|
1102
|
+
* the active algorithm is non-Custom) re-applies the layout.
|
|
1103
|
+
*
|
|
1104
|
+
* @param {HTMLElement} pContainer
|
|
1105
|
+
* @param {Object} pAlgoDescriptor - the descriptor for the active algorithm
|
|
1106
|
+
* @param {{ Algorithm: string, Parameters: Object }} pCurrentSettings
|
|
1107
|
+
*/
|
|
1108
|
+
_buildLayoutParameterFormSection(pContainer, pAlgoDescriptor, pCurrentSettings)
|
|
1109
|
+
{
|
|
1110
|
+
if (!pAlgoDescriptor) return;
|
|
1111
|
+
|
|
1112
|
+
let tmpHasManifest = !!(pAlgoDescriptor.ParameterManifest && pAlgoDescriptor.ParameterManifest.Descriptors);
|
|
1113
|
+
let tmpMetacontrollerType = this._resolveMetacontrollerServiceType();
|
|
1114
|
+
|
|
1115
|
+
if (tmpHasManifest && tmpMetacontrollerType)
|
|
1116
|
+
{
|
|
1117
|
+
this._mountLayoutParameterMetacontroller(pContainer, pAlgoDescriptor, pCurrentSettings, tmpMetacontrollerType);
|
|
856
1118
|
return;
|
|
857
1119
|
}
|
|
858
1120
|
|
|
859
|
-
|
|
1121
|
+
// Fallback path — schema-driven hand-rolled inputs.
|
|
1122
|
+
let tmpSchema = (pAlgoDescriptor.ParameterSchema) ? pAlgoDescriptor.ParameterSchema : {};
|
|
1123
|
+
let tmpParamKeys = Object.keys(tmpSchema);
|
|
1124
|
+
if (tmpParamKeys.length === 0) return;
|
|
1125
|
+
|
|
1126
|
+
let tmpParamDivider = document.createElement('div');
|
|
1127
|
+
tmpParamDivider.className = 'pict-flow-popup-divider';
|
|
1128
|
+
pContainer.appendChild(tmpParamDivider);
|
|
1129
|
+
|
|
1130
|
+
let tmpParamHeader = document.createElement('div');
|
|
1131
|
+
tmpParamHeader.className = 'pict-flow-popup-settings-label';
|
|
1132
|
+
tmpParamHeader.style.fontWeight = 'bold';
|
|
1133
|
+
tmpParamHeader.style.padding = '4px 8px';
|
|
1134
|
+
tmpParamHeader.textContent = 'Parameters';
|
|
1135
|
+
pContainer.appendChild(tmpParamHeader);
|
|
1136
|
+
|
|
1137
|
+
for (let i = 0; i < tmpParamKeys.length; i++)
|
|
860
1138
|
{
|
|
861
|
-
let
|
|
1139
|
+
let tmpKey = tmpParamKeys[i];
|
|
1140
|
+
let tmpFieldSchema = tmpSchema[tmpKey];
|
|
1141
|
+
let tmpCurrentValue = (pCurrentSettings.Parameters && pCurrentSettings.Parameters.hasOwnProperty(tmpKey))
|
|
1142
|
+
? pCurrentSettings.Parameters[tmpKey]
|
|
1143
|
+
: tmpFieldSchema.Default;
|
|
862
1144
|
|
|
863
1145
|
let tmpRow = document.createElement('div');
|
|
864
|
-
tmpRow.className = 'pict-flow-popup-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1146
|
+
tmpRow.className = 'pict-flow-popup-settings-section';
|
|
1147
|
+
tmpRow.style.display = 'flex';
|
|
1148
|
+
tmpRow.style.alignItems = 'center';
|
|
1149
|
+
tmpRow.style.gap = '8px';
|
|
1150
|
+
|
|
1151
|
+
let tmpRowLabel = document.createElement('label');
|
|
1152
|
+
tmpRowLabel.className = 'pict-flow-popup-settings-label';
|
|
1153
|
+
tmpRowLabel.textContent = tmpFieldSchema.Label || tmpKey;
|
|
1154
|
+
tmpRowLabel.style.flex = '1';
|
|
1155
|
+
tmpRow.appendChild(tmpRowLabel);
|
|
1156
|
+
|
|
1157
|
+
let tmpInput = this._buildLayoutParamInput(tmpKey, tmpFieldSchema, tmpCurrentValue);
|
|
1158
|
+
tmpRow.appendChild(tmpInput);
|
|
1159
|
+
pContainer.appendChild(tmpRow);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
870
1162
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1163
|
+
/**
|
|
1164
|
+
* Remove `PictSectionForm-*` views the layout-algorithm popup has
|
|
1165
|
+
* previously registered on the host Pict. Without this, switching
|
|
1166
|
+
* algorithms accumulates dead section views — the metatemplate
|
|
1167
|
+
* generated by the next inject would still reference the old ones.
|
|
1168
|
+
* Idempotent.
|
|
1169
|
+
*/
|
|
1170
|
+
_evictLayoutFormViews()
|
|
1171
|
+
{
|
|
1172
|
+
if (!this.pict || !this.pict.views) return;
|
|
1173
|
+
let tmpKeys = Object.keys(this.pict.views);
|
|
1174
|
+
for (let i = 0; i < tmpKeys.length; i++)
|
|
1175
|
+
{
|
|
1176
|
+
let tmpKey = tmpKeys[i];
|
|
1177
|
+
if (tmpKey.indexOf('PictSectionForm-') === 0 && tmpKey.indexOf('PictFlowLayout') > 0)
|
|
876
1178
|
{
|
|
877
|
-
|
|
1179
|
+
delete this.pict.views[tmpKey];
|
|
1180
|
+
continue;
|
|
878
1181
|
}
|
|
879
|
-
|
|
1182
|
+
// Match any previously-injected layout-algorithm section by hash
|
|
1183
|
+
// suffix (manifests use unique algorithm-suffixed section names).
|
|
1184
|
+
if (tmpKey.indexOf('PictSectionForm-PFL') === 0)
|
|
880
1185
|
{
|
|
881
|
-
|
|
1186
|
+
delete this.pict.views[tmpKey];
|
|
882
1187
|
}
|
|
883
|
-
tmpRow.appendChild(tmpDeleteBtn);
|
|
884
|
-
|
|
885
|
-
// Click row → restore layout
|
|
886
|
-
tmpRow.addEventListener('click', (pEvent) =>
|
|
887
|
-
{
|
|
888
|
-
// Don't restore if they clicked the delete button
|
|
889
|
-
if (pEvent.target.closest('.pict-flow-popup-layout-delete'))
|
|
890
|
-
{
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
|
-
this._FlowView._LayoutProvider.restoreLayout(tmpLayout.Hash);
|
|
894
|
-
this._closePopup();
|
|
895
|
-
});
|
|
896
|
-
|
|
897
|
-
// Click delete → delete layout and refresh popup
|
|
898
|
-
tmpDeleteBtn.addEventListener('click', (pEvent) =>
|
|
899
|
-
{
|
|
900
|
-
pEvent.stopPropagation();
|
|
901
|
-
this._FlowView._LayoutProvider.deleteLayout(tmpLayout.Hash);
|
|
902
|
-
// Refresh the popup content
|
|
903
|
-
while (pContainer.firstChild)
|
|
904
|
-
{
|
|
905
|
-
pContainer.removeChild(pContainer.firstChild);
|
|
906
|
-
}
|
|
907
|
-
this._buildLayoutPopup(pContainer);
|
|
908
|
-
});
|
|
909
|
-
|
|
910
|
-
pContainer.appendChild(tmpRow);
|
|
911
1188
|
}
|
|
1189
|
+
this._LayoutFormMetacontroller = null;
|
|
912
1190
|
}
|
|
913
1191
|
|
|
914
|
-
|
|
1192
|
+
/**
|
|
1193
|
+
* Look up which form-metacontroller service is registered on the host
|
|
1194
|
+
* Pict instance. pict-section-form has been published under both
|
|
1195
|
+
* 'PictFormMetacontroller' and (older) 'PictViewFormMetacontroller'
|
|
1196
|
+
* names; check both, in that order.
|
|
1197
|
+
* @returns {string|null}
|
|
1198
|
+
*/
|
|
1199
|
+
_resolveMetacontrollerServiceType()
|
|
1200
|
+
{
|
|
1201
|
+
if (!this.fable || !this.fable.servicesMap) return null;
|
|
1202
|
+
if (this.fable.servicesMap.hasOwnProperty('PictFormMetacontroller')) return 'PictFormMetacontroller';
|
|
1203
|
+
if (this.fable.servicesMap.hasOwnProperty('PictViewFormMetacontroller')) return 'PictViewFormMetacontroller';
|
|
1204
|
+
return null;
|
|
1205
|
+
}
|
|
915
1206
|
|
|
916
1207
|
/**
|
|
917
|
-
* Build the
|
|
1208
|
+
* Build the parameter form section using a pict-section-form
|
|
1209
|
+
* metacontroller. Creates a host div, binds the active layout
|
|
1210
|
+
* parameters to `pict.AppData.PictFlowLayoutEditor.Parameters`, and
|
|
1211
|
+
* injects the algorithm's `ParameterManifest`. Form-input changes
|
|
1212
|
+
* propagate back to `_FlowData.LayoutParameters` via a single
|
|
1213
|
+
* `change`/`input` listener on the popup container.
|
|
1214
|
+
*
|
|
918
1215
|
* @param {HTMLElement} pContainer
|
|
1216
|
+
* @param {Object} pAlgoDescriptor
|
|
1217
|
+
* @param {{ Algorithm: string, Parameters: Object }} pCurrentSettings
|
|
1218
|
+
* @param {string} pMetacontrollerType - 'PictFormMetacontroller' or 'PictViewFormMetacontroller'
|
|
919
1219
|
*/
|
|
920
|
-
|
|
1220
|
+
_mountLayoutParameterMetacontroller(pContainer, pAlgoDescriptor, pCurrentSettings, pMetacontrollerType)
|
|
921
1221
|
{
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
let tmpThemeProvider = this._FlowView._ThemeProvider;
|
|
925
|
-
|
|
926
|
-
// Theme selector section
|
|
927
|
-
let tmpThemeSection = document.createElement('div');
|
|
928
|
-
tmpThemeSection.className = 'pict-flow-popup-settings-section';
|
|
929
|
-
|
|
930
|
-
let tmpThemeLabel = document.createElement('label');
|
|
931
|
-
tmpThemeLabel.className = 'pict-flow-popup-settings-label';
|
|
932
|
-
tmpThemeLabel.textContent = 'Theme';
|
|
933
|
-
tmpThemeSection.appendChild(tmpThemeLabel);
|
|
1222
|
+
let tmpManifest = JSON.parse(JSON.stringify(pAlgoDescriptor.ParameterManifest));
|
|
1223
|
+
let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
934
1224
|
|
|
935
|
-
|
|
936
|
-
|
|
1225
|
+
// Section header + divider
|
|
1226
|
+
let tmpDivider = document.createElement('div');
|
|
1227
|
+
tmpDivider.className = 'pict-flow-popup-divider';
|
|
1228
|
+
pContainer.appendChild(tmpDivider);
|
|
937
1229
|
|
|
938
|
-
|
|
939
|
-
|
|
1230
|
+
// Form host div — the metacontroller renders into here. Inline
|
|
1231
|
+
// onclick stops popup-close propagation; onchange/oninput drive the
|
|
1232
|
+
// _FlowData.LayoutParameters writeback (see _pushFormBack).
|
|
1233
|
+
let tmpHostID = `Flow-Toolbar-LayoutForm-${tmpFlowViewIdentifier}`;
|
|
1234
|
+
this._LayoutFormHostID = tmpHostID;
|
|
1235
|
+
|
|
1236
|
+
let tmpHostDiv = document.createElement('div');
|
|
1237
|
+
tmpHostDiv.id = tmpHostID;
|
|
1238
|
+
tmpHostDiv.className = 'pict-flow-popup-layout-form-host';
|
|
1239
|
+
tmpHostDiv.setAttribute('data-collapsed', this._LayoutFormExpanded ? 'false' : 'true');
|
|
1240
|
+
tmpHostDiv.setAttribute('onclick', 'event.stopPropagation()');
|
|
1241
|
+
tmpHostDiv.setAttribute('onchange',
|
|
1242
|
+
"_Pict.views['" + tmpFlowViewIdentifier + "']._ToolbarView._pushLayoutFormBack('" + pCurrentSettings.Algorithm + "')");
|
|
1243
|
+
tmpHostDiv.setAttribute('oninput',
|
|
1244
|
+
"_Pict.views['" + tmpFlowViewIdentifier + "']._ToolbarView._pushLayoutFormBack('" + pCurrentSettings.Algorithm + "')");
|
|
1245
|
+
pContainer.appendChild(tmpHostDiv);
|
|
1246
|
+
|
|
1247
|
+
// Bind the layout parameters as the data source for the form.
|
|
1248
|
+
// `injectManifestAndRender(manifest, _, UUID)` calls
|
|
1249
|
+
// `createDistinctManifest` which prepends the UUID to every descriptor
|
|
1250
|
+
// address (so re-injecting the same manifest doesn't collide). We use
|
|
1251
|
+
// the algorithm name as the UUID, so the form's expected address is
|
|
1252
|
+
// `AppData.<Algorithm>.PictFlowLayoutEditor.Parameters.<Key>` —
|
|
1253
|
+
// bind there, not at the unprefixed root.
|
|
1254
|
+
let tmpScope = pCurrentSettings.Algorithm;
|
|
1255
|
+
this.pict.AppData[tmpScope] = this.pict.AppData[tmpScope] || {};
|
|
1256
|
+
this.pict.AppData[tmpScope].PictFlowLayoutEditor = this.pict.AppData[tmpScope].PictFlowLayoutEditor || {};
|
|
1257
|
+
this.pict.AppData[tmpScope].PictFlowLayoutEditor.Parameters = JSON.parse(JSON.stringify(pCurrentSettings.Parameters || {}));
|
|
1258
|
+
|
|
1259
|
+
// Always recreate the metacontroller per mount. Reusing the same
|
|
1260
|
+
// metacontroller across algorithm switches accumulates section views
|
|
1261
|
+
// (each switch's manifest gets injected onto the existing roster) and
|
|
1262
|
+
// the metatemplate ends up wedging multiple sections into a single
|
|
1263
|
+
// shared destination — only the last one survives. Fresh instance per
|
|
1264
|
+
// mount keeps the DOM consistent; we also evict the prior
|
|
1265
|
+
// `PictSectionForm-*` views from `pict.views` so they GC cleanly.
|
|
1266
|
+
this._evictLayoutFormViews();
|
|
1267
|
+
try
|
|
1268
|
+
{
|
|
1269
|
+
this._LayoutFormMetacontroller = this.fable.instantiateServiceProviderWithoutRegistration(
|
|
1270
|
+
pMetacontrollerType,
|
|
1271
|
+
{
|
|
1272
|
+
ViewIdentifier: `Flow-Toolbar-LayoutForm-MC-${tmpFlowViewIdentifier}-${this.fable.getUUID()}`,
|
|
1273
|
+
DefaultDestinationAddress: `#${tmpHostID}`,
|
|
1274
|
+
AutoRender: false,
|
|
1275
|
+
AutoPopulateAfterRender: true,
|
|
1276
|
+
AutoSolveBeforeRender: false
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
catch (pError)
|
|
1280
|
+
{
|
|
1281
|
+
this.log.warn(`Failed to instantiate ${pMetacontrollerType}: ${pError.message}`);
|
|
1282
|
+
this._LayoutFormMetacontroller = null;
|
|
1283
|
+
}
|
|
940
1284
|
|
|
941
|
-
|
|
1285
|
+
if (!this._LayoutFormMetacontroller)
|
|
942
1286
|
{
|
|
943
|
-
|
|
944
|
-
|
|
1287
|
+
tmpHostDiv.innerHTML = '<em style="padding:8px;display:block;opacity:0.7;">pict-section-form not available; parameter form skipped.</em>';
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
945
1290
|
|
|
946
|
-
|
|
947
|
-
|
|
1291
|
+
// Establish the form-container div the metacontroller expects.
|
|
1292
|
+
let tmpFormContainerID = `Pict-${this._LayoutFormMetacontroller.UUID}-FormContainer`;
|
|
1293
|
+
tmpHostDiv.innerHTML = `<div id="${tmpFormContainerID}" class="pict-form pict-flow-popup-layout-form"></div>`;
|
|
948
1294
|
|
|
949
|
-
|
|
1295
|
+
try
|
|
1296
|
+
{
|
|
1297
|
+
// Use `injectManifest` + explicit per-section destination divs +
|
|
1298
|
+
// per-section render. Don't use `injectManifestAndRender` —
|
|
1299
|
+
// its metatemplate flow ends up wedging multi-section manifests
|
|
1300
|
+
// into a single shared destination (each section's render call
|
|
1301
|
+
// uses RenderMethod=replace, blowing the others away). The
|
|
1302
|
+
// explicit path gives each section its own destination div and
|
|
1303
|
+
// renders them independently.
|
|
1304
|
+
let tmpInjectFn = (typeof this._LayoutFormMetacontroller.injectManifest === 'function')
|
|
1305
|
+
? this._LayoutFormMetacontroller.injectManifest.bind(this._LayoutFormMetacontroller)
|
|
1306
|
+
: null;
|
|
1307
|
+
if (!tmpInjectFn) throw new Error('Metacontroller exposes neither injectManifest nor injectManifestAndRender');
|
|
1308
|
+
|
|
1309
|
+
// Pass the algorithm name as the section-hash discriminator so
|
|
1310
|
+
// re-injecting the same algorithm gets unique view registrations.
|
|
1311
|
+
// (createDistinctManifest does the address-prefixing too — see
|
|
1312
|
+
// the per-algorithm AppData binding above.)
|
|
1313
|
+
let tmpDistinct = (typeof this._LayoutFormMetacontroller.createDistinctManifest === 'function')
|
|
1314
|
+
? this._LayoutFormMetacontroller.createDistinctManifest(tmpManifest, pCurrentSettings.Algorithm)
|
|
1315
|
+
: tmpManifest;
|
|
1316
|
+
|
|
1317
|
+
let tmpViews = tmpInjectFn(tmpDistinct);
|
|
1318
|
+
|
|
1319
|
+
// Build a destination div per section view, in order, and render each.
|
|
1320
|
+
let tmpFormContainerEl = tmpHostDiv.querySelector(`#${tmpFormContainerID}`);
|
|
1321
|
+
if (tmpFormContainerEl)
|
|
950
1322
|
{
|
|
951
|
-
|
|
1323
|
+
let tmpInner = '';
|
|
1324
|
+
for (let i = 0; i < tmpViews.length; i++)
|
|
1325
|
+
{
|
|
1326
|
+
let tmpDest = tmpViews[i].options.DefaultDestinationAddress;
|
|
1327
|
+
if (tmpDest && tmpDest.charAt(0) === '#') tmpDest = tmpDest.substring(1);
|
|
1328
|
+
tmpInner += `<div id="${tmpDest}" class="pict-form-view"></div>`;
|
|
1329
|
+
}
|
|
1330
|
+
tmpFormContainerEl.innerHTML = tmpInner;
|
|
952
1331
|
}
|
|
953
|
-
|
|
1332
|
+
// Defer render() to the next microtask so the popup has been
|
|
1333
|
+
// appended to the DOM by `_openPopup` — pict-section-form
|
|
1334
|
+
// resolves destinations via `document.querySelector`, which
|
|
1335
|
+
// can't see detached subtrees. (Same workaround the
|
|
1336
|
+
// metacontroller's own `injectManifestAndRender` uses.)
|
|
1337
|
+
setTimeout(() =>
|
|
1338
|
+
{
|
|
1339
|
+
for (let i = 0; i < tmpViews.length; i++)
|
|
1340
|
+
{
|
|
1341
|
+
tmpViews[i].render();
|
|
1342
|
+
if (typeof tmpViews[i].marshalToView === 'function')
|
|
1343
|
+
{
|
|
1344
|
+
tmpViews[i].marshalToView();
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}, 0);
|
|
1348
|
+
}
|
|
1349
|
+
catch (pError)
|
|
1350
|
+
{
|
|
1351
|
+
this.log.warn(`PictViewFlowToolbar: layout-form injection failed: ${pError.message}`);
|
|
1352
|
+
tmpHostDiv.innerHTML = `<em style="padding:8px;display:block;opacity:0.7;">Form render error: ${pError.message}</em>`;
|
|
1353
|
+
return;
|
|
954
1354
|
}
|
|
955
1355
|
|
|
956
|
-
|
|
1356
|
+
// Form-change writeback runs through the host div's inline
|
|
1357
|
+
// onchange/oninput attributes (set above), which call
|
|
1358
|
+
// `_pushLayoutFormBack(algorithm)`.
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* Inline handler for layout-parameter form changes. Pushes Informary's
|
|
1363
|
+
* write into AppData back into `_FlowData.LayoutParameters` and
|
|
1364
|
+
* re-applies the layout (when non-Custom). De-bounces via microtask so
|
|
1365
|
+
* multi-key edits collapse to a single layout pass.
|
|
1366
|
+
*
|
|
1367
|
+
* @param {string} pAlgorithm
|
|
1368
|
+
*/
|
|
1369
|
+
_pushLayoutFormBack(pAlgorithm)
|
|
1370
|
+
{
|
|
1371
|
+
if (this._LayoutFormPushScheduled) return;
|
|
1372
|
+
this._LayoutFormPushScheduled = true;
|
|
1373
|
+
Promise.resolve().then(() =>
|
|
957
1374
|
{
|
|
958
|
-
this.
|
|
959
|
-
|
|
960
|
-
this.
|
|
1375
|
+
this._LayoutFormPushScheduled = false;
|
|
1376
|
+
if (!this._FlowView) return;
|
|
1377
|
+
let tmpScopedRoot = this.pict.AppData[pAlgorithm] || {};
|
|
1378
|
+
let tmpEditorParams = (tmpScopedRoot.PictFlowLayoutEditor || {}).Parameters || {};
|
|
1379
|
+
let tmpMerged = Object.assign({}, this._FlowView.getLayoutAlgorithm().Parameters || {}, tmpEditorParams);
|
|
1380
|
+
this._FlowView.setLayoutAlgorithm(pAlgorithm, tmpMerged);
|
|
961
1381
|
});
|
|
1382
|
+
}
|
|
962
1383
|
|
|
963
|
-
|
|
964
|
-
|
|
1384
|
+
/**
|
|
1385
|
+
* Build a single parameter input row for the layout-algorithm popup.
|
|
1386
|
+
* Wires the input's change handler to update the flow's
|
|
1387
|
+
* `LayoutParameters` and re-apply the layout if non-Custom.
|
|
1388
|
+
*
|
|
1389
|
+
* @param {string} pKey
|
|
1390
|
+
* @param {Object} pSchema - { Type: 'number' | 'string' | 'boolean' | 'enum', Default, Options? }
|
|
1391
|
+
* @param {*} pCurrentValue
|
|
1392
|
+
* @returns {HTMLElement}
|
|
1393
|
+
*/
|
|
1394
|
+
_buildLayoutParamInput(pKey, pSchema, pCurrentValue)
|
|
1395
|
+
{
|
|
1396
|
+
let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
1397
|
+
let tmpViewPath = "_Pict.views['" + tmpFlowViewIdentifier + "']._ToolbarView";
|
|
1398
|
+
let tmpKeyEsc = pKey.replace(/'/g, "\\'");
|
|
1399
|
+
let tmpInput;
|
|
965
1400
|
|
|
966
|
-
|
|
967
|
-
|
|
1401
|
+
if (pSchema.Type === 'boolean')
|
|
1402
|
+
{
|
|
1403
|
+
tmpInput = document.createElement('input');
|
|
1404
|
+
tmpInput.type = 'checkbox';
|
|
1405
|
+
tmpInput.checked = !!pCurrentValue;
|
|
1406
|
+
tmpInput.setAttribute('onchange',
|
|
1407
|
+
tmpViewPath + "._updateLayoutParameter('" + tmpKeyEsc + "', this.checked)");
|
|
1408
|
+
}
|
|
1409
|
+
else if (pSchema.Type === 'enum' && Array.isArray(pSchema.Options))
|
|
1410
|
+
{
|
|
1411
|
+
tmpInput = document.createElement('select');
|
|
1412
|
+
tmpInput.className = 'pict-flow-popup-settings-select';
|
|
1413
|
+
for (let i = 0; i < pSchema.Options.length; i++)
|
|
1414
|
+
{
|
|
1415
|
+
let tmpOpt = document.createElement('option');
|
|
1416
|
+
tmpOpt.value = pSchema.Options[i];
|
|
1417
|
+
tmpOpt.textContent = pSchema.Options[i];
|
|
1418
|
+
if (pSchema.Options[i] === pCurrentValue) tmpOpt.selected = true;
|
|
1419
|
+
tmpInput.appendChild(tmpOpt);
|
|
1420
|
+
}
|
|
1421
|
+
tmpInput.setAttribute('onchange',
|
|
1422
|
+
tmpViewPath + "._updateLayoutParameter('" + tmpKeyEsc + "', this.value)");
|
|
1423
|
+
}
|
|
1424
|
+
else if (pSchema.Type === 'number' || pSchema.Type === 'Number' || pSchema.Type === 'PreciseNumber')
|
|
1425
|
+
{
|
|
1426
|
+
tmpInput = document.createElement('input');
|
|
1427
|
+
tmpInput.type = 'number';
|
|
1428
|
+
tmpInput.className = 'pict-flow-popup-settings-input';
|
|
1429
|
+
let tmpIsPrecise = (pSchema.Type === 'PreciseNumber');
|
|
1430
|
+
if (typeof pSchema.Min === 'number') tmpInput.min = String(pSchema.Min);
|
|
1431
|
+
if (typeof pSchema.Max === 'number') tmpInput.max = String(pSchema.Max);
|
|
1432
|
+
tmpInput.step = tmpIsPrecise ? 'any' : '1';
|
|
1433
|
+
tmpInput.value = (pCurrentValue == null) ? '' : String(pCurrentValue);
|
|
1434
|
+
tmpInput.style.width = '90px';
|
|
1435
|
+
let tmpPreciseFlag = tmpIsPrecise ? 'true' : 'false';
|
|
1436
|
+
tmpInput.setAttribute('onchange',
|
|
1437
|
+
tmpViewPath + "._updateLayoutNumberParameter('" + tmpKeyEsc + "', this.value, " + tmpPreciseFlag + ")");
|
|
1438
|
+
}
|
|
1439
|
+
else
|
|
1440
|
+
{
|
|
1441
|
+
tmpInput = document.createElement('input');
|
|
1442
|
+
tmpInput.type = 'text';
|
|
1443
|
+
tmpInput.className = 'pict-flow-popup-settings-input';
|
|
1444
|
+
tmpInput.value = (pCurrentValue == null) ? '' : String(pCurrentValue);
|
|
1445
|
+
tmpInput.style.width = '90px';
|
|
1446
|
+
tmpInput.setAttribute('onchange',
|
|
1447
|
+
tmpViewPath + "._updateLayoutParameter('" + tmpKeyEsc + "', this.value)");
|
|
1448
|
+
}
|
|
968
1449
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
pContainer.appendChild(tmpDivider);
|
|
1450
|
+
tmpInput.setAttribute('onclick', 'event.stopPropagation()');
|
|
1451
|
+
return tmpInput;
|
|
1452
|
+
}
|
|
973
1453
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1454
|
+
/**
|
|
1455
|
+
* Update a single layout parameter on the flow and re-apply the layout
|
|
1456
|
+
* if the configured algorithm is not 'Custom'.
|
|
1457
|
+
* @param {string} pKey
|
|
1458
|
+
* @param {*} pValue
|
|
1459
|
+
*/
|
|
1460
|
+
_updateLayoutParameter(pKey, pValue)
|
|
1461
|
+
{
|
|
1462
|
+
if (!this._FlowView) return;
|
|
1463
|
+
let tmpSettings = this._FlowView.getLayoutAlgorithm();
|
|
1464
|
+
let tmpParams = Object.assign({}, tmpSettings.Parameters || {});
|
|
1465
|
+
tmpParams[pKey] = pValue;
|
|
1466
|
+
this._FlowView.setLayoutAlgorithm(tmpSettings.Algorithm, tmpParams);
|
|
1467
|
+
}
|
|
978
1468
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1469
|
+
/**
|
|
1470
|
+
* Inline-handler helper for numeric parameter inputs. Preserves the
|
|
1471
|
+
* raw string when PreciseNumber so big.js / ExpressionParser can use
|
|
1472
|
+
* it without float round-trip drift.
|
|
1473
|
+
*
|
|
1474
|
+
* @param {string} pKey
|
|
1475
|
+
* @param {string} pRawValue
|
|
1476
|
+
* @param {boolean} pIsPrecise
|
|
1477
|
+
*/
|
|
1478
|
+
_updateLayoutNumberParameter(pKey, pRawValue, pIsPrecise)
|
|
1479
|
+
{
|
|
1480
|
+
if (pRawValue === '') return;
|
|
1481
|
+
if (pIsPrecise)
|
|
1482
|
+
{
|
|
1483
|
+
this._updateLayoutParameter(pKey, pRawValue);
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
let tmpNum = parseFloat(pRawValue);
|
|
1487
|
+
if (isNaN(tmpNum)) return;
|
|
1488
|
+
this._updateLayoutParameter(pKey, tmpNum);
|
|
1489
|
+
}
|
|
983
1490
|
|
|
984
|
-
|
|
985
|
-
tmpNoiseRow.className = 'pict-flow-popup-settings-slider-row';
|
|
1491
|
+
// ── Settings Popup ───────────────────────────────────────────────────
|
|
986
1492
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1493
|
+
/**
|
|
1494
|
+
* Build the Settings popup using a template with inline handlers.
|
|
1495
|
+
*
|
|
1496
|
+
* Theme options are iterated via `{~TS:~}` from AppData. Sliders and
|
|
1497
|
+
* selects fire inline handlers that update the FlowView's theme state.
|
|
1498
|
+
*
|
|
1499
|
+
* @param {HTMLElement} pContainer
|
|
1500
|
+
*/
|
|
1501
|
+
_buildSettingsPopup(pContainer)
|
|
1502
|
+
{
|
|
1503
|
+
if (!this._FlowView || !this._FlowView._ThemeProvider) return;
|
|
993
1504
|
|
|
994
|
-
let
|
|
995
|
-
|
|
996
|
-
|
|
1505
|
+
let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
1506
|
+
let tmpThemeProvider = this._FlowView._ThemeProvider;
|
|
1507
|
+
let tmpThemeKeys = tmpThemeProvider.getThemeKeys();
|
|
1508
|
+
let tmpActiveKey = tmpThemeProvider.getActiveThemeKey();
|
|
997
1509
|
|
|
998
|
-
|
|
1510
|
+
let tmpThemeOptions = [];
|
|
1511
|
+
for (let i = 0; i < tmpThemeKeys.length; i++)
|
|
999
1512
|
{
|
|
1000
|
-
let
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1513
|
+
let tmpTheme = tmpThemeProvider._Themes[tmpThemeKeys[i]];
|
|
1514
|
+
tmpThemeOptions.push(
|
|
1515
|
+
{
|
|
1516
|
+
Value: tmpThemeKeys[i],
|
|
1517
|
+
Label: tmpTheme.Label || tmpThemeKeys[i],
|
|
1518
|
+
SelectedAttr: (tmpThemeKeys[i] === tmpActiveKey) ? ' selected="selected"' : ''
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1004
1521
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1522
|
+
let tmpThemeOptionsHTML = this.pict.parseTemplateByHash('Flow-Layout-OptionList', { Options: tmpThemeOptions });
|
|
1523
|
+
|
|
1524
|
+
let tmpNoiseLevel = Math.round(tmpThemeProvider.getNoiseLevel() * 100);
|
|
1525
|
+
let tmpActiveTheme = tmpThemeProvider.getActiveTheme();
|
|
1526
|
+
let tmpNoiseEnabled = !!(tmpActiveTheme && tmpActiveTheme.NoiseConfig && tmpActiveTheme.NoiseConfig.Enabled);
|
|
1527
|
+
let tmpNoiseDisplay = tmpNoiseEnabled ? '' : 'display:none;';
|
|
1528
|
+
|
|
1529
|
+
this.pict.ContentAssignment.assignContent(pContainer,
|
|
1530
|
+
'<div class="pict-flow-popup-settings-section">'
|
|
1531
|
+
+ '<label class="pict-flow-popup-settings-label">Theme</label>'
|
|
1532
|
+
+ '<select class="pict-flow-popup-settings-select"'
|
|
1533
|
+
+ ' onclick="event.stopPropagation()"'
|
|
1534
|
+
+ ' onchange="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._handleThemeSelectChange(this.value)">'
|
|
1535
|
+
+ tmpThemeOptionsHTML
|
|
1536
|
+
+ '</select>'
|
|
1537
|
+
+ '</div>'
|
|
1538
|
+
+ '<div class="pict-flow-popup-divider"></div>'
|
|
1539
|
+
+ '<div class="pict-flow-popup-settings-section pict-flow-popup-settings-noise" data-settings-type="noise" style="' + tmpNoiseDisplay + '">'
|
|
1540
|
+
+ '<label class="pict-flow-popup-settings-label">Noise</label>'
|
|
1541
|
+
+ '<div class="pict-flow-popup-settings-slider-row">'
|
|
1542
|
+
+ '<input type="range" class="pict-flow-popup-settings-slider" min="0" max="100" value="' + tmpNoiseLevel + '"'
|
|
1543
|
+
+ ' onclick="event.stopPropagation()"'
|
|
1544
|
+
+ ' onpointerdown="event.stopPropagation()"'
|
|
1545
|
+
+ ' oninput="_Pict.views[\'' + tmpFlowViewIdentifier + '\']._ToolbarView._handleNoiseSliderInput(this)" />'
|
|
1546
|
+
+ '<span class="pict-flow-popup-settings-slider-value">' + tmpNoiseLevel + '%</span>'
|
|
1547
|
+
+ '</div>'
|
|
1548
|
+
+ '</div>');
|
|
1549
|
+
}
|
|
1008
1550
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1551
|
+
/**
|
|
1552
|
+
* Inline handler — apply selected flow theme and refresh the noise
|
|
1553
|
+
* slider visibility (some themes don't expose noise).
|
|
1554
|
+
*/
|
|
1555
|
+
_handleThemeSelectChange(pThemeKey)
|
|
1556
|
+
{
|
|
1557
|
+
if (!this._FlowView) return;
|
|
1558
|
+
this._FlowView.setTheme(pThemeKey);
|
|
1013
1559
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1560
|
+
let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
1561
|
+
let tmpPopupEl = document.getElementById(`Flow-Toolbar-Popup-${tmpFlowViewIdentifier}`);
|
|
1562
|
+
if (tmpPopupEl) this._refreshNoiseSlider(tmpPopupEl);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
/**
|
|
1566
|
+
* Inline handler — push slider position into the noise level and
|
|
1567
|
+
* update the percentage label adjacent to it.
|
|
1568
|
+
*/
|
|
1569
|
+
_handleNoiseSliderInput(pSlider)
|
|
1570
|
+
{
|
|
1571
|
+
let tmpRaw = parseInt(pSlider.value, 10);
|
|
1572
|
+
if (isNaN(tmpRaw)) return;
|
|
1573
|
+
let tmpLevel = tmpRaw / 100;
|
|
1574
|
+
let tmpValueLabel = pSlider.parentNode ? pSlider.parentNode.querySelector('.pict-flow-popup-settings-slider-value') : null;
|
|
1575
|
+
if (tmpValueLabel) tmpValueLabel.textContent = tmpRaw + '%';
|
|
1576
|
+
if (this._FlowView) this._FlowView.setNoiseLevel(tmpLevel);
|
|
1016
1577
|
}
|
|
1017
1578
|
|
|
1018
1579
|
/**
|
|
@@ -1202,6 +1763,17 @@ class PictViewFlowToolbar extends libPictView
|
|
|
1202
1763
|
break;
|
|
1203
1764
|
|
|
1204
1765
|
case 'auto-layout':
|
|
1766
|
+
// Legacy alias kept for backward compatibility with any
|
|
1767
|
+
// caller still firing 'auto-layout'. The toolbar's "Auto"
|
|
1768
|
+
// button now uses 'apply-current-layout' (below).
|
|
1769
|
+
this._FlowView.autoLayout();
|
|
1770
|
+
break;
|
|
1771
|
+
|
|
1772
|
+
case 'apply-current-layout':
|
|
1773
|
+
// Respects whichever algorithm is configured in the
|
|
1774
|
+
// Algorithm popup. Falls back to Layered when the
|
|
1775
|
+
// configured value is 'Custom' or unset (autoLayout's
|
|
1776
|
+
// existing behavior — "do something useful").
|
|
1205
1777
|
this._FlowView.autoLayout();
|
|
1206
1778
|
break;
|
|
1207
1779
|
|
|
@@ -1213,6 +1785,14 @@ class PictViewFlowToolbar extends libPictView
|
|
|
1213
1785
|
this._openPopup('layout');
|
|
1214
1786
|
break;
|
|
1215
1787
|
|
|
1788
|
+
case 'layout-algorithm-popup':
|
|
1789
|
+
this._openPopup('layout-algorithm');
|
|
1790
|
+
break;
|
|
1791
|
+
|
|
1792
|
+
case 'apply-layout':
|
|
1793
|
+
this._FlowView.applyCurrentLayout();
|
|
1794
|
+
break;
|
|
1795
|
+
|
|
1216
1796
|
case 'settings-popup':
|
|
1217
1797
|
this._openPopup('settings');
|
|
1218
1798
|
break;
|