@node-projects/web-component-designer-visualization-addons 0.1.144 → 0.1.146

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.
@@ -1,80 +1,341 @@
1
- import { BaseCustomWebComponentConstructorAppend, css, html } from "@node-projects/base-custom-webcomponent";
2
- import { ContextMenu } from "@node-projects/web-component-designer";
3
- import { typeInfoFromJsonSchema } from "@node-projects/propertygrid.webcomponent";
1
+ import { BaseCustomWebComponentConstructorAppend, TypedEvent, css, html, } from "@node-projects/base-custom-webcomponent";
2
+ import { ContextMenu, assetsPath, } from "@node-projects/web-component-designer";
3
+ import { typeInfoFromJsonSchema, } from "@node-projects/propertygrid.webcomponent";
4
4
  import { defaultOptions } from "@node-projects/web-component-designer-widgets-wunderbaum";
5
- import { Wunderbaum } from 'wunderbaum';
5
+ import { Wunderbaum } from "wunderbaum";
6
6
  //@ts-ignore
7
- import wunderbaumStyle from 'wunderbaum/dist/wunderbaum.css' with { type: 'css' };
7
+ import wunderbaumStyle from "wunderbaum/dist/wunderbaum.css" with { type: "css" };
8
8
  import { VisualizationPropertyGrid } from "./VisualizationPropertyGrid.js";
9
- import '@node-projects/splitview.webcomponent';
9
+ import { SimpleScriptCommandPicker } from "./SimpleScriptCommandPicker.js";
10
+ import { simpleScriptEditorHelpHtml } from "./SimpleScriptEditorHelp.js";
11
+ import { nativeScriptCommandDescriptions, } from "../scripting/ScriptCommandsDescriptions.js";
12
+ import "@node-projects/splitview.webcomponent";
10
13
  import { ScriptUpgrades } from "../scripting/ScriptUpgrader.js";
