pict-section-flow 0.0.2 → 0.0.3
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/.claude/launch.json +11 -0
- package/docs/README.md +51 -0
- package/example_applications/simple_cards/source/Pict-Application-FlowExample.js +105 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Comment.js +36 -0
- package/example_applications/simple_cards/source/cards/FlowCard-DataPreview.js +42 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Each.js +1 -1
- package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +1 -1
- package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +1 -1
- package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +1 -1
- package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +1 -1
- package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +1 -1
- package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +1 -1
- package/example_applications/simple_cards/source/cards/FlowCard-Sparkline.js +98 -0
- package/example_applications/simple_cards/source/cards/FlowCard-StatusMonitor.js +44 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +1 -1
- package/example_applications/simple_cards/source/views/PictView-FlowExample-MainWorkspace.js +9 -1
- package/package.json +2 -2
- package/source/Pict-Section-Flow.js +8 -1
- package/source/PictFlowCard.js +49 -1
- package/source/providers/PictProvider-Flow-CSS.js +1440 -0
- package/source/providers/PictProvider-Flow-ConnectorShapes.js +413 -0
- package/source/providers/PictProvider-Flow-Geometry.js +43 -0
- package/source/providers/PictProvider-Flow-Icons.js +335 -0
- package/source/providers/PictProvider-Flow-Layouts.js +214 -2
- package/source/providers/PictProvider-Flow-NodeTypes.js +30 -7
- package/source/providers/PictProvider-Flow-Noise.js +241 -0
- package/source/providers/PictProvider-Flow-PanelChrome.js +19 -0
- package/source/providers/PictProvider-Flow-Theme.js +755 -0
- package/source/services/PictService-Flow-ConnectionRenderer.js +95 -32
- package/source/services/PictService-Flow-PanelManager.js +188 -0
- package/source/services/PictService-Flow-SelectionManager.js +109 -0
- package/source/services/PictService-Flow-Tether.js +52 -25
- package/source/services/PictService-Flow-ViewportManager.js +176 -0
- package/source/views/PictView-Flow-FloatingToolbar.js +352 -0
- package/source/views/PictView-Flow-Node.js +654 -169
- package/source/views/PictView-Flow-PropertiesPanel.js +176 -1
- package/source/views/PictView-Flow-Toolbar.js +846 -379
- package/source/views/PictView-Flow.js +279 -671
|
@@ -13,222 +13,73 @@ const _DefaultConfiguration =
|
|
|
13
13
|
|
|
14
14
|
EnablePalette: true,
|
|
15
15
|
|
|
16
|
-
CSS:
|
|
17
|
-
.pict-flow-toolbar {
|
|
18
|
-
display: flex;
|
|
19
|
-
align-items: center;
|
|
20
|
-
gap: 0.5em;
|
|
21
|
-
padding: 0.5em 0.75em;
|
|
22
|
-
background-color: #ffffff;
|
|
23
|
-
border-bottom: 1px solid #e0e0e0;
|
|
24
|
-
flex-wrap: wrap;
|
|
25
|
-
}
|
|
26
|
-
.pict-flow-toolbar-group {
|
|
27
|
-
display: flex;
|
|
28
|
-
align-items: center;
|
|
29
|
-
gap: 0.25em;
|
|
30
|
-
padding-right: 0.75em;
|
|
31
|
-
border-right: 1px solid #e0e0e0;
|
|
32
|
-
}
|
|
33
|
-
.pict-flow-toolbar-group:last-child {
|
|
34
|
-
border-right: none;
|
|
35
|
-
padding-right: 0;
|
|
36
|
-
}
|
|
37
|
-
.pict-flow-toolbar-btn {
|
|
38
|
-
display: inline-flex;
|
|
39
|
-
align-items: center;
|
|
40
|
-
justify-content: center;
|
|
41
|
-
padding: 0.35em 0.65em;
|
|
42
|
-
border: 1px solid #bdc3c7;
|
|
43
|
-
border-radius: 4px;
|
|
44
|
-
background-color: #fff;
|
|
45
|
-
color: #2c3e50;
|
|
46
|
-
font-size: 0.85em;
|
|
47
|
-
cursor: pointer;
|
|
48
|
-
transition: background-color 0.15s, border-color 0.15s;
|
|
49
|
-
user-select: none;
|
|
50
|
-
-webkit-user-select: none;
|
|
51
|
-
}
|
|
52
|
-
.pict-flow-toolbar-btn:hover {
|
|
53
|
-
background-color: #ecf0f1;
|
|
54
|
-
border-color: #95a5a6;
|
|
55
|
-
}
|
|
56
|
-
.pict-flow-toolbar-btn:active {
|
|
57
|
-
background-color: #d5dbdb;
|
|
58
|
-
}
|
|
59
|
-
.pict-flow-toolbar-btn.danger {
|
|
60
|
-
color: #e74c3c;
|
|
61
|
-
border-color: #e74c3c;
|
|
62
|
-
}
|
|
63
|
-
.pict-flow-toolbar-btn.danger:hover {
|
|
64
|
-
background-color: #fdedec;
|
|
65
|
-
}
|
|
66
|
-
.pict-flow-toolbar-label {
|
|
67
|
-
font-size: 0.8em;
|
|
68
|
-
color: #7f8c8d;
|
|
69
|
-
margin-right: 0.25em;
|
|
70
|
-
}
|
|
71
|
-
.pict-flow-toolbar-select {
|
|
72
|
-
padding: 0.3em 0.5em;
|
|
73
|
-
border: 1px solid #bdc3c7;
|
|
74
|
-
border-radius: 4px;
|
|
75
|
-
font-size: 0.85em;
|
|
76
|
-
background-color: #fff;
|
|
77
|
-
color: #2c3e50;
|
|
78
|
-
}
|
|
79
|
-
.pict-flow-palette-container {
|
|
80
|
-
border-bottom: 1px solid #e0e0e0;
|
|
81
|
-
background-color: #fafafa;
|
|
82
|
-
}
|
|
83
|
-
.pict-flow-palette-toggle {
|
|
84
|
-
display: flex;
|
|
85
|
-
align-items: center;
|
|
86
|
-
justify-content: space-between;
|
|
87
|
-
padding: 0.4em 0.75em;
|
|
88
|
-
cursor: pointer;
|
|
89
|
-
user-select: none;
|
|
90
|
-
-webkit-user-select: none;
|
|
91
|
-
font-size: 0.8em;
|
|
92
|
-
color: #7f8c8d;
|
|
93
|
-
background-color: #f4f4f5;
|
|
94
|
-
border-bottom: 1px solid #e0e0e0;
|
|
95
|
-
}
|
|
96
|
-
.pict-flow-palette-toggle:hover {
|
|
97
|
-
background-color: #ecf0f1;
|
|
98
|
-
color: #2c3e50;
|
|
99
|
-
}
|
|
100
|
-
.pict-flow-palette-toggle-arrow {
|
|
101
|
-
font-size: 0.7em;
|
|
102
|
-
transition: transform 0.2s;
|
|
103
|
-
}
|
|
104
|
-
.pict-flow-palette-toggle-arrow.open {
|
|
105
|
-
transform: rotate(180deg);
|
|
106
|
-
}
|
|
107
|
-
.pict-flow-palette-body {
|
|
108
|
-
display: none;
|
|
109
|
-
padding: 0.5em 0.75em 0.75em 0.75em;
|
|
110
|
-
max-height: 280px;
|
|
111
|
-
overflow-y: auto;
|
|
112
|
-
}
|
|
113
|
-
.pict-flow-palette-body.open {
|
|
114
|
-
display: block;
|
|
115
|
-
}
|
|
116
|
-
.pict-flow-palette-category {
|
|
117
|
-
margin-bottom: 0.5em;
|
|
118
|
-
}
|
|
119
|
-
.pict-flow-palette-category:last-child {
|
|
120
|
-
margin-bottom: 0;
|
|
121
|
-
}
|
|
122
|
-
.pict-flow-palette-category-label {
|
|
123
|
-
font-size: 0.7em;
|
|
124
|
-
font-weight: 700;
|
|
125
|
-
text-transform: uppercase;
|
|
126
|
-
letter-spacing: 0.05em;
|
|
127
|
-
color: #95a5a6;
|
|
128
|
-
margin-bottom: 0.35em;
|
|
129
|
-
padding-bottom: 0.2em;
|
|
130
|
-
border-bottom: 1px solid #ecf0f1;
|
|
131
|
-
}
|
|
132
|
-
.pict-flow-palette-cards {
|
|
133
|
-
display: flex;
|
|
134
|
-
flex-wrap: wrap;
|
|
135
|
-
gap: 0.35em;
|
|
136
|
-
}
|
|
137
|
-
.pict-flow-palette-card {
|
|
138
|
-
display: inline-flex;
|
|
139
|
-
align-items: center;
|
|
140
|
-
gap: 0.35em;
|
|
141
|
-
padding: 0.35em 0.6em;
|
|
142
|
-
border: 1px solid #d5d8dc;
|
|
143
|
-
border-radius: 4px;
|
|
144
|
-
background-color: #ffffff;
|
|
145
|
-
font-size: 0.8em;
|
|
146
|
-
cursor: pointer;
|
|
147
|
-
transition: background-color 0.15s, border-color 0.15s, box-shadow 0.15s;
|
|
148
|
-
user-select: none;
|
|
149
|
-
-webkit-user-select: none;
|
|
150
|
-
position: relative;
|
|
151
|
-
}
|
|
152
|
-
.pict-flow-palette-card:hover {
|
|
153
|
-
background-color: #eaf2f8;
|
|
154
|
-
border-color: #3498db;
|
|
155
|
-
box-shadow: 0 1px 3px rgba(52, 152, 219, 0.15);
|
|
156
|
-
}
|
|
157
|
-
.pict-flow-palette-card.disabled {
|
|
158
|
-
opacity: 0.45;
|
|
159
|
-
pointer-events: none;
|
|
160
|
-
cursor: default;
|
|
161
|
-
}
|
|
162
|
-
.pict-flow-palette-card-icon {
|
|
163
|
-
font-size: 1.1em;
|
|
164
|
-
line-height: 1;
|
|
165
|
-
}
|
|
166
|
-
.pict-flow-palette-card-swatch {
|
|
167
|
-
width: 10px;
|
|
168
|
-
height: 10px;
|
|
169
|
-
border-radius: 2px;
|
|
170
|
-
flex-shrink: 0;
|
|
171
|
-
}
|
|
172
|
-
.pict-flow-palette-card-title {
|
|
173
|
-
font-weight: 500;
|
|
174
|
-
color: #2c3e50;
|
|
175
|
-
white-space: nowrap;
|
|
176
|
-
}
|
|
177
|
-
.pict-flow-palette-card-code {
|
|
178
|
-
font-size: 0.8em;
|
|
179
|
-
color: #95a5a6;
|
|
180
|
-
font-family: monospace;
|
|
181
|
-
}
|
|
182
|
-
.pict-flow-toolbar-select.layout-select {
|
|
183
|
-
min-width: 120px;
|
|
184
|
-
max-width: 200px;
|
|
185
|
-
}
|
|
186
|
-
`,
|
|
16
|
+
CSS: false,
|
|
187
17
|
|
|
188
18
|
Templates:
|
|
189
19
|
[
|
|
190
20
|
{
|
|
191
21
|
Hash: 'Flow-Toolbar-Template',
|
|
192
22
|
Template: /*html*/`
|
|
193
|
-
<div class="pict-flow-toolbar">
|
|
23
|
+
<div class="pict-flow-toolbar" id="Flow-Toolbar-Bar-{~D:Record.FlowViewIdentifier~}">
|
|
194
24
|
<div class="pict-flow-toolbar-group">
|
|
195
|
-
<
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
25
|
+
<button class="pict-flow-toolbar-btn" data-flow-action="add-node" id="Flow-Toolbar-AddNode-{~D:Record.FlowViewIdentifier~}" title="Add Node">
|
|
26
|
+
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-plus-{~D:Record.FlowViewIdentifier~}"></span>
|
|
27
|
+
<span class="pict-flow-toolbar-btn-text">Node</span>
|
|
28
|
+
</button>
|
|
29
|
+
<button class="pict-flow-toolbar-btn danger" data-flow-action="delete-selected" title="Delete Node">
|
|
30
|
+
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-trash-{~D:Record.FlowViewIdentifier~}"></span>
|
|
31
|
+
</button>
|
|
199
32
|
</div>
|
|
200
33
|
<div class="pict-flow-toolbar-group">
|
|
201
|
-
<button class="pict-flow-toolbar-btn
|
|
34
|
+
<button class="pict-flow-toolbar-btn" data-flow-action="cards-popup" id="Flow-Toolbar-Cards-{~D:Record.FlowViewIdentifier~}" title="Card Palette">
|
|
35
|
+
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-cards-{~D:Record.FlowViewIdentifier~}"></span>
|
|
36
|
+
<span class="pict-flow-toolbar-btn-text">Cards</span>
|
|
37
|
+
<span class="pict-flow-toolbar-btn-chevron" id="Flow-Toolbar-CardsChevron-{~D:Record.FlowViewIdentifier~}"></span>
|
|
38
|
+
</button>
|
|
202
39
|
</div>
|
|
203
40
|
<div class="pict-flow-toolbar-group">
|
|
204
|
-
<button class="pict-flow-toolbar-btn" data-flow-action="
|
|
205
|
-
|
|
206
|
-
|
|
41
|
+
<button class="pict-flow-toolbar-btn" data-flow-action="layout-popup" id="Flow-Toolbar-Layout-{~D:Record.FlowViewIdentifier~}" title="Manage Layouts">
|
|
42
|
+
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-layout-{~D:Record.FlowViewIdentifier~}"></span>
|
|
43
|
+
<span class="pict-flow-toolbar-btn-text">Layout</span>
|
|
44
|
+
<span class="pict-flow-toolbar-btn-chevron" id="Flow-Toolbar-LayoutChevron-{~D:Record.FlowViewIdentifier~}"></span>
|
|
45
|
+
</button>
|
|
46
|
+
<button class="pict-flow-toolbar-btn" data-flow-action="auto-layout" title="Auto Layout">
|
|
47
|
+
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-auto-layout-{~D:Record.FlowViewIdentifier~}"></span>
|
|
48
|
+
<span class="pict-flow-toolbar-btn-text">Auto Layout</span>
|
|
49
|
+
</button>
|
|
207
50
|
</div>
|
|
208
51
|
<div class="pict-flow-toolbar-group">
|
|
209
|
-
<button class="pict-flow-toolbar-btn" data-flow-action="
|
|
52
|
+
<button class="pict-flow-toolbar-btn" data-flow-action="zoom-in" title="Zoom In">
|
|
53
|
+
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-zoom-in-{~D:Record.FlowViewIdentifier~}"></span>
|
|
54
|
+
</button>
|
|
55
|
+
<button class="pict-flow-toolbar-btn" data-flow-action="zoom-out" title="Zoom Out">
|
|
56
|
+
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-zoom-out-{~D:Record.FlowViewIdentifier~}"></span>
|
|
57
|
+
</button>
|
|
58
|
+
<button class="pict-flow-toolbar-btn" data-flow-action="zoom-fit" title="Fit to View">
|
|
59
|
+
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-zoom-fit-{~D:Record.FlowViewIdentifier~}"></span>
|
|
60
|
+
</button>
|
|
210
61
|
</div>
|
|
211
|
-
<div class="pict-flow-toolbar-group">
|
|
212
|
-
<
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
<button class="pict-flow-toolbar-btn" data-flow-action="
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
62
|
+
<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">
|
|
64
|
+
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-settings-{~D:Record.FlowViewIdentifier~}"></span>
|
|
65
|
+
</button>
|
|
66
|
+
<button class="pict-flow-toolbar-btn" data-flow-action="fullscreen" id="Flow-Toolbar-Fullscreen-{~D:Record.FlowViewIdentifier~}" title="Toggle Fullscreen">
|
|
67
|
+
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Fullscreen-Icon-{~D:Record.FlowViewIdentifier~}"></span>
|
|
68
|
+
</button>
|
|
69
|
+
<button class="pict-flow-toolbar-btn" data-flow-action="toggle-floating" title="Float">
|
|
70
|
+
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-grip-{~D:Record.FlowViewIdentifier~}"></span>
|
|
71
|
+
</button>
|
|
72
|
+
<button class="pict-flow-toolbar-btn" data-flow-action="collapse-toolbar" title="Collapse Toolbar">
|
|
73
|
+
<span class="pict-flow-toolbar-btn-icon" id="Flow-Toolbar-Icon-collapse-{~D:Record.FlowViewIdentifier~}"></span>
|
|
74
|
+
</button>
|
|
223
75
|
</div>
|
|
224
76
|
</div>
|
|
225
|
-
<div class="pict-flow-
|
|
226
|
-
<
|
|
227
|
-
<span
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
</div>
|
|
77
|
+
<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~}">
|
|
79
|
+
<span id="Flow-Toolbar-Icon-expand-{~D:Record.FlowViewIdentifier~}"></span>
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="pict-flow-toolbar-popup-anchor" id="Flow-Toolbar-PopupAnchor-{~D:Record.FlowViewIdentifier~}">
|
|
232
83
|
</div>
|
|
233
84
|
`
|
|
234
85
|
}
|
|
@@ -255,7 +106,13 @@ class PictViewFlowToolbar extends libPictView
|
|
|
255
106
|
this.serviceType = 'PictViewFlowToolbar';
|
|
256
107
|
|
|
257
108
|
this._FlowView = null;
|
|
258
|
-
|
|
109
|
+
|
|
110
|
+
// Toolbar mode state
|
|
111
|
+
this._ToolbarMode = 'docked'; // 'docked' | 'floating' | 'collapsed'
|
|
112
|
+
this._ActivePopup = null; // 'add-node' | 'cards' | 'layout' | null
|
|
113
|
+
this._FloatingPosition = { X: 80, Y: 80 };
|
|
114
|
+
this._DocumentClickHandler = null;
|
|
115
|
+
this._FloatingToolbarView = null;
|
|
259
116
|
}
|
|
260
117
|
|
|
261
118
|
render(pRenderableHash, pRenderDestinationAddress, pTemplateRecordAddress)
|
|
@@ -267,12 +124,13 @@ class PictViewFlowToolbar extends libPictView
|
|
|
267
124
|
|
|
268
125
|
onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent)
|
|
269
126
|
{
|
|
127
|
+
let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
128
|
+
|
|
270
129
|
// Bind toolbar button events via event delegation
|
|
271
|
-
let
|
|
272
|
-
if (
|
|
130
|
+
let tmpToolbarBar = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-Bar-${tmpFlowViewIdentifier}`);
|
|
131
|
+
if (tmpToolbarBar.length > 0)
|
|
273
132
|
{
|
|
274
|
-
|
|
275
|
-
tmpToolbar.addEventListener('click', (pEvent) =>
|
|
133
|
+
tmpToolbarBar[0].addEventListener('click', (pEvent) =>
|
|
276
134
|
{
|
|
277
135
|
let tmpTarget = pEvent.target;
|
|
278
136
|
if (!tmpTarget) return;
|
|
@@ -286,170 +144,395 @@ class PictViewFlowToolbar extends libPictView
|
|
|
286
144
|
});
|
|
287
145
|
}
|
|
288
146
|
|
|
289
|
-
// Bind
|
|
290
|
-
let
|
|
291
|
-
|
|
292
|
-
if (tmpPaletteContainer.length > 0)
|
|
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)
|
|
293
150
|
{
|
|
294
|
-
|
|
151
|
+
tmpExpandBtn[0].addEventListener('click', () =>
|
|
295
152
|
{
|
|
296
|
-
|
|
297
|
-
if (!tmpTarget) return;
|
|
298
|
-
|
|
299
|
-
// Check for toggle
|
|
300
|
-
let tmpToggle = tmpTarget.closest('[data-flow-action="toggle-palette"]');
|
|
301
|
-
if (tmpToggle)
|
|
302
|
-
{
|
|
303
|
-
this._togglePalette();
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Check for card click
|
|
308
|
-
let tmpCard = tmpTarget.closest('[data-card-type]');
|
|
309
|
-
if (tmpCard)
|
|
310
|
-
{
|
|
311
|
-
let tmpCardType = tmpCard.getAttribute('data-card-type');
|
|
312
|
-
this._addCardFromPalette(tmpCardType);
|
|
313
|
-
}
|
|
153
|
+
this._setToolbarMode('docked');
|
|
314
154
|
});
|
|
315
155
|
}
|
|
316
156
|
|
|
317
|
-
// Populate
|
|
318
|
-
this.
|
|
319
|
-
this._renderPalette();
|
|
320
|
-
this._populateLayoutDropdown();
|
|
157
|
+
// Populate SVG icons for toolbar buttons
|
|
158
|
+
this._populateToolbarIcons();
|
|
321
159
|
|
|
322
160
|
return super.onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent);
|
|
323
161
|
}
|
|
324
162
|
|
|
163
|
+
// ── Icon Population ───────────────────────────────────────────────────
|
|
164
|
+
|
|
325
165
|
/**
|
|
326
|
-
* Populate
|
|
166
|
+
* Populate SVG icons for all toolbar buttons.
|
|
327
167
|
*/
|
|
328
|
-
|
|
168
|
+
_populateToolbarIcons()
|
|
329
169
|
{
|
|
330
|
-
|
|
170
|
+
let tmpIconProvider = this._FlowView ? this._FlowView._IconProvider : null;
|
|
171
|
+
if (!tmpIconProvider) return;
|
|
172
|
+
|
|
173
|
+
let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
174
|
+
|
|
175
|
+
// Map of element ID suffix → icon key
|
|
176
|
+
let tmpIconMap =
|
|
331
177
|
{
|
|
332
|
-
|
|
178
|
+
'plus': 'plus',
|
|
179
|
+
'trash': 'trash',
|
|
180
|
+
'zoom-in': 'zoom-in',
|
|
181
|
+
'zoom-out': 'zoom-out',
|
|
182
|
+
'zoom-fit': 'zoom-fit',
|
|
183
|
+
'auto-layout': 'auto-layout',
|
|
184
|
+
'cards': 'cards',
|
|
185
|
+
'layout': 'layout',
|
|
186
|
+
'settings': 'settings',
|
|
187
|
+
'grip': 'grip',
|
|
188
|
+
'collapse': 'collapse',
|
|
189
|
+
'expand': 'expand'
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
let tmpKeys = Object.keys(tmpIconMap);
|
|
193
|
+
for (let i = 0; i < tmpKeys.length; i++)
|
|
194
|
+
{
|
|
195
|
+
let tmpElementId = `Flow-Toolbar-Icon-${tmpKeys[i]}-${tmpFlowViewIdentifier}`;
|
|
196
|
+
let tmpElements = this.pict.ContentAssignment.getElement(`#${tmpElementId}`);
|
|
197
|
+
if (tmpElements.length > 0)
|
|
198
|
+
{
|
|
199
|
+
tmpElements[0].innerHTML = tmpIconProvider.getIconSVGMarkup(tmpIconMap[tmpKeys[i]], 14);
|
|
200
|
+
}
|
|
333
201
|
}
|
|
334
202
|
|
|
335
|
-
|
|
336
|
-
let
|
|
337
|
-
if (
|
|
203
|
+
// Fullscreen icon
|
|
204
|
+
let tmpFullscreenIcon = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-Fullscreen-Icon-${tmpFlowViewIdentifier}`);
|
|
205
|
+
if (tmpFullscreenIcon.length > 0)
|
|
338
206
|
{
|
|
339
|
-
|
|
207
|
+
tmpFullscreenIcon[0].innerHTML = tmpIconProvider.getIconSVGMarkup('fullscreen', 14);
|
|
340
208
|
}
|
|
341
209
|
|
|
342
|
-
|
|
210
|
+
// Chevrons (smaller)
|
|
211
|
+
let tmpCardsChevron = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-CardsChevron-${tmpFlowViewIdentifier}`);
|
|
212
|
+
if (tmpCardsChevron.length > 0)
|
|
213
|
+
{
|
|
214
|
+
tmpCardsChevron[0].innerHTML = tmpIconProvider.getIconSVGMarkup('chevron-down', 8);
|
|
215
|
+
}
|
|
343
216
|
|
|
344
|
-
|
|
345
|
-
|
|
217
|
+
let tmpLayoutChevron = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-LayoutChevron-${tmpFlowViewIdentifier}`);
|
|
218
|
+
if (tmpLayoutChevron.length > 0)
|
|
346
219
|
{
|
|
347
|
-
|
|
220
|
+
tmpLayoutChevron[0].innerHTML = tmpIconProvider.getIconSVGMarkup('chevron-down', 8);
|
|
348
221
|
}
|
|
222
|
+
}
|
|
349
223
|
|
|
350
|
-
|
|
351
|
-
let tmpTypeKeys = Object.keys(tmpTypes);
|
|
224
|
+
// ── Popup Management ──────────────────────────────────────────────────
|
|
352
225
|
|
|
353
|
-
|
|
226
|
+
/**
|
|
227
|
+
* Open a popup below a trigger button.
|
|
228
|
+
* @param {string} pType - 'add-node' | 'cards' | 'layout'
|
|
229
|
+
*/
|
|
230
|
+
_openPopup(pType)
|
|
231
|
+
{
|
|
232
|
+
// Toggle off if already open
|
|
233
|
+
if (this._ActivePopup === pType)
|
|
354
234
|
{
|
|
355
|
-
|
|
235
|
+
this._closePopup();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
356
238
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
{
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
239
|
+
// Close any existing popup first
|
|
240
|
+
this._closePopup();
|
|
362
241
|
|
|
363
|
-
|
|
364
|
-
|
|
242
|
+
let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
243
|
+
let tmpAnchor = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-PopupAnchor-${tmpFlowViewIdentifier}`);
|
|
244
|
+
if (tmpAnchor.length < 1) return;
|
|
245
|
+
|
|
246
|
+
// Create popup div
|
|
247
|
+
let tmpPopup = document.createElement('div');
|
|
248
|
+
tmpPopup.className = 'pict-flow-toolbar-popup';
|
|
249
|
+
tmpPopup.setAttribute('id', `Flow-Toolbar-Popup-${tmpFlowViewIdentifier}`);
|
|
250
|
+
|
|
251
|
+
// Build popup content
|
|
252
|
+
switch (pType)
|
|
253
|
+
{
|
|
254
|
+
case 'add-node':
|
|
255
|
+
this._buildAddNodePopup(tmpPopup);
|
|
256
|
+
break;
|
|
257
|
+
case 'cards':
|
|
258
|
+
this._buildCardsPopup(tmpPopup);
|
|
259
|
+
break;
|
|
260
|
+
case 'layout':
|
|
261
|
+
this._buildLayoutPopup(tmpPopup);
|
|
262
|
+
break;
|
|
263
|
+
case 'settings':
|
|
264
|
+
this._buildSettingsPopup(tmpPopup);
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
365
267
|
|
|
366
|
-
|
|
268
|
+
tmpAnchor[0].appendChild(tmpPopup);
|
|
269
|
+
this._ActivePopup = pType;
|
|
270
|
+
|
|
271
|
+
// Position the popup below the trigger button
|
|
272
|
+
this._positionPopup(tmpPopup, pType);
|
|
273
|
+
|
|
274
|
+
// Click-outside-to-close handler (delayed to avoid catching the opening click)
|
|
275
|
+
setTimeout(() =>
|
|
276
|
+
{
|
|
277
|
+
this._DocumentClickHandler = (pEvent) =>
|
|
367
278
|
{
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
279
|
+
if (!tmpPopup.contains(pEvent.target))
|
|
280
|
+
{
|
|
281
|
+
// Check if click was on the trigger button itself (toggle behavior)
|
|
282
|
+
let tmpButton = pEvent.target.closest('[data-flow-action]');
|
|
283
|
+
if (tmpButton)
|
|
284
|
+
{
|
|
285
|
+
let tmpAction = tmpButton.getAttribute('data-flow-action');
|
|
286
|
+
if (tmpAction === pType || tmpAction === pType.replace('-popup', '') + '-popup')
|
|
287
|
+
{
|
|
288
|
+
return; // Let the toggle handle it
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
this._closePopup();
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
document.addEventListener('click', this._DocumentClickHandler, true);
|
|
295
|
+
}, 0);
|
|
296
|
+
|
|
297
|
+
// Focus search input if Add Node popup
|
|
298
|
+
if (pType === 'add-node')
|
|
299
|
+
{
|
|
300
|
+
let tmpSearch = tmpPopup.querySelector('.pict-flow-popup-search');
|
|
301
|
+
if (tmpSearch)
|
|
371
302
|
{
|
|
372
|
-
|
|
303
|
+
setTimeout(() => { tmpSearch.focus(); }, 50);
|
|
373
304
|
}
|
|
374
|
-
|
|
375
|
-
tmpSelect.appendChild(tmpOption);
|
|
376
305
|
}
|
|
377
306
|
}
|
|
378
307
|
|
|
379
308
|
/**
|
|
380
|
-
*
|
|
309
|
+
* Close the active popup and clean up.
|
|
381
310
|
*/
|
|
382
|
-
|
|
311
|
+
_closePopup()
|
|
383
312
|
{
|
|
384
|
-
if (
|
|
313
|
+
if (this._DocumentClickHandler)
|
|
385
314
|
{
|
|
386
|
-
|
|
315
|
+
document.removeEventListener('click', this._DocumentClickHandler, true);
|
|
316
|
+
this._DocumentClickHandler = null;
|
|
387
317
|
}
|
|
388
318
|
|
|
389
319
|
let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
390
|
-
let
|
|
391
|
-
|
|
392
|
-
);
|
|
393
|
-
if (tmpSelectElements.length < 1)
|
|
320
|
+
let tmpPopup = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-Popup-${tmpFlowViewIdentifier}`);
|
|
321
|
+
if (tmpPopup.length > 0)
|
|
394
322
|
{
|
|
395
|
-
|
|
323
|
+
tmpPopup[0].parentNode.removeChild(tmpPopup[0]);
|
|
396
324
|
}
|
|
397
325
|
|
|
398
|
-
|
|
326
|
+
this._ActivePopup = null;
|
|
327
|
+
}
|
|
399
328
|
|
|
400
|
-
|
|
401
|
-
|
|
329
|
+
/**
|
|
330
|
+
* Position a popup below its trigger button.
|
|
331
|
+
* @param {HTMLElement} pPopupDiv
|
|
332
|
+
* @param {string} pType
|
|
333
|
+
*/
|
|
334
|
+
_positionPopup(pPopupDiv, pType)
|
|
335
|
+
{
|
|
336
|
+
let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
337
|
+
|
|
338
|
+
// Determine which button triggered the popup
|
|
339
|
+
let tmpTriggerSelector;
|
|
340
|
+
switch (pType)
|
|
402
341
|
{
|
|
403
|
-
|
|
342
|
+
case 'add-node':
|
|
343
|
+
tmpTriggerSelector = `#Flow-Toolbar-AddNode-${tmpFlowViewIdentifier}`;
|
|
344
|
+
break;
|
|
345
|
+
case 'cards':
|
|
346
|
+
tmpTriggerSelector = `#Flow-Toolbar-Cards-${tmpFlowViewIdentifier}`;
|
|
347
|
+
break;
|
|
348
|
+
case 'layout':
|
|
349
|
+
tmpTriggerSelector = `#Flow-Toolbar-Layout-${tmpFlowViewIdentifier}`;
|
|
350
|
+
break;
|
|
351
|
+
case 'settings':
|
|
352
|
+
tmpTriggerSelector = `#Flow-Toolbar-Settings-${tmpFlowViewIdentifier}`;
|
|
353
|
+
break;
|
|
354
|
+
default:
|
|
355
|
+
return;
|
|
404
356
|
}
|
|
405
357
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
tmpPlaceholder.value = '';
|
|
409
|
-
tmpPlaceholder.textContent = '-- select layout --';
|
|
410
|
-
tmpSelect.appendChild(tmpPlaceholder);
|
|
358
|
+
let tmpTriggerElements = this.pict.ContentAssignment.getElement(tmpTriggerSelector);
|
|
359
|
+
if (tmpTriggerElements.length < 1) return;
|
|
411
360
|
|
|
412
|
-
let
|
|
413
|
-
|
|
361
|
+
let tmpAnchor = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-PopupAnchor-${tmpFlowViewIdentifier}`);
|
|
362
|
+
if (tmpAnchor.length < 1) return;
|
|
363
|
+
|
|
364
|
+
let tmpTriggerRect = tmpTriggerElements[0].getBoundingClientRect();
|
|
365
|
+
let tmpAnchorRect = tmpAnchor[0].getBoundingClientRect();
|
|
366
|
+
|
|
367
|
+
let tmpLeft = tmpTriggerRect.left - tmpAnchorRect.left;
|
|
368
|
+
pPopupDiv.style.left = tmpLeft + 'px';
|
|
369
|
+
pPopupDiv.style.top = '0px';
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ── Add Node Popup ────────────────────────────────────────────────────
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Build the searchable Add Node popup content.
|
|
376
|
+
* @param {HTMLElement} pContainer
|
|
377
|
+
*/
|
|
378
|
+
_buildAddNodePopup(pContainer)
|
|
379
|
+
{
|
|
380
|
+
// Search wrapper
|
|
381
|
+
let tmpSearchWrapper = document.createElement('div');
|
|
382
|
+
tmpSearchWrapper.className = 'pict-flow-popup-search-wrapper';
|
|
383
|
+
|
|
384
|
+
let tmpSearchIcon = document.createElement('span');
|
|
385
|
+
tmpSearchIcon.className = 'pict-flow-popup-search-icon';
|
|
386
|
+
let tmpIconProvider = this._FlowView ? this._FlowView._IconProvider : null;
|
|
387
|
+
if (tmpIconProvider)
|
|
414
388
|
{
|
|
415
|
-
|
|
416
|
-
let tmpOption = document.createElement('option');
|
|
417
|
-
tmpOption.value = tmpLayout.Hash;
|
|
418
|
-
tmpOption.textContent = tmpLayout.Name;
|
|
419
|
-
tmpSelect.appendChild(tmpOption);
|
|
389
|
+
tmpSearchIcon.innerHTML = tmpIconProvider.getIconSVGMarkup('search', 12);
|
|
420
390
|
}
|
|
391
|
+
tmpSearchWrapper.appendChild(tmpSearchIcon);
|
|
392
|
+
|
|
393
|
+
let tmpSearchInput = document.createElement('input');
|
|
394
|
+
tmpSearchInput.className = 'pict-flow-popup-search';
|
|
395
|
+
tmpSearchInput.setAttribute('type', 'text');
|
|
396
|
+
tmpSearchInput.setAttribute('placeholder', 'Search node types...');
|
|
397
|
+
tmpSearchWrapper.appendChild(tmpSearchInput);
|
|
398
|
+
pContainer.appendChild(tmpSearchWrapper);
|
|
399
|
+
|
|
400
|
+
// Node list
|
|
401
|
+
let tmpListDiv = document.createElement('div');
|
|
402
|
+
tmpListDiv.className = 'pict-flow-popup-node-list';
|
|
403
|
+
pContainer.appendChild(tmpListDiv);
|
|
404
|
+
|
|
405
|
+
// Initial population
|
|
406
|
+
this._populateNodeList(tmpListDiv, '');
|
|
407
|
+
|
|
408
|
+
// Filter on input
|
|
409
|
+
tmpSearchInput.addEventListener('input', () =>
|
|
410
|
+
{
|
|
411
|
+
this._populateNodeList(tmpListDiv, tmpSearchInput.value);
|
|
412
|
+
});
|
|
421
413
|
}
|
|
422
414
|
|
|
423
415
|
/**
|
|
424
|
-
*
|
|
416
|
+
* Populate the node list in the Add Node popup, filtered by search text.
|
|
417
|
+
* @param {HTMLElement} pListDiv
|
|
418
|
+
* @param {string} pFilter
|
|
425
419
|
*/
|
|
426
|
-
|
|
420
|
+
_populateNodeList(pListDiv, pFilter)
|
|
427
421
|
{
|
|
428
422
|
if (!this._FlowView || !this._FlowView._NodeTypeProvider) return;
|
|
429
423
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
424
|
+
// Clear
|
|
425
|
+
while (pListDiv.firstChild)
|
|
426
|
+
{
|
|
427
|
+
pListDiv.removeChild(pListDiv.firstChild);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
let tmpTypes = this._FlowView._NodeTypeProvider.getNodeTypes();
|
|
431
|
+
let tmpTypeKeys = Object.keys(tmpTypes);
|
|
432
|
+
let tmpFilter = (pFilter || '').toLowerCase().trim();
|
|
433
|
+
let tmpIconProvider = this._FlowView._IconProvider;
|
|
434
|
+
let tmpMatchCount = 0;
|
|
435
|
+
|
|
436
|
+
for (let i = 0; i < tmpTypeKeys.length; i++)
|
|
437
|
+
{
|
|
438
|
+
let tmpTypeConfig = tmpTypes[tmpTypeKeys[i]];
|
|
439
|
+
let tmpMeta = tmpTypeConfig.CardMetadata || {};
|
|
440
|
+
|
|
441
|
+
// Skip disabled cards
|
|
442
|
+
if (tmpMeta.Enabled === false) continue;
|
|
443
|
+
|
|
444
|
+
// Filter match: label, code, or category
|
|
445
|
+
if (tmpFilter)
|
|
446
|
+
{
|
|
447
|
+
let tmpLabel = (tmpTypeConfig.Label || '').toLowerCase();
|
|
448
|
+
let tmpCode = (tmpMeta.Code || '').toLowerCase();
|
|
449
|
+
let tmpCategory = (tmpMeta.Category || '').toLowerCase();
|
|
450
|
+
if (tmpLabel.indexOf(tmpFilter) < 0 &&
|
|
451
|
+
tmpCode.indexOf(tmpFilter) < 0 &&
|
|
452
|
+
tmpCategory.indexOf(tmpFilter) < 0)
|
|
453
|
+
{
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
433
457
|
|
|
434
|
-
|
|
458
|
+
tmpMatchCount++;
|
|
435
459
|
|
|
436
|
-
|
|
437
|
-
|
|
460
|
+
let tmpRow = document.createElement('div');
|
|
461
|
+
tmpRow.className = 'pict-flow-popup-list-item';
|
|
462
|
+
tmpRow.setAttribute('data-node-type', tmpTypeKeys[i]);
|
|
463
|
+
|
|
464
|
+
// Icon
|
|
465
|
+
let tmpIconSpan = document.createElement('span');
|
|
466
|
+
tmpIconSpan.className = 'pict-flow-popup-list-item-icon';
|
|
467
|
+
if (tmpIconProvider)
|
|
468
|
+
{
|
|
469
|
+
let tmpResolvedKey = tmpIconProvider.resolveIconKey(tmpMeta);
|
|
470
|
+
tmpIconSpan.innerHTML = tmpIconProvider.getIconSVGMarkup(tmpResolvedKey, 16);
|
|
471
|
+
}
|
|
472
|
+
tmpRow.appendChild(tmpIconSpan);
|
|
473
|
+
|
|
474
|
+
// Label
|
|
475
|
+
let tmpLabelSpan = document.createElement('span');
|
|
476
|
+
tmpLabelSpan.className = 'pict-flow-popup-list-item-label';
|
|
477
|
+
tmpLabelSpan.textContent = tmpTypeConfig.Label;
|
|
478
|
+
tmpRow.appendChild(tmpLabelSpan);
|
|
479
|
+
|
|
480
|
+
// Code badge
|
|
481
|
+
if (tmpMeta.Code)
|
|
482
|
+
{
|
|
483
|
+
let tmpCodeSpan = document.createElement('span');
|
|
484
|
+
tmpCodeSpan.className = 'pict-flow-popup-list-item-code';
|
|
485
|
+
tmpCodeSpan.textContent = tmpMeta.Code;
|
|
486
|
+
tmpRow.appendChild(tmpCodeSpan);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Click handler
|
|
490
|
+
tmpRow.addEventListener('click', () =>
|
|
491
|
+
{
|
|
492
|
+
this._addNodeAtCenter(tmpTypeKeys[i]);
|
|
493
|
+
this._closePopup();
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
pListDiv.appendChild(tmpRow);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (tmpMatchCount === 0)
|
|
438
500
|
{
|
|
439
|
-
|
|
501
|
+
let tmpEmpty = document.createElement('div');
|
|
502
|
+
tmpEmpty.className = 'pict-flow-popup-list-empty';
|
|
503
|
+
tmpEmpty.textContent = 'No matching node types';
|
|
504
|
+
pListDiv.appendChild(tmpEmpty);
|
|
440
505
|
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ── Cards Popup ───────────────────────────────────────────────────────
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Build the Cards popup content (reuses palette rendering).
|
|
512
|
+
* @param {HTMLElement} pContainer
|
|
513
|
+
*/
|
|
514
|
+
_buildCardsPopup(pContainer)
|
|
515
|
+
{
|
|
516
|
+
this._renderPalette(pContainer);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Render the card palette with categories and card chips into a container.
|
|
521
|
+
* @param {HTMLElement} pContainer - The target container element
|
|
522
|
+
*/
|
|
523
|
+
_renderPalette(pContainer)
|
|
524
|
+
{
|
|
525
|
+
if (!this._FlowView || !this._FlowView._NodeTypeProvider) return;
|
|
441
526
|
|
|
442
527
|
let tmpCategories = this._FlowView._NodeTypeProvider.getCardsByCategory();
|
|
443
528
|
let tmpCategoryKeys = Object.keys(tmpCategories);
|
|
444
529
|
|
|
445
530
|
if (tmpCategoryKeys.length === 0)
|
|
446
531
|
{
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
tmpPaletteContainer[0].style.display = 'none';
|
|
452
|
-
}
|
|
532
|
+
let tmpEmpty = document.createElement('div');
|
|
533
|
+
tmpEmpty.className = 'pict-flow-popup-list-empty';
|
|
534
|
+
tmpEmpty.textContent = 'No card types available';
|
|
535
|
+
pContainer.appendChild(tmpEmpty);
|
|
453
536
|
return;
|
|
454
537
|
}
|
|
455
538
|
|
|
@@ -460,6 +543,7 @@ class PictViewFlowToolbar extends libPictView
|
|
|
460
543
|
|
|
461
544
|
let tmpCategoryDiv = document.createElement('div');
|
|
462
545
|
tmpCategoryDiv.className = 'pict-flow-palette-category';
|
|
546
|
+
tmpCategoryDiv.style.padding = '0.35em 0.5em';
|
|
463
547
|
|
|
464
548
|
let tmpCategoryLabel = document.createElement('div');
|
|
465
549
|
tmpCategoryLabel.className = 'pict-flow-palette-category-label';
|
|
@@ -496,7 +580,23 @@ class PictViewFlowToolbar extends libPictView
|
|
|
496
580
|
{
|
|
497
581
|
let tmpIconSpan = document.createElement('span');
|
|
498
582
|
tmpIconSpan.className = 'pict-flow-palette-card-icon';
|
|
499
|
-
|
|
583
|
+
let tmpIconProvider = this._FlowView._IconProvider;
|
|
584
|
+
if (tmpIconProvider && !tmpIconProvider.isEmojiIcon(tmpMeta.Icon))
|
|
585
|
+
{
|
|
586
|
+
let tmpResolvedKey = tmpIconProvider.resolveIconKey(tmpMeta);
|
|
587
|
+
tmpIconSpan.innerHTML = tmpIconProvider.getIconSVGMarkup(tmpResolvedKey, 14);
|
|
588
|
+
}
|
|
589
|
+
else
|
|
590
|
+
{
|
|
591
|
+
tmpIconSpan.textContent = tmpMeta.Icon;
|
|
592
|
+
}
|
|
593
|
+
tmpCardEl.appendChild(tmpIconSpan);
|
|
594
|
+
}
|
|
595
|
+
else if (this._FlowView._IconProvider)
|
|
596
|
+
{
|
|
597
|
+
let tmpIconSpan = document.createElement('span');
|
|
598
|
+
tmpIconSpan.className = 'pict-flow-palette-card-icon';
|
|
599
|
+
tmpIconSpan.innerHTML = this._FlowView._IconProvider.getIconSVGMarkup('default', 14);
|
|
500
600
|
tmpCardEl.appendChild(tmpIconSpan);
|
|
501
601
|
}
|
|
502
602
|
else if (tmpCardConfig.TitleBarColor)
|
|
@@ -522,48 +622,439 @@ class PictViewFlowToolbar extends libPictView
|
|
|
522
622
|
tmpCardEl.appendChild(tmpCodeSpan);
|
|
523
623
|
}
|
|
524
624
|
|
|
625
|
+
// Click handler
|
|
626
|
+
tmpCardEl.addEventListener('click', () =>
|
|
627
|
+
{
|
|
628
|
+
this._addCardFromPalette(tmpCardConfig.Hash);
|
|
629
|
+
this._closePopup();
|
|
630
|
+
});
|
|
631
|
+
|
|
525
632
|
tmpCardsDiv.appendChild(tmpCardEl);
|
|
526
633
|
}
|
|
527
634
|
|
|
528
635
|
tmpCategoryDiv.appendChild(tmpCardsDiv);
|
|
529
|
-
|
|
636
|
+
pContainer.appendChild(tmpCategoryDiv);
|
|
530
637
|
}
|
|
531
638
|
}
|
|
532
639
|
|
|
640
|
+
// ── Layout Popup ──────────────────────────────────────────────────────
|
|
641
|
+
|
|
533
642
|
/**
|
|
534
|
-
*
|
|
643
|
+
* Build the Layout popup content.
|
|
644
|
+
* @param {HTMLElement} pContainer
|
|
535
645
|
*/
|
|
536
|
-
|
|
646
|
+
_buildLayoutPopup(pContainer)
|
|
537
647
|
{
|
|
538
|
-
this.
|
|
648
|
+
let tmpIconProvider = this._FlowView ? this._FlowView._IconProvider : null;
|
|
649
|
+
|
|
650
|
+
// Save Layout section at top
|
|
651
|
+
let tmpSaveSection = document.createElement('div');
|
|
652
|
+
tmpSaveSection.className = 'pict-flow-popup-layout-save-section';
|
|
653
|
+
|
|
654
|
+
// Save input row (hidden initially)
|
|
655
|
+
let tmpSaveInputRow = document.createElement('div');
|
|
656
|
+
tmpSaveInputRow.className = 'pict-flow-popup-layout-save-input-row';
|
|
657
|
+
tmpSaveInputRow.style.display = 'none';
|
|
658
|
+
|
|
659
|
+
let tmpSaveInput = document.createElement('input');
|
|
660
|
+
tmpSaveInput.className = 'pict-flow-popup-layout-save-input';
|
|
661
|
+
tmpSaveInput.setAttribute('type', 'text');
|
|
662
|
+
tmpSaveInput.setAttribute('placeholder', 'Layout name...');
|
|
663
|
+
tmpSaveInputRow.appendChild(tmpSaveInput);
|
|
664
|
+
|
|
665
|
+
let tmpSaveConfirmBtn = document.createElement('button');
|
|
666
|
+
tmpSaveConfirmBtn.className = 'pict-flow-popup-layout-save-confirm';
|
|
667
|
+
tmpSaveConfirmBtn.title = 'Save';
|
|
668
|
+
if (tmpIconProvider)
|
|
669
|
+
{
|
|
670
|
+
tmpSaveConfirmBtn.innerHTML = tmpIconProvider.getIconSVGMarkup('save', 14);
|
|
671
|
+
}
|
|
672
|
+
else
|
|
673
|
+
{
|
|
674
|
+
tmpSaveConfirmBtn.textContent = '✓';
|
|
675
|
+
}
|
|
676
|
+
tmpSaveInputRow.appendChild(tmpSaveConfirmBtn);
|
|
539
677
|
|
|
540
|
-
|
|
678
|
+
// "Save Current Layout" clickable row
|
|
679
|
+
let tmpSaveRow = document.createElement('div');
|
|
680
|
+
tmpSaveRow.className = 'pict-flow-popup-layout-save';
|
|
681
|
+
|
|
682
|
+
let tmpSaveIcon = document.createElement('span');
|
|
683
|
+
tmpSaveIcon.className = 'pict-flow-popup-layout-save-icon';
|
|
684
|
+
if (tmpIconProvider)
|
|
685
|
+
{
|
|
686
|
+
tmpSaveIcon.innerHTML = tmpIconProvider.getIconSVGMarkup('save', 14);
|
|
687
|
+
}
|
|
688
|
+
tmpSaveRow.appendChild(tmpSaveIcon);
|
|
689
|
+
|
|
690
|
+
let tmpSaveText = document.createElement('span');
|
|
691
|
+
tmpSaveText.textContent = 'Save Current Layout';
|
|
692
|
+
tmpSaveRow.appendChild(tmpSaveText);
|
|
693
|
+
|
|
694
|
+
// Click "Save Current Layout" to reveal the input row
|
|
695
|
+
tmpSaveRow.addEventListener('click', () =>
|
|
696
|
+
{
|
|
697
|
+
tmpSaveRow.style.display = 'none';
|
|
698
|
+
tmpSaveInputRow.style.display = '';
|
|
699
|
+
tmpSaveInput.value = '';
|
|
700
|
+
setTimeout(() => { tmpSaveInput.focus(); }, 50);
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
// Confirm save via button click
|
|
704
|
+
let tmpDoSave = () =>
|
|
705
|
+
{
|
|
706
|
+
let tmpName = tmpSaveInput.value.trim();
|
|
707
|
+
if (tmpName === '') return;
|
|
708
|
+
this._FlowView._LayoutProvider.saveLayout(tmpName);
|
|
709
|
+
// Refresh the popup content
|
|
710
|
+
while (pContainer.firstChild)
|
|
711
|
+
{
|
|
712
|
+
pContainer.removeChild(pContainer.firstChild);
|
|
713
|
+
}
|
|
714
|
+
this._buildLayoutPopup(pContainer);
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
tmpSaveConfirmBtn.addEventListener('click', tmpDoSave);
|
|
541
718
|
|
|
542
|
-
|
|
543
|
-
|
|
719
|
+
// Confirm save via Enter key
|
|
720
|
+
tmpSaveInput.addEventListener('keydown', (pEvent) =>
|
|
544
721
|
{
|
|
545
|
-
if (
|
|
722
|
+
if (pEvent.key === 'Enter')
|
|
546
723
|
{
|
|
547
|
-
|
|
724
|
+
pEvent.preventDefault();
|
|
725
|
+
tmpDoSave();
|
|
548
726
|
}
|
|
549
|
-
else
|
|
727
|
+
else if (pEvent.key === 'Escape')
|
|
550
728
|
{
|
|
551
|
-
|
|
729
|
+
// Cancel — hide input, show the save row again
|
|
730
|
+
tmpSaveInputRow.style.display = 'none';
|
|
731
|
+
tmpSaveRow.style.display = '';
|
|
552
732
|
}
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
// Prevent clicks inside the input from closing the popup
|
|
736
|
+
tmpSaveInput.addEventListener('click', (pEvent) =>
|
|
737
|
+
{
|
|
738
|
+
pEvent.stopPropagation();
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
tmpSaveSection.appendChild(tmpSaveRow);
|
|
742
|
+
tmpSaveSection.appendChild(tmpSaveInputRow);
|
|
743
|
+
pContainer.appendChild(tmpSaveSection);
|
|
744
|
+
|
|
745
|
+
// Divider
|
|
746
|
+
let tmpDivider = document.createElement('div');
|
|
747
|
+
tmpDivider.className = 'pict-flow-popup-divider';
|
|
748
|
+
pContainer.appendChild(tmpDivider);
|
|
749
|
+
|
|
750
|
+
// Layout rows
|
|
751
|
+
if (!this._FlowView || !this._FlowView._LayoutProvider)
|
|
752
|
+
{
|
|
753
|
+
let tmpEmpty = document.createElement('div');
|
|
754
|
+
tmpEmpty.className = 'pict-flow-popup-list-empty';
|
|
755
|
+
tmpEmpty.textContent = 'No saved layouts';
|
|
756
|
+
pContainer.appendChild(tmpEmpty);
|
|
757
|
+
return;
|
|
553
758
|
}
|
|
554
759
|
|
|
555
|
-
let
|
|
556
|
-
|
|
760
|
+
let tmpLayouts = this._FlowView._LayoutProvider.getLayouts();
|
|
761
|
+
|
|
762
|
+
if (tmpLayouts.length === 0)
|
|
557
763
|
{
|
|
558
|
-
|
|
764
|
+
let tmpEmpty = document.createElement('div');
|
|
765
|
+
tmpEmpty.className = 'pict-flow-popup-list-empty';
|
|
766
|
+
tmpEmpty.textContent = 'No saved layouts';
|
|
767
|
+
pContainer.appendChild(tmpEmpty);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
for (let i = 0; i < tmpLayouts.length; i++)
|
|
772
|
+
{
|
|
773
|
+
let tmpLayout = tmpLayouts[i];
|
|
774
|
+
|
|
775
|
+
let tmpRow = document.createElement('div');
|
|
776
|
+
tmpRow.className = 'pict-flow-popup-layout-row';
|
|
777
|
+
|
|
778
|
+
let tmpNameSpan = document.createElement('span');
|
|
779
|
+
tmpNameSpan.className = 'pict-flow-popup-layout-name';
|
|
780
|
+
tmpNameSpan.textContent = tmpLayout.Name;
|
|
781
|
+
tmpRow.appendChild(tmpNameSpan);
|
|
782
|
+
|
|
783
|
+
// Delete button (visible on hover via CSS)
|
|
784
|
+
let tmpDeleteBtn = document.createElement('button');
|
|
785
|
+
tmpDeleteBtn.className = 'pict-flow-popup-layout-delete';
|
|
786
|
+
tmpDeleteBtn.title = 'Delete layout';
|
|
787
|
+
if (tmpIconProvider)
|
|
559
788
|
{
|
|
560
|
-
|
|
789
|
+
tmpDeleteBtn.innerHTML = tmpIconProvider.getIconSVGMarkup('trash', 12);
|
|
561
790
|
}
|
|
562
791
|
else
|
|
563
792
|
{
|
|
564
|
-
|
|
793
|
+
tmpDeleteBtn.textContent = '×';
|
|
794
|
+
}
|
|
795
|
+
tmpRow.appendChild(tmpDeleteBtn);
|
|
796
|
+
|
|
797
|
+
// Click row → restore layout
|
|
798
|
+
tmpRow.addEventListener('click', (pEvent) =>
|
|
799
|
+
{
|
|
800
|
+
// Don't restore if they clicked the delete button
|
|
801
|
+
if (pEvent.target.closest('.pict-flow-popup-layout-delete'))
|
|
802
|
+
{
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
this._FlowView._LayoutProvider.restoreLayout(tmpLayout.Hash);
|
|
806
|
+
this._closePopup();
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
// Click delete → delete layout and refresh popup
|
|
810
|
+
tmpDeleteBtn.addEventListener('click', (pEvent) =>
|
|
811
|
+
{
|
|
812
|
+
pEvent.stopPropagation();
|
|
813
|
+
this._FlowView._LayoutProvider.deleteLayout(tmpLayout.Hash);
|
|
814
|
+
// Refresh the popup content
|
|
815
|
+
while (pContainer.firstChild)
|
|
816
|
+
{
|
|
817
|
+
pContainer.removeChild(pContainer.firstChild);
|
|
818
|
+
}
|
|
819
|
+
this._buildLayoutPopup(pContainer);
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
pContainer.appendChild(tmpRow);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// ── Settings Popup ───────────────────────────────────────────────────
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Build the Settings popup content (theme dropdown + noise slider).
|
|
830
|
+
* @param {HTMLElement} pContainer
|
|
831
|
+
*/
|
|
832
|
+
_buildSettingsPopup(pContainer)
|
|
833
|
+
{
|
|
834
|
+
if (!this._FlowView || !this._FlowView._ThemeProvider) return;
|
|
835
|
+
|
|
836
|
+
let tmpThemeProvider = this._FlowView._ThemeProvider;
|
|
837
|
+
|
|
838
|
+
// Theme selector section
|
|
839
|
+
let tmpThemeSection = document.createElement('div');
|
|
840
|
+
tmpThemeSection.className = 'pict-flow-popup-settings-section';
|
|
841
|
+
|
|
842
|
+
let tmpThemeLabel = document.createElement('label');
|
|
843
|
+
tmpThemeLabel.className = 'pict-flow-popup-settings-label';
|
|
844
|
+
tmpThemeLabel.textContent = 'Theme';
|
|
845
|
+
tmpThemeSection.appendChild(tmpThemeLabel);
|
|
846
|
+
|
|
847
|
+
let tmpThemeSelect = document.createElement('select');
|
|
848
|
+
tmpThemeSelect.className = 'pict-flow-popup-settings-select';
|
|
849
|
+
|
|
850
|
+
let tmpThemeKeys = tmpThemeProvider.getThemeKeys();
|
|
851
|
+
let tmpActiveKey = tmpThemeProvider.getActiveThemeKey();
|
|
852
|
+
|
|
853
|
+
for (let i = 0; i < tmpThemeKeys.length; i++)
|
|
854
|
+
{
|
|
855
|
+
let tmpOption = document.createElement('option');
|
|
856
|
+
tmpOption.value = tmpThemeKeys[i];
|
|
857
|
+
|
|
858
|
+
let tmpTheme = tmpThemeProvider._Themes[tmpThemeKeys[i]];
|
|
859
|
+
tmpOption.textContent = tmpTheme.Label || tmpThemeKeys[i];
|
|
860
|
+
|
|
861
|
+
if (tmpThemeKeys[i] === tmpActiveKey)
|
|
862
|
+
{
|
|
863
|
+
tmpOption.selected = true;
|
|
864
|
+
}
|
|
865
|
+
tmpThemeSelect.appendChild(tmpOption);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
tmpThemeSelect.addEventListener('change', () =>
|
|
869
|
+
{
|
|
870
|
+
this._FlowView.setTheme(tmpThemeSelect.value);
|
|
871
|
+
// Refresh the noise slider visibility
|
|
872
|
+
this._refreshNoiseSlider(pContainer);
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
// Prevent popup close on select interaction
|
|
876
|
+
tmpThemeSelect.addEventListener('click', (pEvent) => { pEvent.stopPropagation(); });
|
|
877
|
+
|
|
878
|
+
tmpThemeSection.appendChild(tmpThemeSelect);
|
|
879
|
+
pContainer.appendChild(tmpThemeSection);
|
|
880
|
+
|
|
881
|
+
// Divider
|
|
882
|
+
let tmpDivider = document.createElement('div');
|
|
883
|
+
tmpDivider.className = 'pict-flow-popup-divider';
|
|
884
|
+
pContainer.appendChild(tmpDivider);
|
|
885
|
+
|
|
886
|
+
// Noise level section
|
|
887
|
+
let tmpNoiseSection = document.createElement('div');
|
|
888
|
+
tmpNoiseSection.className = 'pict-flow-popup-settings-section pict-flow-popup-settings-noise';
|
|
889
|
+
tmpNoiseSection.setAttribute('data-settings-type', 'noise');
|
|
890
|
+
|
|
891
|
+
let tmpNoiseLabel = document.createElement('label');
|
|
892
|
+
tmpNoiseLabel.className = 'pict-flow-popup-settings-label';
|
|
893
|
+
tmpNoiseLabel.textContent = 'Noise';
|
|
894
|
+
tmpNoiseSection.appendChild(tmpNoiseLabel);
|
|
895
|
+
|
|
896
|
+
let tmpNoiseRow = document.createElement('div');
|
|
897
|
+
tmpNoiseRow.className = 'pict-flow-popup-settings-slider-row';
|
|
898
|
+
|
|
899
|
+
let tmpNoiseSlider = document.createElement('input');
|
|
900
|
+
tmpNoiseSlider.type = 'range';
|
|
901
|
+
tmpNoiseSlider.className = 'pict-flow-popup-settings-slider';
|
|
902
|
+
tmpNoiseSlider.min = '0';
|
|
903
|
+
tmpNoiseSlider.max = '100';
|
|
904
|
+
tmpNoiseSlider.value = String(Math.round(tmpThemeProvider.getNoiseLevel() * 100));
|
|
905
|
+
|
|
906
|
+
let tmpNoiseValue = document.createElement('span');
|
|
907
|
+
tmpNoiseValue.className = 'pict-flow-popup-settings-slider-value';
|
|
908
|
+
tmpNoiseValue.textContent = tmpNoiseSlider.value + '%';
|
|
909
|
+
|
|
910
|
+
tmpNoiseSlider.addEventListener('input', () =>
|
|
911
|
+
{
|
|
912
|
+
let tmpLevel = parseInt(tmpNoiseSlider.value, 10) / 100;
|
|
913
|
+
tmpNoiseValue.textContent = tmpNoiseSlider.value + '%';
|
|
914
|
+
this._FlowView.setNoiseLevel(tmpLevel);
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
// Prevent popup close on slider interaction
|
|
918
|
+
tmpNoiseSlider.addEventListener('click', (pEvent) => { pEvent.stopPropagation(); });
|
|
919
|
+
tmpNoiseSlider.addEventListener('pointerdown', (pEvent) => { pEvent.stopPropagation(); });
|
|
920
|
+
|
|
921
|
+
tmpNoiseRow.appendChild(tmpNoiseSlider);
|
|
922
|
+
tmpNoiseRow.appendChild(tmpNoiseValue);
|
|
923
|
+
tmpNoiseSection.appendChild(tmpNoiseRow);
|
|
924
|
+
pContainer.appendChild(tmpNoiseSection);
|
|
925
|
+
|
|
926
|
+
// Show/hide noise slider based on active theme
|
|
927
|
+
this._refreshNoiseSlider(pContainer);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Show or hide the noise slider based on whether the active theme supports noise.
|
|
932
|
+
* @param {HTMLElement} pContainer - The settings popup container
|
|
933
|
+
*/
|
|
934
|
+
_refreshNoiseSlider(pContainer)
|
|
935
|
+
{
|
|
936
|
+
let tmpNoiseSection = pContainer.querySelector('[data-settings-type="noise"]');
|
|
937
|
+
if (!tmpNoiseSection) return;
|
|
938
|
+
|
|
939
|
+
let tmpTheme = this._FlowView._ThemeProvider.getActiveTheme();
|
|
940
|
+
if (tmpTheme && tmpTheme.NoiseConfig && tmpTheme.NoiseConfig.Enabled)
|
|
941
|
+
{
|
|
942
|
+
tmpNoiseSection.style.display = '';
|
|
943
|
+
// Update slider value to reflect theme default
|
|
944
|
+
let tmpSlider = tmpNoiseSection.querySelector('.pict-flow-popup-settings-slider');
|
|
945
|
+
let tmpValueLabel = tmpNoiseSection.querySelector('.pict-flow-popup-settings-slider-value');
|
|
946
|
+
if (tmpSlider)
|
|
947
|
+
{
|
|
948
|
+
let tmpLevel = Math.round(this._FlowView._ThemeProvider.getNoiseLevel() * 100);
|
|
949
|
+
tmpSlider.value = String(tmpLevel);
|
|
950
|
+
if (tmpValueLabel) tmpValueLabel.textContent = tmpLevel + '%';
|
|
565
951
|
}
|
|
566
952
|
}
|
|
953
|
+
else
|
|
954
|
+
{
|
|
955
|
+
tmpNoiseSection.style.display = 'none';
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// ── Toolbar Mode Switching ────────────────────────────────────────────
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Switch between docked, floating, and collapsed modes.
|
|
963
|
+
* @param {string} pMode - 'docked' | 'floating' | 'collapsed'
|
|
964
|
+
*/
|
|
965
|
+
_setToolbarMode(pMode)
|
|
966
|
+
{
|
|
967
|
+
// Close any active popup first
|
|
968
|
+
this._closePopup();
|
|
969
|
+
|
|
970
|
+
let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
971
|
+
let tmpBar = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-Bar-${tmpFlowViewIdentifier}`);
|
|
972
|
+
let tmpCollapsed = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-Collapsed-${tmpFlowViewIdentifier}`);
|
|
973
|
+
|
|
974
|
+
switch (pMode)
|
|
975
|
+
{
|
|
976
|
+
case 'docked':
|
|
977
|
+
// Show toolbar bar
|
|
978
|
+
if (tmpBar.length > 0) tmpBar[0].style.display = '';
|
|
979
|
+
// Hide collapsed button
|
|
980
|
+
if (tmpCollapsed.length > 0) tmpCollapsed[0].classList.remove('visible');
|
|
981
|
+
// Hide floating toolbar
|
|
982
|
+
if (this._FloatingToolbarView) this._FloatingToolbarView.hide();
|
|
983
|
+
break;
|
|
984
|
+
|
|
985
|
+
case 'floating':
|
|
986
|
+
// Hide toolbar bar
|
|
987
|
+
if (tmpBar.length > 0) tmpBar[0].style.display = 'none';
|
|
988
|
+
// Hide collapsed button
|
|
989
|
+
if (tmpCollapsed.length > 0) tmpCollapsed[0].classList.remove('visible');
|
|
990
|
+
// Show floating toolbar
|
|
991
|
+
this._showFloatingToolbar();
|
|
992
|
+
break;
|
|
993
|
+
|
|
994
|
+
case 'collapsed':
|
|
995
|
+
// Hide toolbar bar
|
|
996
|
+
if (tmpBar.length > 0) tmpBar[0].style.display = 'none';
|
|
997
|
+
// Show collapsed button
|
|
998
|
+
if (tmpCollapsed.length > 0) tmpCollapsed[0].classList.add('visible');
|
|
999
|
+
// Hide floating toolbar
|
|
1000
|
+
if (this._FloatingToolbarView) this._FloatingToolbarView.hide();
|
|
1001
|
+
break;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
this._ToolbarMode = pMode;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Lazily create and show the floating toolbar.
|
|
1009
|
+
*/
|
|
1010
|
+
_showFloatingToolbar()
|
|
1011
|
+
{
|
|
1012
|
+
if (!this._FlowView) return;
|
|
1013
|
+
|
|
1014
|
+
if (!this._FloatingToolbarView)
|
|
1015
|
+
{
|
|
1016
|
+
let tmpFlowViewIdentifier = this.options.FlowViewIdentifier;
|
|
1017
|
+
this._FloatingToolbarView = this.fable.instantiateServiceProviderWithoutRegistration(
|
|
1018
|
+
'PictViewFlowFloatingToolbar',
|
|
1019
|
+
{
|
|
1020
|
+
FlowViewIdentifier: tmpFlowViewIdentifier,
|
|
1021
|
+
DefaultDestinationAddress: `#Flow-FloatingToolbar-Container-${tmpFlowViewIdentifier}`
|
|
1022
|
+
}
|
|
1023
|
+
);
|
|
1024
|
+
this._FloatingToolbarView._ToolbarView = this;
|
|
1025
|
+
this._FloatingToolbarView._FlowView = this._FlowView;
|
|
1026
|
+
this._FloatingToolbarView.render();
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
this._FloatingToolbarView.show();
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// ── Node Placement Helpers ────────────────────────────────────────────
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Add a node at the center of the visible viewport.
|
|
1036
|
+
* @param {string} pNodeType - The node type hash
|
|
1037
|
+
*/
|
|
1038
|
+
_addNodeAtCenter(pNodeType)
|
|
1039
|
+
{
|
|
1040
|
+
if (!this._FlowView) return;
|
|
1041
|
+
|
|
1042
|
+
let tmpVS = this._FlowView.viewState;
|
|
1043
|
+
|
|
1044
|
+
// Calculate the center of the visible SVG area
|
|
1045
|
+
let tmpSVGContainer = this._FlowView._SVGElement;
|
|
1046
|
+
let tmpWidth = tmpSVGContainer ? tmpSVGContainer.clientWidth : 600;
|
|
1047
|
+
let tmpHeight = tmpSVGContainer ? tmpSVGContainer.clientHeight : 400;
|
|
1048
|
+
|
|
1049
|
+
let tmpCenterX = (-tmpVS.PanX + tmpWidth / 2) / tmpVS.Zoom;
|
|
1050
|
+
let tmpCenterY = (-tmpVS.PanY + tmpHeight / 2) / tmpVS.Zoom;
|
|
1051
|
+
|
|
1052
|
+
// Slight offset to avoid stacking
|
|
1053
|
+
let tmpNodeCount = this._FlowView.flowData.Nodes.length;
|
|
1054
|
+
tmpCenterX += (tmpNodeCount % 5) * 30;
|
|
1055
|
+
tmpCenterY += (tmpNodeCount % 5) * 30;
|
|
1056
|
+
|
|
1057
|
+
this._FlowView.addNode(pNodeType, tmpCenterX, tmpCenterY);
|
|
567
1058
|
}
|
|
568
1059
|
|
|
569
1060
|
/**
|
|
@@ -586,6 +1077,8 @@ class PictViewFlowToolbar extends libPictView
|
|
|
586
1077
|
this._FlowView.addNode(pCardType, tmpX, tmpY);
|
|
587
1078
|
}
|
|
588
1079
|
|
|
1080
|
+
// ── Action Handler ────────────────────────────────────────────────────
|
|
1081
|
+
|
|
589
1082
|
/**
|
|
590
1083
|
* Handle a toolbar action
|
|
591
1084
|
* @param {string} pAction
|
|
@@ -599,27 +1092,7 @@ class PictViewFlowToolbar extends libPictView
|
|
|
599
1092
|
switch (pAction)
|
|
600
1093
|
{
|
|
601
1094
|
case 'add-node':
|
|
602
|
-
|
|
603
|
-
// Get selected node type from dropdown
|
|
604
|
-
let tmpSelectElements = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-NodeType-${tmpFlowViewIdentifier}`);
|
|
605
|
-
let tmpNodeType = 'default';
|
|
606
|
-
if (tmpSelectElements.length > 0)
|
|
607
|
-
{
|
|
608
|
-
tmpNodeType = tmpSelectElements[0].value;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
// Place the new node at a reasonable position
|
|
612
|
-
let tmpVS = this._FlowView.viewState;
|
|
613
|
-
let tmpX = (-tmpVS.PanX + 200) / tmpVS.Zoom;
|
|
614
|
-
let tmpY = (-tmpVS.PanY + 200) / tmpVS.Zoom;
|
|
615
|
-
|
|
616
|
-
// Offset if there are existing nodes to avoid overlap
|
|
617
|
-
let tmpNodeCount = this._FlowView.flowData.Nodes.length;
|
|
618
|
-
tmpX += (tmpNodeCount % 5) * 40;
|
|
619
|
-
tmpY += (tmpNodeCount % 5) * 40;
|
|
620
|
-
|
|
621
|
-
this._FlowView.addNode(tmpNodeType, tmpX, tmpY);
|
|
622
|
-
}
|
|
1095
|
+
this._openPopup('add-node');
|
|
623
1096
|
break;
|
|
624
1097
|
|
|
625
1098
|
case 'delete-selected':
|
|
@@ -642,57 +1115,51 @@ class PictViewFlowToolbar extends libPictView
|
|
|
642
1115
|
this._FlowView.autoLayout();
|
|
643
1116
|
break;
|
|
644
1117
|
|
|
645
|
-
case '
|
|
646
|
-
|
|
647
|
-
let tmpName = window.prompt('Enter a name for this layout:');
|
|
648
|
-
if (tmpName !== null && tmpName.trim() !== '')
|
|
649
|
-
{
|
|
650
|
-
this._FlowView._LayoutProvider.saveLayout(tmpName.trim());
|
|
651
|
-
this._populateLayoutDropdown();
|
|
652
|
-
}
|
|
653
|
-
}
|
|
1118
|
+
case 'cards-popup':
|
|
1119
|
+
this._openPopup('cards');
|
|
654
1120
|
break;
|
|
655
1121
|
|
|
656
|
-
case '
|
|
657
|
-
|
|
658
|
-
let tmpSelectElements = this.pict.ContentAssignment.getElement(
|
|
659
|
-
`#Flow-Toolbar-LayoutSelect-${tmpFlowViewIdentifier}`
|
|
660
|
-
);
|
|
661
|
-
if (tmpSelectElements.length > 0)
|
|
662
|
-
{
|
|
663
|
-
let tmpLayoutHash = tmpSelectElements[0].value;
|
|
664
|
-
if (tmpLayoutHash)
|
|
665
|
-
{
|
|
666
|
-
this._FlowView._LayoutProvider.restoreLayout(tmpLayoutHash);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|
|
1122
|
+
case 'layout-popup':
|
|
1123
|
+
this._openPopup('layout');
|
|
670
1124
|
break;
|
|
671
1125
|
|
|
672
|
-
case '
|
|
1126
|
+
case 'settings-popup':
|
|
1127
|
+
this._openPopup('settings');
|
|
1128
|
+
break;
|
|
1129
|
+
|
|
1130
|
+
case 'toggle-floating':
|
|
1131
|
+
if (this._ToolbarMode === 'floating')
|
|
673
1132
|
{
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
let tmpLayoutHash = tmpSelectElements[0].value;
|
|
680
|
-
if (tmpLayoutHash)
|
|
681
|
-
{
|
|
682
|
-
this._FlowView._LayoutProvider.deleteLayout(tmpLayoutHash);
|
|
683
|
-
this._populateLayoutDropdown();
|
|
684
|
-
}
|
|
685
|
-
}
|
|
1133
|
+
this._setToolbarMode('docked');
|
|
1134
|
+
}
|
|
1135
|
+
else
|
|
1136
|
+
{
|
|
1137
|
+
this._setToolbarMode('floating');
|
|
686
1138
|
}
|
|
687
1139
|
break;
|
|
688
1140
|
|
|
1141
|
+
case 'collapse-toolbar':
|
|
1142
|
+
this._setToolbarMode('collapsed');
|
|
1143
|
+
break;
|
|
1144
|
+
|
|
1145
|
+
case 'expand-toolbar':
|
|
1146
|
+
this._setToolbarMode('docked');
|
|
1147
|
+
break;
|
|
1148
|
+
|
|
689
1149
|
case 'fullscreen':
|
|
690
1150
|
{
|
|
691
1151
|
let tmpIsFullscreen = this._FlowView.toggleFullscreen();
|
|
692
|
-
let
|
|
693
|
-
|
|
1152
|
+
let tmpIconProvider = this._FlowView._IconProvider;
|
|
1153
|
+
let tmpIconElements = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-Fullscreen-Icon-${tmpFlowViewIdentifier}`);
|
|
1154
|
+
if (tmpIconElements.length > 0 && tmpIconProvider)
|
|
1155
|
+
{
|
|
1156
|
+
tmpIconElements[0].innerHTML = tmpIconProvider.getIconSVGMarkup(
|
|
1157
|
+
tmpIsFullscreen ? 'exit-fullscreen' : 'fullscreen', 14);
|
|
1158
|
+
}
|
|
1159
|
+
let tmpFullscreenBtn = this.pict.ContentAssignment.getElement(`#Flow-Toolbar-Fullscreen-${tmpFlowViewIdentifier}`);
|
|
1160
|
+
if (tmpFullscreenBtn.length > 0)
|
|
694
1161
|
{
|
|
695
|
-
|
|
1162
|
+
tmpFullscreenBtn[0].setAttribute('title', tmpIsFullscreen ? 'Exit Fullscreen' : 'Toggle Fullscreen');
|
|
696
1163
|
}
|
|
697
1164
|
}
|
|
698
1165
|
break;
|