html-overlay-node 0.1.6 → 0.1.10
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/dist/example.json +3 -3
- package/dist/html-overlay-node.es.js +997 -1014
- package/dist/html-overlay-node.es.js.map +1 -1
- package/dist/html-overlay-node.umd.js +1 -1
- package/dist/html-overlay-node.umd.js.map +1 -1
- package/index.css +391 -232
- package/package.json +9 -8
- package/readme.md +58 -364
- package/src/core/Edge.js +4 -2
- package/src/core/Graph.js +29 -5
- package/src/core/Node.js +27 -11
- package/src/core/Runner.js +201 -211
- package/src/defaults/contextMenu.js +102 -0
- package/src/defaults/index.js +6 -0
- package/src/index.js +85 -793
- package/src/interact/ContextMenu.js +5 -1
- package/src/interact/Controller.js +73 -46
- package/src/nodes/core.js +266 -0
- package/src/nodes/index.js +42 -0
- package/src/nodes/logic.js +60 -0
- package/src/nodes/math.js +99 -0
- package/src/nodes/util.js +176 -0
- package/src/nodes/value.js +100 -0
- package/src/render/CanvasRenderer.js +784 -604
- package/src/render/HtmlOverlay.js +15 -5
- package/src/render/hitTest.js +18 -9
- package/src/ui/HelpOverlay.js +158 -0
- package/src/ui/PropertyPanel.css +58 -27
- package/src/ui/PropertyPanel.js +441 -268
- package/src/utils/utils.js +4 -4
package/src/ui/PropertyPanel.js
CHANGED
|
@@ -1,268 +1,441 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PropertyPanel - Node property editor panel
|
|
3
|
-
*/
|
|
4
|
-
export class PropertyPanel {
|
|
5
|
-
constructor(container, { graph, hooks, registry, render }) {
|
|
6
|
-
this.container = container;
|
|
7
|
-
this.graph = graph;
|
|
8
|
-
this.hooks = hooks;
|
|
9
|
-
this.registry = registry;
|
|
10
|
-
this.render = render;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
this.
|
|
14
|
-
this.
|
|
15
|
-
|
|
16
|
-
this.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
//
|
|
43
|
-
this.
|
|
44
|
-
this.
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
this.
|
|
59
|
-
this.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
this.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
<div class="field
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
<div class="
|
|
151
|
-
<
|
|
152
|
-
${node.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* PropertyPanel - Node property editor panel
|
|
3
|
+
*/
|
|
4
|
+
export class PropertyPanel {
|
|
5
|
+
constructor(container, { graph, hooks, registry, render }) {
|
|
6
|
+
this.container = container;
|
|
7
|
+
this.graph = graph;
|
|
8
|
+
this.hooks = hooks;
|
|
9
|
+
this.registry = registry;
|
|
10
|
+
this.render = render;
|
|
11
|
+
this._def = null; // current node type definition
|
|
12
|
+
|
|
13
|
+
this.panel = null;
|
|
14
|
+
this.currentNode = null;
|
|
15
|
+
this.isVisible = false;
|
|
16
|
+
this._selfUpdating = false; // prevent re-render loop while user is editing
|
|
17
|
+
|
|
18
|
+
this._createPanel();
|
|
19
|
+
this._bindHooks();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
_bindHooks() {
|
|
23
|
+
// Refresh when edges change
|
|
24
|
+
this.hooks?.on('edge:create', () => {
|
|
25
|
+
if (this._canRefresh()) this._renderContent();
|
|
26
|
+
});
|
|
27
|
+
this.hooks?.on('edge:delete', () => {
|
|
28
|
+
if (this._canRefresh()) this._renderContent();
|
|
29
|
+
});
|
|
30
|
+
// Refresh when node state changes externally
|
|
31
|
+
this.hooks?.on('node:updated', (node) => {
|
|
32
|
+
if (this._canRefresh() && this.currentNode?.id === node?.id && !this._selfUpdating) {
|
|
33
|
+
this._renderContent();
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
// Refresh position fields when node moves
|
|
37
|
+
this.hooks?.on('node:move', (node) => {
|
|
38
|
+
if (this._canRefresh() && this.currentNode?.id === node?.id) {
|
|
39
|
+
this._updatePositionFields();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
// Refresh live values on every runner tick (lightweight DOM update)
|
|
43
|
+
this.hooks?.on('runner:tick', () => {
|
|
44
|
+
if (this._canRefresh()) this._updateLiveValues();
|
|
45
|
+
});
|
|
46
|
+
this.hooks?.on('runner:stop', () => {
|
|
47
|
+
if (this._canRefresh()) this._updateLiveValues();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_canRefresh() {
|
|
52
|
+
if (!this.isVisible || !this.currentNode) return false;
|
|
53
|
+
// Don't clobber in-progress edits
|
|
54
|
+
return !this.panel.querySelector('[data-field]:focus');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_createPanel() {
|
|
58
|
+
this.panel = document.createElement('div');
|
|
59
|
+
this.panel.className = 'property-panel';
|
|
60
|
+
this.panel.style.display = 'none';
|
|
61
|
+
|
|
62
|
+
this.panel.innerHTML = `
|
|
63
|
+
<div class="panel-inner">
|
|
64
|
+
<div class="panel-header">
|
|
65
|
+
<div class="panel-title">
|
|
66
|
+
<span class="title-text">Node Properties</span>
|
|
67
|
+
</div>
|
|
68
|
+
<button class="panel-close" type="button">×</button>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="panel-content">
|
|
71
|
+
<!-- Content will be dynamically generated -->
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
this.container.appendChild(this.panel);
|
|
77
|
+
|
|
78
|
+
this.panel.querySelector('.panel-close').addEventListener('click', () => {
|
|
79
|
+
this.close();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
document.addEventListener('keydown', (e) => {
|
|
83
|
+
if (e.key === 'Escape' && this.isVisible) {
|
|
84
|
+
this.close();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
open(node) {
|
|
90
|
+
if (!node) return;
|
|
91
|
+
this.currentNode = node;
|
|
92
|
+
this._def = this.registry?.types?.get(node.type) || null;
|
|
93
|
+
this.isVisible = true;
|
|
94
|
+
this._renderContent();
|
|
95
|
+
this.panel.style.display = 'block';
|
|
96
|
+
this.panel.classList.add('panel-visible');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
close() {
|
|
100
|
+
this.isVisible = false;
|
|
101
|
+
this.panel.classList.remove('panel-visible');
|
|
102
|
+
setTimeout(() => {
|
|
103
|
+
this.panel.style.display = 'none';
|
|
104
|
+
this.currentNode = null;
|
|
105
|
+
}, 200);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
_renderContent() {
|
|
109
|
+
const node = this.currentNode;
|
|
110
|
+
if (!node) return;
|
|
111
|
+
|
|
112
|
+
const content = this.panel.querySelector('.panel-content');
|
|
113
|
+
content.innerHTML = `
|
|
114
|
+
<div class="section">
|
|
115
|
+
<div class="section-title">Basic Info</div>
|
|
116
|
+
<div class="section-body">
|
|
117
|
+
<div class="field">
|
|
118
|
+
<label>Type</label>
|
|
119
|
+
<input type="text" value="${node.type}" readonly />
|
|
120
|
+
</div>
|
|
121
|
+
<div class="field">
|
|
122
|
+
<label>Title</label>
|
|
123
|
+
<input type="text" data-field="title" value="${node.title || ''}" />
|
|
124
|
+
</div>
|
|
125
|
+
<div class="field">
|
|
126
|
+
<label>ID</label>
|
|
127
|
+
<input type="text" value="${node.id}" readonly />
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div class="section">
|
|
133
|
+
<div class="section-title">Position & Size</div>
|
|
134
|
+
<div class="section-body">
|
|
135
|
+
<div class="field-row">
|
|
136
|
+
<div class="field">
|
|
137
|
+
<label>X</label>
|
|
138
|
+
<input type="number" data-field="x" value="${Math.round(node.computed.x)}" />
|
|
139
|
+
</div>
|
|
140
|
+
<div class="field">
|
|
141
|
+
<label>Y</label>
|
|
142
|
+
<input type="number" data-field="y" value="${Math.round(node.computed.y)}" />
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="field-row">
|
|
146
|
+
<div class="field">
|
|
147
|
+
<label>Width</label>
|
|
148
|
+
<input type="number" data-field="width" value="${node.computed.w}" />
|
|
149
|
+
</div>
|
|
150
|
+
<div class="field">
|
|
151
|
+
<label>Height</label>
|
|
152
|
+
<input type="number" data-field="height" value="${node.computed.h}" />
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
${this._renderConnections(node)}
|
|
159
|
+
${this._renderPorts(node)}
|
|
160
|
+
${this._renderLiveValues(node)}
|
|
161
|
+
${this._renderState(node)}
|
|
162
|
+
|
|
163
|
+
<div class="panel-actions">
|
|
164
|
+
<button class="btn-secondary panel-close-btn">Close</button>
|
|
165
|
+
</div>
|
|
166
|
+
`;
|
|
167
|
+
|
|
168
|
+
this._attachInputListeners();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
_renderConnections(node) {
|
|
172
|
+
const edges = [...this.graph.edges.values()];
|
|
173
|
+
const incoming = edges.filter(e => e.toNode === node.id);
|
|
174
|
+
const outgoing = edges.filter(e => e.fromNode === node.id);
|
|
175
|
+
|
|
176
|
+
if (!incoming.length && !outgoing.length) return '';
|
|
177
|
+
|
|
178
|
+
const edgeLabel = (e, dir) => {
|
|
179
|
+
const otherId = dir === 'in' ? e.fromNode : e.toNode;
|
|
180
|
+
const other = this.graph.nodes.get(otherId);
|
|
181
|
+
return `<div class="port-item">
|
|
182
|
+
<span class="port-icon data"></span>
|
|
183
|
+
<span class="port-name" style="font-size:10px;color:#5a5a78;">${other?.title ?? otherId}</span>
|
|
184
|
+
</div>`;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
return `
|
|
188
|
+
<div class="section">
|
|
189
|
+
<div class="section-title">Connections</div>
|
|
190
|
+
<div class="section-body">
|
|
191
|
+
${incoming.length ? `
|
|
192
|
+
<div class="port-group">
|
|
193
|
+
<div class="port-group-title">Incoming (${incoming.length})</div>
|
|
194
|
+
${incoming.map(e => edgeLabel(e, 'in')).join('')}
|
|
195
|
+
</div>` : ''}
|
|
196
|
+
${outgoing.length ? `
|
|
197
|
+
<div class="port-group">
|
|
198
|
+
<div class="port-group-title">Outgoing (${outgoing.length})</div>
|
|
199
|
+
${outgoing.map(e => edgeLabel(e, 'out')).join('')}
|
|
200
|
+
</div>` : ''}
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
_renderLiveValues(node) {
|
|
207
|
+
// Show live runtime values from the graph buffer (inputs & outputs)
|
|
208
|
+
const cur = this.graph?._curBuf?.();
|
|
209
|
+
if (!cur) return '';
|
|
210
|
+
|
|
211
|
+
const lines = [];
|
|
212
|
+
|
|
213
|
+
for (const input of node.inputs) {
|
|
214
|
+
const key = `${node.id}:${input.id}`;
|
|
215
|
+
// For inputs: look at the connected upstream node's output
|
|
216
|
+
for (const edge of this.graph.edges.values()) {
|
|
217
|
+
if (edge.toNode === node.id && edge.toPort === input.id) {
|
|
218
|
+
const upKey = `${edge.fromNode}:${edge.fromPort}`;
|
|
219
|
+
const val = cur.get(upKey);
|
|
220
|
+
if (val !== undefined) {
|
|
221
|
+
lines.push(`<div class="port-item">
|
|
222
|
+
<span class="port-icon data"></span>
|
|
223
|
+
<span class="port-name">↳ ${input.name}</span>
|
|
224
|
+
<span class="port-type" style="color:var(--color-primary);background:rgba(99,102,241,0.1);">${JSON.stringify(val)}</span>
|
|
225
|
+
</div>`);
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (const output of node.outputs) {
|
|
233
|
+
const key = `${node.id}:${output.id}`;
|
|
234
|
+
const val = cur.get(key);
|
|
235
|
+
if (val !== undefined) {
|
|
236
|
+
lines.push(`<div class="port-item">
|
|
237
|
+
<span class="port-icon exec" style="background:#10b981;"></span>
|
|
238
|
+
<span class="port-name">↳ ${output.name}</span>
|
|
239
|
+
<span class="port-type" style="color:#10b981;background:rgba(16,185,129,0.1);">${JSON.stringify(val)}</span>
|
|
240
|
+
</div>`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!lines.length) return '';
|
|
245
|
+
|
|
246
|
+
return `
|
|
247
|
+
<div class="section">
|
|
248
|
+
<div class="section-title">Live Values</div>
|
|
249
|
+
<div class="section-body">
|
|
250
|
+
${lines.join('')}
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
_renderPorts(node) {
|
|
257
|
+
if (!node.inputs.length && !node.outputs.length) return '';
|
|
258
|
+
|
|
259
|
+
return `
|
|
260
|
+
<div class="section">
|
|
261
|
+
<div class="section-title">Ports</div>
|
|
262
|
+
<div class="section-body">
|
|
263
|
+
${node.inputs.length ? `
|
|
264
|
+
<div class="port-group">
|
|
265
|
+
<div class="port-group-title">Inputs (${node.inputs.length})</div>
|
|
266
|
+
${node.inputs.map(p => `
|
|
267
|
+
<div class="port-item">
|
|
268
|
+
<span class="port-icon ${p.portType || 'data'}"></span>
|
|
269
|
+
<span class="port-name">${p.name}</span>
|
|
270
|
+
${p.datatype ? `<span class="port-type">${p.datatype}</span>` : ''}
|
|
271
|
+
</div>
|
|
272
|
+
`).join('')}
|
|
273
|
+
</div>
|
|
274
|
+
` : ''}
|
|
275
|
+
${node.outputs.length ? `
|
|
276
|
+
<div class="port-group">
|
|
277
|
+
<div class="port-group-title">Outputs (${node.outputs.length})</div>
|
|
278
|
+
${node.outputs.map(p => `
|
|
279
|
+
<div class="port-item">
|
|
280
|
+
<span class="port-icon ${p.portType || 'data'}"></span>
|
|
281
|
+
<span class="port-name">${p.name}</span>
|
|
282
|
+
${p.datatype ? `<span class="port-type">${p.datatype}</span>` : ''}
|
|
283
|
+
</div>
|
|
284
|
+
`).join('')}
|
|
285
|
+
</div>
|
|
286
|
+
` : ''}
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
_renderState(node) {
|
|
293
|
+
if (!node.state) return '';
|
|
294
|
+
|
|
295
|
+
// Only show primitive, non-private keys
|
|
296
|
+
const entries = Object.entries(node.state).filter(([key, value]) => {
|
|
297
|
+
if (key.startsWith('_')) return false;
|
|
298
|
+
const t = typeof value;
|
|
299
|
+
return t === 'string' || t === 'number' || t === 'boolean';
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
if (!entries.length) return '';
|
|
303
|
+
|
|
304
|
+
const fieldHtml = ([key, value]) => {
|
|
305
|
+
if (typeof value === 'boolean') {
|
|
306
|
+
return `
|
|
307
|
+
<div class="field">
|
|
308
|
+
<label>${key}</label>
|
|
309
|
+
<select data-field="state.${key}">
|
|
310
|
+
<option value="true"${value ? ' selected' : ''}>true</option>
|
|
311
|
+
<option value="false"${!value ? ' selected' : ''}>false</option>
|
|
312
|
+
</select>
|
|
313
|
+
</div>`;
|
|
314
|
+
}
|
|
315
|
+
return `
|
|
316
|
+
<div class="field">
|
|
317
|
+
<label>${key}</label>
|
|
318
|
+
<input type="${typeof value === 'number' ? 'number' : 'text'}"
|
|
319
|
+
data-field="state.${key}"
|
|
320
|
+
value="${value}" />
|
|
321
|
+
</div>`;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
return `
|
|
325
|
+
<div class="section">
|
|
326
|
+
<div class="section-title">State</div>
|
|
327
|
+
<div class="section-body">
|
|
328
|
+
${entries.map(fieldHtml).join('')}
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
_attachInputListeners() {
|
|
335
|
+
this.panel.querySelectorAll('[data-field]').forEach(input => {
|
|
336
|
+
input.addEventListener('change', () => {
|
|
337
|
+
this._selfUpdating = true;
|
|
338
|
+
this._handleFieldChange(input.dataset.field, input.value);
|
|
339
|
+
this._selfUpdating = false;
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
this.panel.querySelector('.panel-close-btn')?.addEventListener('click', () => {
|
|
344
|
+
this.close();
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
_handleFieldChange(field, value) {
|
|
349
|
+
const node = this.currentNode;
|
|
350
|
+
if (!node) return;
|
|
351
|
+
|
|
352
|
+
switch (field) {
|
|
353
|
+
case 'title':
|
|
354
|
+
node.title = value;
|
|
355
|
+
break;
|
|
356
|
+
case 'x':
|
|
357
|
+
node.pos.x = parseFloat(value);
|
|
358
|
+
this.graph.updateWorldTransforms();
|
|
359
|
+
break;
|
|
360
|
+
case 'y':
|
|
361
|
+
node.pos.y = parseFloat(value);
|
|
362
|
+
this.graph.updateWorldTransforms();
|
|
363
|
+
break;
|
|
364
|
+
case 'width':
|
|
365
|
+
node.size.width = parseFloat(value);
|
|
366
|
+
break;
|
|
367
|
+
case 'height':
|
|
368
|
+
node.size.height = parseFloat(value);
|
|
369
|
+
break;
|
|
370
|
+
default:
|
|
371
|
+
if (field.startsWith('state.')) {
|
|
372
|
+
const key = field.substring(6);
|
|
373
|
+
if (node.state && key in node.state) {
|
|
374
|
+
const orig = node.state[key];
|
|
375
|
+
if (typeof orig === 'boolean') {
|
|
376
|
+
node.state[key] = value === 'true';
|
|
377
|
+
} else if (typeof orig === 'number') {
|
|
378
|
+
node.state[key] = parseFloat(value);
|
|
379
|
+
} else {
|
|
380
|
+
node.state[key] = value;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
this.hooks?.emit('node:updated', node);
|
|
387
|
+
this.render?.();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Lightweight update of position fields only (no full re-render) */
|
|
391
|
+
_updatePositionFields() {
|
|
392
|
+
const node = this.currentNode;
|
|
393
|
+
if (!node) return;
|
|
394
|
+
const xEl = this.panel.querySelector('[data-field="x"]');
|
|
395
|
+
const yEl = this.panel.querySelector('[data-field="y"]');
|
|
396
|
+
if (xEl) xEl.value = Math.round(node.computed.x);
|
|
397
|
+
if (yEl) yEl.value = Math.round(node.computed.y);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Lightweight in-place update of the Live Values section */
|
|
401
|
+
_updateLiveValues() {
|
|
402
|
+
const node = this.currentNode;
|
|
403
|
+
if (!node) return;
|
|
404
|
+
|
|
405
|
+
const cur = this.graph?._curBuf?.();
|
|
406
|
+
if (!cur) return;
|
|
407
|
+
|
|
408
|
+
// Find or create the live values section container
|
|
409
|
+
let section = this.panel.querySelector('.live-values-section');
|
|
410
|
+
const newHtml = this._renderLiveValues(node);
|
|
411
|
+
|
|
412
|
+
if (!newHtml) {
|
|
413
|
+
// No live values — remove section if present
|
|
414
|
+
if (section) section.remove();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const wrapper = document.createElement('div');
|
|
419
|
+
wrapper.innerHTML = newHtml;
|
|
420
|
+
const newSection = wrapper.firstElementChild;
|
|
421
|
+
newSection.classList.add('live-values-section');
|
|
422
|
+
|
|
423
|
+
if (section) {
|
|
424
|
+
section.replaceWith(newSection);
|
|
425
|
+
} else {
|
|
426
|
+
// Insert before the State section, or before panel-actions
|
|
427
|
+
const stateSection = this.panel.querySelectorAll('.section');
|
|
428
|
+
const actions = this.panel.querySelector('.panel-actions');
|
|
429
|
+
// insert as second-to-last section (before actions)
|
|
430
|
+
if (actions) {
|
|
431
|
+
actions.before(newSection);
|
|
432
|
+
} else {
|
|
433
|
+
this.panel.querySelector('.panel-content').appendChild(newSection);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
destroy() {
|
|
439
|
+
this.panel?.remove();
|
|
440
|
+
}
|
|
441
|
+
}
|