14
+ const SAME_TREE_MOVE_MIME = "application/x-simple-script-move";
11
15
  export class SimpleScriptEditor extends BaseCustomWebComponentConstructorAppend {
12
16
  static style = css `
13
- :host {
14
- background: white;
15
- }
16
- .list{
17
- display: grid;
18
- grid-template-columns: 1fr 40px;
19
- width: calc(100% - 6px);
20
- box-sizing: border-box;
21
- margin: 3px;
22
- }
17
+ :host {
18
+ background: white;
19
+ }
23
20
 
24
- .list button{
25
- padding: 5px 10px;
26
- box-sizing: border-box;
27
- display: flex;
28
- justify-content: center;
29
- margin-right: 5px;
30
- margin-left: 5px;
31
- }
21
+ .root {
22
+ width: 100%;
23
+ height: 100%;
24
+ overflow: hidden;
25
+ display: flex;
26
+ flex-direction: column;
27
+ box-sizing: border-box;
28
+ }
32
29
 
33
- node-projects-visualization-property-grid {
34
- width: 100%;
35
- height: 100%;
36
- }
37
- `;
30
+ .content {
31
+ flex: 1 1 auto;
32
+ min-height: 0;
33
+ position: relative;
34
+ }
35
+
36
+ .split-view {
37
+ height: calc(100% - 4px);
38
+ width: calc(100% - 4px);
39
+ position: relative;
40
+ margin: 2px;
41
+ }
42
+
43
+ .list-pane {
44
+ width: 48%;
45
+ height: 100%;
46
+ position: relative;
47
+ box-sizing: border-box;
48
+ display: flex;
49
+ flex-direction: column;
50
+ }
51
+
52
+ .list-content {
53
+ flex: 1 1 auto;
54
+ min-height: 0;
55
+ position: relative;
56
+ }
57
+
58
+ .grid-pane {
59
+ width: 52%;
60
+ height: 100%;
61
+ position: relative;
62
+ box-sizing: border-box;
63
+ }
64
+
65
+ .split-view.code-mode .grid-pane {
66
+ display: none;
67
+ }
68
+
69
+ .split-view.code-mode::part(splitter) {
70
+ display: none;
71
+ }
72
+
73
+ #commandList {
74
+ position: absolute;
75
+ inset: 0;
76
+ overflow-x: hidden;
77
+ overflow-y: auto;
78
+ box-sizing: border-box;
79
+ }
80
+
81
+ #commandList i.wb-expander,
82
+ #commandList i.wb-indent {
83
+ display: none;
84
+ }
85
+
86
+ #commandList.hidden {
87
+ display: none;
88
+ }
89
+
90
+ .code-view {
91
+ position: absolute;
92
+ inset: 0;
93
+ display: none;
94
+ }
95
+
96
+ .code-view.visible {
97
+ display: block;
98
+ }
99
+
100
+ node-projects-visualization-property-grid {
101
+ width: 100%;
102
+ height: 100%;
103
+ }
104
+
105
+ .toolbar {
106
+ display: flex;
107
+ align-items: center;
108
+ gap: 8px;
109
+ box-sizing: border-box;
110
+ flex: 0 0 auto;
111
+ width: 100%;
112
+ height: 36px;
113
+ padding: 4px 8px;
114
+ border-top: 1px solid #ccc;
115
+ background-color: rgb(230, 230, 230);
116
+ }
117
+
118
+ .toolbar button {
119
+ box-sizing: border-box;
120
+ padding: 6px 16px;
121
+ border-radius: 4px;
122
+ border: 1px solid #b0b0b0;
123
+ background: white;
124
+ font-size: 12px;
125
+ cursor: pointer;
126
+ }
127
+
128
+ .toolbar button:hover:not(:disabled) {
129
+ background: #f0f0f0;
130
+ border-color: #909090;
131
+ }
132
+
133
+ .toolbar button:active:not(:disabled) {
134
+ background: #e2e2e2;
135
+ }
136
+
137
+ .toolbar button:disabled {
138
+ opacity: 0.5;
139
+ cursor: not-allowed;
140
+ }
141
+
142
+ .toolbar button.primary {
143
+ background: #2c6e9b;
144
+ border-color: #2c6e9b;
145
+ color: white;
146
+ }
147
+
148
+ .toolbar button.primary:hover:not(:disabled) {
149
+ background: #255d83;
150
+ border-color: #255d83;
151
+ }
152
+
153
+ .toolbar button.primary:active:not(:disabled) {
154
+ background: #1e4d6b;
155
+ }
156
+
157
+ .toolbar .spacer {
158
+ flex: 1 1 auto;
159
+ }
160
+
161
+ .toolbar .icon-btn {
162
+ display: flex;
163
+ align-items: center;
164
+ justify-content: center;
165
+ width: 22px;
166
+ height: 22px;
167
+ padding: 0;
168
+ box-sizing: border-box;
169
+ border: none;
170
+ background: transparent;
171
+ cursor: pointer;
172
+ }
173
+
174
+ .toolbar .icon-btn:hover {
175
+ background: #eee;
176
+ }
177
+
178
+ .drag-handle {
179
+ position: absolute;
180
+ left: 0;
181
+ top: 0;
182
+ height: 100%;
183
+ width: 24px;
184
+ display: flex;
185
+ align-items: center;
186
+ justify-content: center;
187
+ color: #888;
188
+ cursor: grab;
189
+ }
190
+
191
+ .cmd {
192
+ position: absolute;
193
+ right: 0;
194
+ top: 0;
195
+ height: 100%;
196
+ display: flex;
197
+ align-items: center;
198
+ gap: 2px;
199
+ padding: 0 2px;
200
+ background: white;
201
+ }
202
+
203
+ .cmd img {
204
+ width: 12px;
205
+ height: 12px;
206
+ cursor: pointer;
207
+ }
208
+ `;
38
209
  static template = html `
39
- <div style="width:100%; height:100%; overflow: hidden;">
40
- <node-projects-split-view style="height: 100%; width: 100%; position: relative;" orientation="horizontal">
41
- <div style="width: 40%; position: relative;">
42
- <div style="width:calc(100% - 4px); height:calc(100% - 4px)">
43
- <div id="commandList" style="overflow-x: hidden; overflow-y: auto; width:100%; height: calc(100% - 34px);"></div>
44
- <div class="list">
45
- <select id="possibleCommands" style="width: 100%"></select>
46
- <button @click="[[this.addItem()]]">Add</button>
47
- </div>
48
- </div>
49
- </div>
50
- <div style="width: 60%; position: relative;">
51
- <node-projects-visualization-property-grid id="propertygrid"></node-projects-visualization-property-grid>
52
- </div>
53
- </node-projects-split-view>
54
- </div>
55
- `;
56
- static is = 'node-projects-visualization-simple-script-editor';
210
+ <div class="root">
211
+ <div class="content">
212
+ <node-projects-split-view
213
+ id="splitView"
214
+ class="split-view"
215
+ orientation="horizontal"
216
+ >
217
+ <div id="listPane" class="list-pane">
218
+ <div id="listContent" class="list-content">
219
+ <div id="commandList"></div>
220
+ <node-projects-code-view-monaco
221
+ id="codeView"
222
+ class="code-view"
223
+ language="json"
224
+ ></node-projects-code-view-monaco>
225
+ </div>
226
+ <div class="toolbar">
227
+ <button
228
+ id="addCommandBtn"
229
+ class="primary"
230
+ @click="[[this.addCommand()]]"
231
+ >
232
+ Add Command
233
+ </button>
234
+ <button id="codeToggleBtn" @click="[[this.toggleCodeView()]]">
235
+ Code
236
+ </button>
237
+ <span class="spacer"></span>
238
+ <button
239
+ class="icon-btn"
240
+ title="Help"
241
+ @click="[[this.helpClicked()]]"
242
+ >
243
+ <svg viewBox="0 0 512 512" width="16" height="16">
244
+ <path
245
+ d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"
246
+ />
247
+ </svg>
248
+ </button>
249
+ </div>
250
+ </div>
251
+ <div class="grid-pane">
252
+ <node-projects-visualization-property-grid
253
+ id="propertygrid"
254
+ ></node-projects-visualization-property-grid>
255
+ </div>
256
+ </node-projects-split-view>
257
+ </div>
258
+ </div>
259
+ `;
260
+ static is = "node-projects-visualization-simple-script-editor";
261
+ _title;
262
+ get title() {
263
+ return this._title;
264
+ }
265
+ set title(value) {
266
+ this._title = value;
267
+ }
57
268
  serviceContainer;
58
269
  instanceServiceContainer;
59
270
  visualizationHandler;
60
271
  visualizationShell;
61
272
  scriptCommandsTypeInfo;
62
273
  propertiesTypeInfo;
274
+ helpRequested = new TypedEvent();
275
+ commandDescriptions = {
276
+ ...nativeScriptCommandDescriptions,
277
+ };
63
278
  _script;
64
279
  _commandListDiv;
65
280
  _commandListFancyTree;
66
- _possibleCommands;
67
281
  _propertygrid;
282
+ _splitView;
283
+ _codeView;
284
+ _codeToggleBtn;
285
+ _addCommandBtn;
286
+ _showingCode = false;
287
+ _dragFromHandle = false;
288
+ _draggedNode = null;
68
289
  constructor() {
69
290
  super();
70
291
  this._restoreCachedInititalValues();
71
- this._commandListDiv = this._getDomElement('commandList');
72
- this._possibleCommands = this._getDomElement('possibleCommands');
73
- this._propertygrid = this._getDomElement('propertygrid');
74
- this.shadowRoot.adoptedStyleSheets = [wunderbaumStyle, SimpleScriptEditor.style];
292
+ this._commandListDiv = this._getDomElement("commandList");
293
+ this._propertygrid =
294
+ this._getDomElement("propertygrid");
295
+ this._splitView = this._getDomElement("splitView");
296
+ this._codeView = this._getDomElement("codeView");
297
+ this._codeToggleBtn =
298
+ this._getDomElement("codeToggleBtn");
299
+ this._addCommandBtn =
300
+ this._getDomElement("addCommandBtn");
301
+ const listContent = this._getDomElement("listContent");
302
+ listContent.addEventListener("mousedown", (ev) => {
303
+ this._dragFromHandle = !!ev.target?.closest(".drag-handle");
304
+ });
305
+ document.addEventListener("dragend", () => {
306
+ this._draggedNode = null;
307
+ }, true);
308
+ listContent.addEventListener("dragover", (ev) => {
309
+ ev.preventDefault();
310
+ if (ev.dataTransfer) {
311
+ const isMove = ev.dataTransfer.types.includes(SAME_TREE_MOVE_MIME);
312
+ ev.dataTransfer.dropEffect = isMove ? "move" : "copy";
313
+ }
314
+ });
315
+ listContent.addEventListener("drop", (ev) => {
316
+ ev.preventDefault();
317
+ const isMove = ev.dataTransfer?.types.includes(SAME_TREE_MOVE_MIME);
318
+ if (isMove && this._draggedNode) {
319
+ const children = this._commandListFancyTree.root.children;
320
+ const last = children?.[children.length - 1];
321
+ if (last)
322
+ this._draggedNode.moveTo(last, "after");
323
+ this._draggedNode.setClass("wb-drag-source", false);
324
+ }
325
+ else {
326
+ const type = ev.dataTransfer?.getData("text/plain");
327
+ if (type) {
328
+ const command = { type };
329
+ this._commandListFancyTree.root.addChildren(this.createTreeItem(command));
330
+ }
331
+ }
332
+ });
333
+ this.shadowRoot.adoptedStyleSheets = [
334
+ wunderbaumStyle,
335
+ SimpleScriptEditor.style,
336
+ ];
75
337
  }
76
338
  async ready() {
77
- this.addPossibleCommands();
78
339
  this._parseAttributesToProperties();
79
340
  this._bindingsParse(null, true);
80
341
  this._assignEvents();
@@ -84,29 +345,35 @@ export class SimpleScriptEditor extends BaseCustomWebComponentConstructorAppend
84
345
  pg.visualizationShell = this.visualizationShell;
85
346
  pg.serviceContainer = this.serviceContainer;
86
347
  pg.instanceServiceContainer = this.instanceServiceContainer;
87
- pg.bindableObjectsTarget = 'script';
348
+ pg.bindableObjectsTarget = "script";
88
349
  pg.getTypeInfo = (obj, type) => typeInfoFromJsonSchema(this.propertiesTypeInfo, obj, type);
89
350
  pg.showHead = false;
90
- pg.typeName = 'IScriptMultiplexValue';
351
+ pg.typeName = "IScriptMultiplexValue";
91
352
  pg.title = 'Complex for "' + data.propertyPath + '"';
92
- if (typeof data.value === 'object')
353
+ if (typeof data.value === "object")
93
354
  pg.selectedObject = data.value ?? {};
94
355
  else
95
356
  pg.selectedObject = {};
96
357
  pg.bindingDoubleClicked = (b) => {
97
- if (b.bindabletype == 'property') {
98
- pg.setPropertyValue('source', 'property');
358
+ if (b.bindabletype == "property") {
359
+ pg.setPropertyValue("source", "property");
99
360
  }
100
- else if (b.bindabletype == 'context') {
101
- pg.setPropertyValue('source', 'context');
361
+ else if (b.bindabletype == "context") {
362
+ pg.setPropertyValue("source", "context");
102
363
  }
103
364
  else {
104
- pg.setPropertyValue('source', 'signal');
365
+ pg.setPropertyValue("source", "signal");
105
366
  }
106
- pg.setPropertyValue('name', b.fullName);
367
+ pg.setPropertyValue("name", b.fullName);
107
368
  pg.refresh();
108
369
  };
109
- let res = await this.visualizationShell.openConfirmation(pg, { x: 100, y: 100, width: 400, height: 500, parent: this });
370
+ let res = await this.visualizationShell.openConfirmation(pg, {
371
+ x: 100,
372
+ y: 100,
373
+ width: 400,
374
+ height: 500,
375
+ parent: this,
376
+ });
110
377
  if (res) {
111
378
  this._propertygrid.setPropertyValue(data.propertyPath, pg.selectedObject);
112
379
  this._propertygrid.refresh();
@@ -116,117 +383,167 @@ export class SimpleScriptEditor extends BaseCustomWebComponentConstructorAppend
116
383
  this._propertygrid.visualizationShell = this.visualizationShell;
117
384
  this._propertygrid.serviceContainer = this.serviceContainer;
118
385
  this._propertygrid.instanceServiceContainer = this.instanceServiceContainer;
119
- this._propertygrid.bindableObjectsTarget = 'script';
386
+ this._propertygrid.bindableObjectsTarget = "script";
120
387
  this._propertygrid.getTypeInfo = (obj, type) => typeInfoFromJsonSchema(this.scriptCommandsTypeInfo, obj, type);
121
388
  this._propertygrid.getSpecialEditorForType = async (property, currentValue, propertyPath, wbRender, additionalInfo) => {
122
389
  //@ts-ignore
123
390
  if (!property.specialAllreadyAdded) {
124
391
  //@ts-ignore
125
392
  property.specialAllreadyAdded = true;
126
- if (property.format === 'collection') {
393
+ if (property.format === "collection") {
127
394
  //TODO: create a collection edt. in property grid control used
128
395
  }
129
- else if ((typeof currentValue === 'object' && currentValue !== null) || property.format === 'complex') {
130
- let rB = document.createElement('button');
131
- rB.style.height = 'calc(100% - 6px)';
132
- rB.style.position = 'relative';
133
- rB.style.display = 'flex';
134
- rB.style.justifyContent = 'center';
135
- rB.style.width = '20px';
136
- rB.style.boxSizing = 'content-box';
137
- rB.innerText = 'del';
396
+ else if ((typeof currentValue === "object" && currentValue !== null) ||
397
+ property.format === "complex") {
398
+ let rB = document.createElement("button");
399
+ rB.style.height = "calc(100% - 6px)";
400
+ rB.style.position = "relative";
401
+ rB.style.display = "flex";
402
+ rB.style.justifyContent = "center";
403
+ rB.style.width = "20px";
404
+ rB.style.boxSizing = "content-box";
405
+ rB.innerText = "del";
138
406
  rB.onclick = () => {
139
407
  this._propertygrid.setPropertyValue(propertyPath, undefined);
140
408
  this._propertygrid.refresh();
141
409
  };
142
- wbRender.nodeElem.insertAdjacentElement('afterbegin', rB);
143
- let d = document.createElement('div');
144
- d.style.display = 'flex';
145
- let sp = document.createElement('span');
410
+ wbRender.nodeElem.insertAdjacentElement("afterbegin", rB);
411
+ let d = document.createElement("div");
412
+ d.style.display = "flex";
413
+ let sp = document.createElement("span");
146
414
  if (currentValue)
147
- sp.innerText = (currentValue.source ?? '') + ': ' + (currentValue.name ?? '');
415
+ sp.innerText =
416
+ (currentValue.source ?? "") + ": " + (currentValue.name ?? "");
148
417
  else
149
- sp.innerText = '';
150
- sp.style.overflow = 'hidden';
151
- sp.style.whiteSpace = 'nowrap';
152
- sp.style.textOverflow = 'ellipsis';
153
- sp.style.flexGrow = '1';
418
+ sp.innerText = "";
419
+ sp.style.overflow = "hidden";
420
+ sp.style.whiteSpace = "nowrap";
421
+ sp.style.textOverflow = "ellipsis";
422
+ sp.style.flexGrow = "1";
154
423
  sp.title = JSON.stringify(currentValue);
155
424
  d.appendChild(sp);
156
- let b = document.createElement('button');
157
- b.innerText = '...';
425
+ let b = document.createElement("button");
426
+ b.innerText = "...";
158
427
  b.onclick = () => {
159
428
  editComplex({ value: currentValue, propertyPath });
160
429
  };
161
430
  d.appendChild(b);
162
- wbRender.nodeElem.style.display = 'flex';
431
+ wbRender.nodeElem.style.display = "flex";
163
432
  return d;
164
433
  }
165
434
  else {
166
- let b = document.createElement('button');
167
- b.style.height = 'calc(100% - 6px)';
168
- b.style.position = 'relative';
169
- b.style.display = 'flex';
170
- b.style.justifyContent = 'center';
171
- b.style.width = '20px';
172
- b.style.boxSizing = 'content-box';
173
- b.title = 'complex property value';
174
- b.style.opacity = '0.2';
175
- b.innerText = '...';
435
+ let b = document.createElement("button");
436
+ b.style.height = "calc(100% - 6px)";
437
+ b.style.position = "relative";
438
+ b.style.display = "flex";
439
+ b.style.justifyContent = "center";
440
+ b.style.width = "20px";
441
+ b.style.boxSizing = "content-box";
442
+ b.title = "complex property value";
443
+ b.style.opacity = "0.2";
444
+ b.innerText = "...";
176
445
  b.onclick = () => {
177
446
  editComplex({ value: currentValue, propertyPath });
178
447
  };
179
- wbRender.nodeElem.insertAdjacentElement('afterbegin', b);
180
- wbRender.nodeElem.style.display = 'flex';
448
+ wbRender.nodeElem.insertAdjacentElement("afterbegin", b);
449
+ wbRender.nodeElem.style.display = "flex";
181
450
  }
182
451
  }
183
452
  return null;
184
453
  };
185
454
  this._propertygrid.propertyNodeContextMenu.on((data) => {
186
- ContextMenu.show([{
187
- title: 'edit complex value',
455
+ ContextMenu.show([
456
+ {
457
+ title: "edit complex value",
188
458
  action: async () => {
189
459
  editComplex(data);
190
- }
460
+ },
191
461
  },
192
462
  {
193
- title: 'edit string',
463
+ title: "edit string",
194
464
  action: async () => {
195
465
  const v = prompt("enter value:");
196
466
  if (v) {
197
467
  this._propertygrid.setPropertyValue(data.propertyPath, v);
198
468
  this._propertygrid.refresh();
199
469
  }
200
- }
470
+ },
201
471
  },
202
472
  {
203
- title: 'remove complex value',
473
+ title: "remove complex value",
204
474
  action: async () => {
205
475
  this._propertygrid.setPropertyValue(data.propertyPath, undefined);
206
476
  this._propertygrid.refresh();
207
- }
208
- }], data.event);
477
+ },
478
+ },
479
+ ], data.event);
209
480
  });
210
481
  }
211
- //Converter from TypscriptJsonSchema to our Property list...
212
- async addPossibleCommands() {
213
- let commands = Object.keys(this.scriptCommandsTypeInfo.definitions).filter(x => this.scriptCommandsTypeInfo.definitions[x].type == 'object');
214
- for (let c of commands) {
215
- if (c == 'ScriptCommands')
216
- continue;
217
- let option = document.createElement('option');
218
- option.innerText = c;
219
- this._possibleCommands.add(option);
482
+ helpClicked() {
483
+ const iframe = document.createElement("iframe");
484
+ iframe.srcdoc = simpleScriptEditorHelpHtml;
485
+ iframe.style.cssText =
486
+ "width:100%; height:100%; border:none; display:block;";
487
+ this.visualizationShell.openModal(iframe, {
488
+ title: "Simple Script Editor Help",
489
+ parent: this,
490
+ x: -650,
491
+ y: -100,
492
+ width: 620,
493
+ height: 620,
494
+ });
495
+ this.helpRequested.emit();
496
+ }
497
+ toggleCodeView() {
498
+ if (!this._showingCode) {
499
+ const json = JSON.stringify({ commands: this.getScriptCommands() }, null, 2);
500
+ this._codeView.update(json);
501
+ this._showCodeView(true);
502
+ return;
220
503
  }
504
+ const parsed = this._parseCodeViewScript();
505
+ if (!parsed)
506
+ return;
507
+ this._showCodeView(false);
508
+ this.loadScript(parsed);
509
+ }
510
+ _showCodeView(show) {
511
+ this._codeView.classList.toggle("visible", show);
512
+ this._commandListDiv.classList.toggle("hidden", show);
513
+ this._splitView.classList.toggle("code-mode", show);
514
+ this._addCommandBtn.disabled = show;
515
+ this._codeToggleBtn.innerText = show ? "Visual" : "Code";
516
+ this._showingCode = show;
517
+ if (show)
518
+ requestAnimationFrame(() => this._codeView.activated());
519
+ }
520
+ /** Parses+validates the code view's JSON. Returns null (and alerts) on invalid JSON. */
521
+ _parseCodeViewScript() {
522
+ let parsed;
523
+ try {
524
+ parsed = JSON.parse(this._codeView.getText());
525
+ }
526
+ catch (e) {
527
+ alert("Invalid JSON:\n" + e.message);
528
+ return null;
529
+ }
530
+ if (!Array.isArray(parsed?.commands)) {
531
+ alert('The JSON must have a "commands" array.');
532
+ return null;
533
+ }
534
+ return parsed;
221
535
  }
222
536
  loadScript(script) {
223
537
  this._script = script;
538
+ if (this._commandListFancyTree) {
539
+ this._commandListFancyTree.destroy();
540
+ this._commandListDiv = this._getDomElement("commandList");
541
+ }
224
542
  let commandListTreeItems = [];
225
543
  for (let c of this._script.commands) {
226
544
  c = ScriptUpgrades.upgradeScriptCommand(c);
227
545
  commandListTreeItems.push(this.createTreeItem(c));
228
546
  }
229
- ;
230
547
  this._commandListFancyTree = new Wunderbaum({
231
548
  ...defaultOptions,
232
549
  element: this._commandListDiv,
@@ -237,15 +554,51 @@ export class SimpleScriptEditor extends BaseCustomWebComponentConstructorAppend
237
554
  },
238
555
  render: (e) => {
239
556
  if (e.isNew) {
240
- let span = e.nodeElem;
241
- span.oncontextmenu = (ev) => {
242
- e.node.setActive();
243
- if (e.node.data.contextMenu) {
244
- e.node.data.contextMenu(ev, e.node.data, e.node);
245
- }
557
+ e.nodeElem.oncontextmenu = (ev) => {
246
558
  ev.preventDefault();
247
559
  return false;
248
560
  };
561
+ const handle = document.createElement("div");
562
+ handle.className = "drag-handle";
563
+ handle.title = "Drag to reorder";
564
+ handle.innerHTML =
565
+ '<svg viewBox="0 0 24 24" width="14" height="14"><path d="M4 6h16M4 12h16M4 18h16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>';
566
+ const leadingSpacer = document.createElement("span");
567
+ leadingSpacer.style.cssText = "display:inline-block; width:24px;";
568
+ e.nodeElem.prepend(leadingSpacer, handle);
569
+ const rowElem = e.nodeElem.parentElement;
570
+ const spacer = document.createElement("span");
571
+ spacer.style.cssText = "display:inline-block; width:32px;";
572
+ e.nodeElem.appendChild(spacer);
573
+ const cmd = document.createElement("div");
574
+ cmd.className = "cmd";
575
+ const copyImg = document.createElement("img");
576
+ copyImg.src = assetsPath + "icons/copy.svg";
577
+ copyImg.title = "Duplicate";
578
+ copyImg.onclick = () => {
579
+ const clone = structuredClone(e.node.data.data.item);
580
+ const newNode = e.node.parent.addChildren(this.createTreeItem(clone));
581
+ newNode.moveTo(e.node, "after");
582
+ };
583
+ const delImg = document.createElement("img");
584
+ delImg.src = assetsPath + "icons/delete.svg";
585
+ delImg.title = "Remove";
586
+ delImg.onclick = () => e.node.remove();
587
+ cmd.append(copyImg, delImg);
588
+ rowElem.appendChild(cmd);
589
+ //@ts-ignore
590
+ e.nodeElem._reservedRowWidth =
591
+ leadingSpacer.offsetWidth + spacer.offsetWidth;
592
+ }
593
+ //@ts-ignore
594
+ const reserved = e.nodeElem._reservedRowWidth ?? 0;
595
+ if (reserved) {
596
+ const titleSpan = e.nodeElem.querySelector("span.wb-title");
597
+ if (titleSpan) {
598
+ const w = parseFloat(titleSpan.style.width);
599
+ if (!isNaN(w))
600
+ titleSpan.style.width = Math.max(0, w - reserved) + "px";
601
+ }
249
602
  }
250
603
  },
251
604
  dnd: {
@@ -254,42 +607,85 @@ export class SimpleScriptEditor extends BaseCustomWebComponentConstructorAppend
254
607
  preventVoidMoves: false,
255
608
  serializeClipboardData: false,
256
609
  dragStart: (e) => {
610
+ if (!this._dragFromHandle)
611
+ return false;
612
+ this._draggedNode = e.node;
257
613
  e.event.dataTransfer.effectAllowed = "move";
258
614
  e.event.dataTransfer.dropEffect = "move";
615
+ e.event.dataTransfer.setData(SAME_TREE_MOVE_MIME, "1");
259
616
  return true;
260
617
  },
618
+ dragEnd: () => {
619
+ this._draggedNode = null;
620
+ },
261
621
  dragEnter: (e) => {
262
- e.event.dataTransfer.dropEffect = 'move';
622
+ const isMove = e.event.dataTransfer?.types.includes(SAME_TREE_MOVE_MIME);
623
+ e.event.dataTransfer.dropEffect = isMove ? "move" : "copy";
263
624
  return true;
264
625
  },
265
626
  dragOver: (e) => {
266
- e.event.dataTransfer.dropEffect = 'move';
627
+ const isMove = e.event.dataTransfer?.types.includes(SAME_TREE_MOVE_MIME);
628
+ e.event.dataTransfer.dropEffect = isMove ? "move" : "copy";
267
629
  },
268
630
  drop: async (e) => {
269
- e.sourceNode.moveTo(e.node, e.region == 'before' ? 'before' : 'after');
270
- }
271
- }
631
+ const region = e.region == "before" ? "before" : "after";
632
+ const isMove = e.event.dataTransfer?.types.includes(SAME_TREE_MOVE_MIME);
633
+ if (isMove && this._draggedNode) {
634
+ this._draggedNode.moveTo(e.node, region);
635
+ this._draggedNode.setClass("wb-drag-source", false);
636
+ }
637
+ else {
638
+ const type = e.event.dataTransfer?.getData("text/plain");
639
+ if (type) {
640
+ const command = { type };
641
+ const ti = this.createTreeItem(command);
642
+ const newNode = e.node.parent.addChildren(ti);
643
+ newNode.moveTo(e.node, region);
644
+ }
645
+ }
646
+ },
647
+ },
272
648
  });
649
+ this._commandListFancyTree.root.children?.[0]?.setActive();
273
650
  }
274
651
  createTreeItem(currentItem) {
275
652
  let cti = {
276
653
  title: currentItem.type,
277
654
  data: { item: currentItem },
278
- contextMenu: (e, data, node) => {
279
- ContextMenu.show([{ title: 'Remove Item', action: (e) => node.remove() }], e);
280
- }
281
655
  };
282
656
  return cti;
283
657
  }
284
- addItem() {
285
- const cmdName = this._possibleCommands.value;
286
- const command = { type: cmdName };
287
- const ti = this.createTreeItem(command);
288
- this._commandListFancyTree.addChildren(ti);
658
+ async addCommand() {
659
+ const picker = new SimpleScriptCommandPicker();
660
+ picker.loadCommands(this.scriptCommandsTypeInfo, this.commandDescriptions);
661
+ picker.title = "Add Script Command";
662
+ picker.commandTypeChosen.on(() => {
663
+ if (picker.selectedType) {
664
+ const command = { type: picker.selectedType };
665
+ const ti = this.createTreeItem(command);
666
+ this._commandListFancyTree.addChildren(ti);
667
+ }
668
+ });
669
+ const abortController = new AbortController();
670
+ picker.closeRequested.on(() => abortController.abort());
671
+ await this.visualizationShell.openModal(picker, {
672
+ title: "Add Script Command",
673
+ x: 300,
674
+ y: 50,
675
+ width: 620,
676
+ height: 400,
677
+ parent: this,
678
+ abortSignal: abortController.signal,
679
+ });
289
680
  }
290
681
  getScriptCommands() {
682
+ if (this._showingCode) {
683
+ const parsed = this._parseCodeViewScript();
684
+ if (parsed)
685
+ this.loadScript(parsed);
686
+ }
291
687
  let children = this._commandListFancyTree.root.children;
292
- return children.map(x => x.data.data.item);
688
+ return children.map((x) => x.data.data.item);
293
689
  }
294
690
  }
295
691
  customElements.define(SimpleScriptEditor.is, SimpleScriptEditor);