@node-projects/web-component-designer-visualization-addons 0.1.145 → 0.1.147

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,349 @@
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";
4
- import { defaultOptions } from "@node-projects/web-component-designer-widgets-wunderbaum";
5
- import { Wunderbaum } from 'wunderbaum';
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
+ import { defaultOptions, defaultStyle } from "@node-projects/web-component-designer-widgets-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 { CodeViewMonaco } from "@node-projects/web-component-designer-codeview-monaco";
10
+ import { SimpleScriptCommandPicker } from "./SimpleScriptCommandPicker.js";
11
+ import { simpleScriptEditorHelpHtml } from "./SimpleScriptEditorHelp.js";
12
+ import { nativeScriptCommandDescriptions, } from "../scripting/ScriptCommandsDescriptions.js";
13
+ import "@node-projects/splitview.webcomponent";
10
14
  import { ScriptUpgrades } from "../scripting/ScriptUpgrader.js";
15
+ const SAME_TREE_MOVE_MIME = "application/x-simple-script-move";
11
16
  export class SimpleScriptEditor extends BaseCustomWebComponentConstructorAppend {
12
17
  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
- }
18
+ :host {
19
+ background: white;
20
+ }
23
21
 
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
- }
22
+ .root {
23
+ width: 100%;
24
+ height: 100%;
25
+ overflow: hidden;
26
+ display: flex;
27
+ flex-direction: column;
28
+ box-sizing: border-box;
29
+ }
32
30
 
33
- node-projects-visualization-property-grid {
34
- width: 100%;
35
- height: 100%;
36
- }
37
- `;
31
+ .content {
32
+ flex: 1 1 auto;
33
+ min-height: 0;
34
+ position: relative;
35
+ }
36
+
37
+ .split-view {
38
+ height: calc(100% - 4px);
39
+ width: calc(100% - 4px);
40
+ position: relative;
41
+ margin: 2px;
42
+ }
43
+
44
+ .list-pane {
45
+ width: 48%;
46
+ height: 100%;
47
+ position: relative;
48
+ box-sizing: border-box;
49
+ display: flex;
50
+ flex-direction: column;
51
+ }
52
+
53
+ .list-content {
54
+ flex: 1 1 auto;
55
+ min-height: 0;
56
+ position: relative;
57
+ }
58
+
59
+ .grid-pane {
60
+ width: 52%;
61
+ height: 100%;
62
+ position: relative;
63
+ box-sizing: border-box;
64
+ }
65
+
66
+ .split-view.code-mode .grid-pane {
67
+ display: none;
68
+ }
69
+
70
+ .split-view.code-mode::part(splitter) {
71
+ display: none;
72
+ }
73
+
74
+ #commandList {
75
+ position: absolute;
76
+ inset: 0;
77
+ overflow-x: hidden;
78
+ overflow-y: auto;
79
+ box-sizing: border-box;
80
+ }
81
+
82
+ #commandList.hidden {
83
+ display: none;
84
+ }
85
+
86
+ #commandList {
87
+ --wb-icon-outer-width: 22px;
88
+ }
89
+
90
+ #commandList i.wb-expander {
91
+ background-size: 9px 9px;
92
+ background-position-x: 6px;
93
+ background-position-y: 6px;
94
+ opacity: 0.55;
95
+ }
96
+
97
+ .code-view {
98
+ position: absolute;
99
+ inset: 0;
100
+ display: none;
101
+ }
102
+
103
+ .code-view.visible {
104
+ display: block;
105
+ }
106
+
107
+ node-projects-visualization-property-grid {
108
+ width: 100%;
109
+ height: 100%;
110
+ }
111
+
112
+ .toolbar {
113
+ display: flex;
114
+ align-items: center;
115
+ gap: 8px;
116
+ box-sizing: border-box;
117
+ flex: 0 0 auto;
118
+ width: 100%;
119
+ height: 36px;
120
+ padding: 4px 8px;
121
+ border-top: 1px solid #ccc;
122
+ background-color: rgb(230, 230, 230);
123
+ }
124
+
125
+ .toolbar button {
126
+ box-sizing: border-box;
127
+ padding: 6px 16px;
128
+ border-radius: 4px;
129
+ border: 1px solid #b0b0b0;
130
+ background: white;
131
+ font-size: 12px;
132
+ cursor: pointer;
133
+ }
134
+
135
+ .toolbar button:hover:not(:disabled) {
136
+ background: #f0f0f0;
137
+ border-color: #909090;
138
+ }
139
+
140
+ .toolbar button:active:not(:disabled) {
141
+ background: #e2e2e2;
142
+ }
143
+
144
+ .toolbar button:disabled {
145
+ opacity: 0.5;
146
+ cursor: not-allowed;
147
+ }
148
+
149
+ .toolbar button.primary {
150
+ background: #2c6e9b;
151
+ border-color: #2c6e9b;
152
+ color: white;
153
+ }
154
+
155
+ .toolbar button.primary:hover:not(:disabled) {
156
+ background: #255d83;
157
+ border-color: #255d83;
158
+ }
159
+
160
+ .toolbar button.primary:active:not(:disabled) {
161
+ background: #1e4d6b;
162
+ }
163
+
164
+ .toolbar .spacer {
165
+ flex: 1 1 auto;
166
+ }
167
+
168
+ .toolbar .icon-btn {
169
+ display: flex;
170
+ align-items: center;
171
+ justify-content: center;
172
+ width: 22px;
173
+ height: 22px;
174
+ padding: 0;
175
+ box-sizing: border-box;
176
+ border: none;
177
+ background: transparent;
178
+ cursor: pointer;
179
+ }
180
+
181
+ .toolbar .icon-btn:hover {
182
+ background: #eee;
183
+ }
184
+
185
+ .drag-handle {
186
+ position: absolute;
187
+ left: 0;
188
+ top: 0;
189
+ height: 100%;
190
+ width: 24px;
191
+ display: flex;
192
+ align-items: center;
193
+ justify-content: center;
194
+ color: #888;
195
+ cursor: grab;
196
+ }
197
+
198
+ .cmd {
199
+ position: absolute;
200
+ right: 0;
201
+ top: 0;
202
+ height: 100%;
203
+ display: flex;
204
+ align-items: center;
205
+ gap: 2px;
206
+ padding: 0 2px;
207
+ background: white;
208
+ }
209
+
210
+ .cmd img {
211
+ width: 12px;
212
+ height: 12px;
213
+ cursor: pointer;
214
+ }
215
+ `;
38
216
  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';
217
+ <div class="root">
218
+ <div class="content">
219
+ <node-projects-split-view
220
+ id="splitView"
221
+ class="split-view"
222
+ orientation="horizontal"
223
+ >
224
+ <div id="listPane" class="list-pane">
225
+ <div id="listContent" class="list-content">
226
+ <div id="commandList"></div>
227
+ <node-projects-code-view-monaco
228
+ id="codeView"
229
+ class="code-view"
230
+ language="json"
231
+ ></node-projects-code-view-monaco>
232
+ </div>
233
+ <div class="toolbar">
234
+ <button
235
+ id="addCommandBtn"
236
+ class="primary"
237
+ @click="[[this.addCommand()]]"
238
+ >
239
+ Add Command
240
+ </button>
241
+ <button id="codeToggleBtn" @click="[[this.toggleCodeView()]]">
242
+ Code
243
+ </button>
244
+ <span class="spacer"></span>
245
+ <button
246
+ class="icon-btn"
247
+ title="Help"
248
+ @click="[[this.helpClicked()]]"
249
+ >
250
+ <svg viewBox="0 0 512 512" width="16" height="16">
251
+ <path
252
+ 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"
253
+ />
254
+ </svg>
255
+ </button>
256
+ </div>
257
+ </div>
258
+ <div class="grid-pane">
259
+ <node-projects-visualization-property-grid
260
+ id="propertygrid"
261
+ ></node-projects-visualization-property-grid>
262
+ </div>
263
+ </node-projects-split-view>
264
+ </div>
265
+ </div>
266
+ `;
267
+ static is = "node-projects-visualization-simple-script-editor";
268
+ _title;
269
+ get title() {
270
+ return this._title;
271
+ }
272
+ set title(value) {
273
+ this._title = value;
274
+ }
57
275
  serviceContainer;
58
276
  instanceServiceContainer;
59
277
  visualizationHandler;
60
278
  visualizationShell;
61
279
  scriptCommandsTypeInfo;
62
280
  propertiesTypeInfo;
281
+ helpRequested = new TypedEvent();
282
+ commandDescriptions = {
283
+ ...nativeScriptCommandDescriptions,
284
+ };
63
285
  _script;
64
286
  _commandListDiv;
65
287
  _commandListFancyTree;
66
- _possibleCommands;
67
288
  _propertygrid;
289
+ _splitView;
290
+ _codeView;
291
+ _codeToggleBtn;
292
+ _addCommandBtn;
293
+ _showingCode = false;
294
+ _dragFromHandle = false;
295
+ _draggedNode = null;
68
296
  constructor() {
69
297
  super();
70
298
  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];
299
+ this._commandListDiv = this._getDomElement("commandList");
300
+ this._propertygrid =
301
+ this._getDomElement("propertygrid");
302
+ this._splitView = this._getDomElement("splitView");
303
+ this._codeView = this._getDomElement("codeView");
304
+ this._codeToggleBtn =
305
+ this._getDomElement("codeToggleBtn");
306
+ this._addCommandBtn =
307
+ this._getDomElement("addCommandBtn");
308
+ const listContent = this._getDomElement("listContent");
309
+ listContent.addEventListener("mousedown", (ev) => {
310
+ this._dragFromHandle = !!ev.target?.closest(".drag-handle");
311
+ });
312
+ document.addEventListener("dragend", () => {
313
+ this._draggedNode = null;
314
+ }, true);
315
+ listContent.addEventListener("dragover", (ev) => {
316
+ ev.preventDefault();
317
+ if (ev.dataTransfer) {
318
+ const isMove = ev.dataTransfer.types.includes(SAME_TREE_MOVE_MIME);
319
+ ev.dataTransfer.dropEffect = isMove ? "move" : "copy";
320
+ }
321
+ });
322
+ listContent.addEventListener("drop", (ev) => {
323
+ ev.preventDefault();
324
+ const isMove = ev.dataTransfer?.types.includes(SAME_TREE_MOVE_MIME);
325
+ if (isMove && this._draggedNode) {
326
+ const children = this._commandListFancyTree.root.children;
327
+ const last = children?.[children.length - 1];
328
+ if (last)
329
+ this._draggedNode.moveTo(last, "after");
330
+ this._draggedNode.setClass("wb-drag-source", false);
331
+ }
332
+ else {
333
+ const type = ev.dataTransfer?.getData("text/plain");
334
+ if (type) {
335
+ const command = { type };
336
+ this._commandListFancyTree.root.addChildren(this.createTreeItem(command));
337
+ }
338
+ }
339
+ });
340
+ this.shadowRoot.adoptedStyleSheets = [
341
+ wunderbaumStyle,
342
+ defaultStyle,
343
+ SimpleScriptEditor.style,
344
+ ];
75
345
  }
76
346
  async ready() {
77
- this.addPossibleCommands();
78
347
  this._parseAttributesToProperties();
79
348
  this._bindingsParse(null, true);
80
349
  this._assignEvents();
@@ -84,168 +353,528 @@ export class SimpleScriptEditor extends BaseCustomWebComponentConstructorAppend
84
353
  pg.visualizationShell = this.visualizationShell;
85
354
  pg.serviceContainer = this.serviceContainer;
86
355
  pg.instanceServiceContainer = this.instanceServiceContainer;
87
- pg.bindableObjectsTarget = 'script';
356
+ pg.bindableObjectsTarget = "script";
88
357
  pg.getTypeInfo = (obj, type) => typeInfoFromJsonSchema(this.propertiesTypeInfo, obj, type);
89
358
  pg.showHead = false;
90
- pg.typeName = 'IScriptMultiplexValue';
359
+ pg.typeName = "IScriptMultiplexValue";
91
360
  pg.title = 'Complex for "' + data.propertyPath + '"';
92
- if (typeof data.value === 'object')
361
+ if (typeof data.value === "object")
93
362
  pg.selectedObject = data.value ?? {};
94
363
  else
95
364
  pg.selectedObject = {};
96
365
  pg.bindingDoubleClicked = (b) => {
97
- if (b.bindabletype == 'property') {
98
- pg.setPropertyValue('source', 'property');
366
+ if (b.bindabletype == "property") {
367
+ pg.setPropertyValue("source", "property");
99
368
  }
100
- else if (b.bindabletype == 'context') {
101
- pg.setPropertyValue('source', 'context');
369
+ else if (b.bindabletype == "context") {
370
+ pg.setPropertyValue("source", "context");
102
371
  }
103
372
  else {
104
- pg.setPropertyValue('source', 'signal');
373
+ pg.setPropertyValue("source", "signal");
105
374
  }
106
- pg.setPropertyValue('name', b.fullName);
375
+ pg.setPropertyValue("name", b.fullName);
107
376
  pg.refresh();
108
377
  };
109
- let res = await this.visualizationShell.openConfirmation(pg, { x: 100, y: 100, width: 400, height: 500, parent: this });
378
+ let res = await this.visualizationShell.openConfirmation(pg, {
379
+ x: 100,
380
+ y: 100,
381
+ width: 400,
382
+ height: 500,
383
+ parent: this,
384
+ });
110
385
  if (res) {
111
386
  this._propertygrid.setPropertyValue(data.propertyPath, pg.selectedObject);
112
387
  this._propertygrid.refresh();
113
388
  }
114
389
  };
390
+ let editFormula = async (data) => {
391
+ const editor = new CodeViewMonaco();
392
+ editor.language = "javascript";
393
+ editor.style.position = "relative";
394
+ editor.code = data.value ?? "";
395
+ const res = await this.visualizationShell.openConfirmation(editor, {
396
+ title: "Edit formula",
397
+ x: 100,
398
+ y: 50,
399
+ width: 700,
400
+ height: 450,
401
+ parent: this,
402
+ });
403
+ if (res) {
404
+ this._propertygrid.setPropertyValue(data.propertyPath, editor.code);
405
+ this._propertygrid.refresh();
406
+ }
407
+ };
408
+ const signalSources = [
409
+ "signal",
410
+ "property",
411
+ "elementProperty",
412
+ "signalInProperty",
413
+ "event",
414
+ "parameter",
415
+ "context",
416
+ "complexString",
417
+ "complexSignal",
418
+ "expression",
419
+ ];
420
+ const identifierRegex = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
421
+ const jsReservedWords = new Set([
422
+ "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do",
423
+ "else", "export", "extends", "finally", "for", "function", "if", "import", "in", "instanceof",
424
+ "new", "return", "super", "switch", "this", "throw", "try", "typeof", "var", "void", "while",
425
+ "with", "yield", "let", "static", "enum", "await", "implements", "package", "protected",
426
+ "interface", "private", "public", "null", "true", "false", "undefined", "arguments", "eval",
427
+ ]);
428
+ const signalSourceDescriptions = {
429
+ signal: "read the value from a Signal",
430
+ property: "read the value from a property of the customControl (not usable in screens)",
431
+ elementProperty: "a property defined on the element raising the event",
432
+ signalInProperty: "read the value from a Signal (wich name is in the Property)",
433
+ event: "read the value of a property of the event object",
434
+ parameter: "a parameter you hand over",
435
+ context: "a value of the context",
436
+ complexString: "a string with signals (contained in {})",
437
+ complexSignal: "read the value from a signal wich name is build here (it can contain other signals in {})",
438
+ expression: "js expression, 'ctx' is context object",
439
+ };
440
+ let editSignalList = async (data) => {
441
+ const arr = (data.value ?? []).map((v) => ({
442
+ ...v,
443
+ }));
444
+ const getVarNameError = (i) => {
445
+ const raw = (arr[i].varName ?? "").trim();
446
+ if (!raw)
447
+ return null;
448
+ if (!identifierRegex.test(raw))
449
+ return `"${raw}" is not a valid JavaScript variable name.`;
450
+ if (jsReservedWords.has(raw))
451
+ return `"${raw}" is a reserved word and cannot be used as a variable name.`;
452
+ if (arr.some((e, j) => j !== i && (e.varName ?? "").trim() === raw))
453
+ return `"${raw}" is used by more than one signal.`;
454
+ return null;
455
+ };
456
+ const controlStyle = "height:22px; box-sizing:border-box; font-size:12px;";
457
+ const container = document.createElement("div");
458
+ container.style.cssText =
459
+ "display:flex; flex-direction:column; gap:6px; padding:8px; height:100%; box-sizing:border-box; overflow-y:auto; font-size:12px;";
460
+ const header = document.createElement("div");
461
+ header.style.cssText = "display:flex; align-items:center; gap:4px; font-weight:bold;";
462
+ const headerName = document.createElement("span");
463
+ headerName.textContent = "Name";
464
+ headerName.style.cssText = "flex:0 0 120px;";
465
+ const headerType = document.createElement("span");
466
+ headerType.textContent = "Typ";
467
+ headerType.style.cssText = "flex:0 0 130px;";
468
+ const headerSignal = document.createElement("span");
469
+ headerSignal.textContent = "Signal";
470
+ headerSignal.style.cssText = "flex:1 1 auto;";
471
+ const headerSpacer = document.createElement("span");
472
+ headerSpacer.style.cssText = "flex:0 0 22px;";
473
+ header.append(headerName, headerType, headerSignal, headerSpacer);
474
+ container.appendChild(header);
475
+ const rowsContainer = document.createElement("div");
476
+ rowsContainer.style.cssText =
477
+ "display:flex; flex-direction:column; gap:4px;";
478
+ container.appendChild(rowsContainer);
479
+ const descPanel = document.createElement("div");
480
+ descPanel.style.cssText =
481
+ "flex:0 0 auto; margin-top:4px; padding-top:6px; border-top:1px solid #ccc; font-size:12px;";
482
+ const descTitle = document.createElement("div");
483
+ descTitle.style.cssText = "font-weight:bold; margin-bottom:2px;";
484
+ const descText = document.createElement("div");
485
+ descText.style.cssText = "color:#333; white-space:pre-line;";
486
+ descPanel.append(descTitle, descText);
487
+ const showSourceDescription = (source) => {
488
+ descTitle.textContent = "Typ: " + source;
489
+ descText.textContent = signalSourceDescriptions[source] ?? "";
490
+ };
491
+ const varNameInputs = [];
492
+ const revalidate = () => {
493
+ arr.forEach((_, i) => {
494
+ const input = varNameInputs[i];
495
+ if (!input)
496
+ return;
497
+ const err = getVarNameError(i);
498
+ input.title = err ?? `Variable name used in the formula (defaults to __${i} if empty)`;
499
+ input.style.borderColor = err ? "#c0392b" : "";
500
+ input.style.backgroundColor = err ? "#fdecea" : "";
501
+ });
502
+ };
503
+ const renderRows = () => {
504
+ rowsContainer.innerHTML = "";
505
+ varNameInputs.length = 0;
506
+ arr.forEach((entry, i) => {
507
+ const row = document.createElement("div");
508
+ row.style.cssText = "display:flex; align-items:center; gap:4px;";
509
+ const varNameInput = document.createElement("input");
510
+ varNameInput.value = entry.varName ?? "";
511
+ varNameInput.placeholder = "__" + i;
512
+ varNameInput.style.cssText = controlStyle + " flex:0 0 120px; min-width: 120px; font-family:monospace;";
513
+ varNameInput.oninput = () => {
514
+ entry.varName = varNameInput.value || undefined;
515
+ revalidate();
516
+ };
517
+ varNameInputs.push(varNameInput);
518
+ row.appendChild(varNameInput);
519
+ const sourceSelect = document.createElement("select");
520
+ sourceSelect.style.cssText = controlStyle + " flex:0 0 130px;";
521
+ for (const s of signalSources) {
522
+ const opt = document.createElement("option");
523
+ opt.value = s;
524
+ opt.textContent = s;
525
+ if (entry.source === s)
526
+ opt.selected = true;
527
+ sourceSelect.appendChild(opt);
528
+ }
529
+ sourceSelect.onchange = () => {
530
+ entry.source = sourceSelect.value;
531
+ showSourceDescription(sourceSelect.value);
532
+ };
533
+ sourceSelect.onfocus = () => showSourceDescription(sourceSelect.value);
534
+ row.appendChild(sourceSelect);
535
+ const nameInput = document.createElement("input");
536
+ nameInput.value = entry.name ?? "";
537
+ nameInput.placeholder = "e.g. srm.rbg{__tagRoot}.error";
538
+ nameInput.style.cssText = controlStyle + " flex:1 1 auto;";
539
+ nameInput.oninput = () => {
540
+ entry.name = nameInput.value;
541
+ };
542
+ row.appendChild(nameInput);
543
+ const delBtn = document.createElement("button");
544
+ delBtn.title = "Remove signal";
545
+ delBtn.style.cssText = controlStyle + " flex:0 0 22px; width:22px; padding:0; display:flex; align-items:center; justify-content:center;";
546
+ const delIcon = document.createElement("img");
547
+ delIcon.src = assetsPath + "icons/delete.svg";
548
+ delIcon.style.cssText = "width:12px; height:12px;";
549
+ delBtn.appendChild(delIcon);
550
+ delBtn.onclick = () => {
551
+ arr.splice(i, 1);
552
+ renderRows();
553
+ };
554
+ row.appendChild(delBtn);
555
+ rowsContainer.appendChild(row);
556
+ });
557
+ revalidate();
558
+ };
559
+ renderRows();
560
+ const addBtn = document.createElement("button");
561
+ addBtn.textContent = "+ Add signal";
562
+ addBtn.style.cssText = controlStyle + " align-self:flex-start; padding:0 8px;";
563
+ addBtn.onclick = () => {
564
+ arr.push({ source: "signal", name: "" });
565
+ renderRows();
566
+ };
567
+ container.appendChild(addBtn);
568
+ showSourceDescription(arr[0]?.source ?? "signal");
569
+ container.appendChild(descPanel);
570
+ const res = await this.visualizationShell.openConfirmation(container, {
571
+ title: "Edit signals",
572
+ x: 100,
573
+ y: 50,
574
+ width: 500,
575
+ height: 400,
576
+ parent: this,
577
+ });
578
+ if (res) {
579
+ this._propertygrid.setPropertyValue(data.propertyPath, arr);
580
+ this._propertygrid.refresh();
581
+ }
582
+ };
115
583
  this._propertygrid.visualizationHandler = this.visualizationHandler;
116
584
  this._propertygrid.visualizationShell = this.visualizationShell;
117
585
  this._propertygrid.serviceContainer = this.serviceContainer;
118
586
  this._propertygrid.instanceServiceContainer = this.instanceServiceContainer;
119
- this._propertygrid.bindableObjectsTarget = 'script';
120
- this._propertygrid.getTypeInfo = (obj, type) => typeInfoFromJsonSchema(this.scriptCommandsTypeInfo, obj, type);
587
+ this._propertygrid.bindableObjectsTarget = "script";
588
+ this._propertygrid.setNameColumnWidth(130);
589
+ this._propertygrid.getTypeInfo = (obj, type) => {
590
+ const info = typeInfoFromJsonSchema(this.scriptCommandsTypeInfo, obj, type);
591
+ // trueCommands/elseCommands of "If" are edited as nested tree nodes, not in the property grid
592
+ if (info && (type === "If" || obj?.type === "If")) {
593
+ info.properties = info.properties.filter((p) => p.name !== "trueCommands" && p.name !== "elseCommands");
594
+ }
595
+ return info;
596
+ };
121
597
  this._propertygrid.getSpecialEditorForType = async (property, currentValue, propertyPath, wbRender, additionalInfo) => {
122
598
  //@ts-ignore
123
599
  if (!property.specialAllreadyAdded) {
124
600
  //@ts-ignore
125
601
  property.specialAllreadyAdded = true;
126
- if (property.format === 'collection') {
602
+ if (property.format === "collection") {
127
603
  //TODO: create a collection edt. in property grid control used
128
604
  }
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';
605
+ else if (property.format === "signalList") {
606
+ let rB = document.createElement("button");
607
+ rB.style.height = "calc(100% - 6px)";
608
+ rB.style.position = "relative";
609
+ rB.style.display = "flex";
610
+ rB.style.justifyContent = "center";
611
+ rB.style.width = "20px";
612
+ rB.style.boxSizing = "content-box";
613
+ rB.innerText = "del";
138
614
  rB.onclick = () => {
139
615
  this._propertygrid.setPropertyValue(propertyPath, undefined);
140
616
  this._propertygrid.refresh();
141
617
  };
142
- wbRender.nodeElem.insertAdjacentElement('afterbegin', rB);
143
- let d = document.createElement('div');
144
- d.style.display = 'flex';
145
- let sp = document.createElement('span');
618
+ wbRender.nodeElem.insertAdjacentElement("afterbegin", rB);
619
+ const arr = Array.isArray(currentValue) ? currentValue : [];
620
+ let d = document.createElement("div");
621
+ d.style.display = "flex";
622
+ let sp = document.createElement("span");
623
+ sp.innerText = arr
624
+ .map((v, i) => (v?.varName || "__" + i) + ": " + (v?.source ?? "") + ":" + (v?.name ?? ""))
625
+ .join(", ");
626
+ sp.style.overflow = "hidden";
627
+ sp.style.whiteSpace = "nowrap";
628
+ sp.style.textOverflow = "ellipsis";
629
+ sp.style.flexGrow = "1";
630
+ sp.title = JSON.stringify(arr);
631
+ d.appendChild(sp);
632
+ let b = document.createElement("button");
633
+ b.innerText = "...";
634
+ b.onclick = () => {
635
+ editSignalList({ value: arr, propertyPath });
636
+ };
637
+ d.appendChild(b);
638
+ wbRender.nodeElem.style.display = "flex";
639
+ return d;
640
+ }
641
+ else if (property.format === "formula") {
642
+ const wrapper = document.createElement("div");
643
+ wrapper.style.cssText =
644
+ "display:flex; width:100%; height:100%; align-items:stretch;";
645
+ const editor = new CodeViewMonaco();
646
+ editor.language = "javascript";
647
+ editor.singleRow = true;
648
+ editor.style.cssText =
649
+ "flex:1 1 auto; min-width:0; height:100%; position:relative; overflow:hidden;";
650
+ editor.code = currentValue ?? "";
651
+ editor.addEventListener("code-changed", () => {
652
+ this._propertygrid.setPropertyValue(propertyPath, editor.code);
653
+ });
654
+ wrapper.appendChild(editor);
655
+ const expandBtn = document.createElement("button");
656
+ expandBtn.innerText = "...";
657
+ expandBtn.title = "Open in larger editor";
658
+ expandBtn.style.cssText = "flex:0 0 28px; width:20px;";
659
+ expandBtn.onclick = () => editFormula({ value: editor.code, propertyPath });
660
+ wrapper.appendChild(expandBtn);
661
+ wbRender.nodeElem.style.display = "flex";
662
+ return wrapper;
663
+ }
664
+ else if ((typeof currentValue === "object" && currentValue !== null) ||
665
+ property.format === "complex") {
666
+ let rB = document.createElement("button");
667
+ rB.style.height = "calc(100% - 6px)";
668
+ rB.style.position = "relative";
669
+ rB.style.display = "flex";
670
+ rB.style.justifyContent = "center";
671
+ rB.style.width = "20px";
672
+ rB.style.boxSizing = "content-box";
673
+ rB.innerText = "del";
674
+ rB.onclick = () => {
675
+ this._propertygrid.setPropertyValue(propertyPath, undefined);
676
+ this._propertygrid.refresh();
677
+ };
678
+ wbRender.nodeElem.insertAdjacentElement("afterbegin", rB);
679
+ let d = document.createElement("div");
680
+ d.style.display = "flex";
681
+ let sp = document.createElement("span");
146
682
  if (currentValue)
147
- sp.innerText = (currentValue.source ?? '') + ': ' + (currentValue.name ?? '');
683
+ sp.innerText =
684
+ (currentValue.source ?? "") + ": " + (currentValue.name ?? "");
148
685
  else
149
- sp.innerText = '';
150
- sp.style.overflow = 'hidden';
151
- sp.style.whiteSpace = 'nowrap';
152
- sp.style.textOverflow = 'ellipsis';
153
- sp.style.flexGrow = '1';
686
+ sp.innerText = "";
687
+ sp.style.overflow = "hidden";
688
+ sp.style.whiteSpace = "nowrap";
689
+ sp.style.textOverflow = "ellipsis";
690
+ sp.style.flexGrow = "1";
154
691
  sp.title = JSON.stringify(currentValue);
155
692
  d.appendChild(sp);
156
- let b = document.createElement('button');
157
- b.innerText = '...';
693
+ let b = document.createElement("button");
694
+ b.innerText = "...";
158
695
  b.onclick = () => {
159
696
  editComplex({ value: currentValue, propertyPath });
160
697
  };
161
698
  d.appendChild(b);
162
- wbRender.nodeElem.style.display = 'flex';
699
+ wbRender.nodeElem.style.display = "flex";
163
700
  return d;
164
701
  }
165
702
  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 = '...';
703
+ let b = document.createElement("button");
704
+ b.style.height = "calc(100% - 6px)";
705
+ b.style.position = "relative";
706
+ b.style.display = "flex";
707
+ b.style.justifyContent = "center";
708
+ b.style.width = "20px";
709
+ b.style.boxSizing = "content-box";
710
+ b.title = "complex property value";
711
+ b.style.opacity = "0.2";
712
+ b.innerText = "...";
176
713
  b.onclick = () => {
177
714
  editComplex({ value: currentValue, propertyPath });
178
715
  };
179
- wbRender.nodeElem.insertAdjacentElement('afterbegin', b);
180
- wbRender.nodeElem.style.display = 'flex';
716
+ wbRender.nodeElem.insertAdjacentElement("afterbegin", b);
717
+ wbRender.nodeElem.style.display = "flex";
181
718
  }
182
719
  }
183
720
  return null;
184
721
  };
185
722
  this._propertygrid.propertyNodeContextMenu.on((data) => {
186
- ContextMenu.show([{
187
- title: 'edit complex value',
723
+ ContextMenu.show([
724
+ {
725
+ title: "edit complex value",
188
726
  action: async () => {
189
727
  editComplex(data);
190
- }
728
+ },
191
729
  },
192
730
  {
193
- title: 'edit string',
731
+ title: "edit string",
194
732
  action: async () => {
195
733
  const v = prompt("enter value:");
196
734
  if (v) {
197
735
  this._propertygrid.setPropertyValue(data.propertyPath, v);
198
736
  this._propertygrid.refresh();
199
737
  }
200
- }
738
+ },
201
739
  },
202
740
  {
203
- title: 'remove complex value',
741
+ title: "remove complex value",
204
742
  action: async () => {
205
743
  this._propertygrid.setPropertyValue(data.propertyPath, undefined);
206
744
  this._propertygrid.refresh();
207
- }
208
- }], data.event);
745
+ },
746
+ },
747
+ ], data.event);
748
+ });
749
+ }
750
+ helpClicked() {
751
+ const iframe = document.createElement("iframe");
752
+ iframe.srcdoc = simpleScriptEditorHelpHtml;
753
+ iframe.style.cssText =
754
+ "width:100%; height:100%; border:none; display:block;";
755
+ this.visualizationShell.openModal(iframe, {
756
+ title: "Simple Script Editor Help",
757
+ parent: this,
758
+ x: -650,
759
+ y: -100,
760
+ width: 620,
761
+ height: 620,
209
762
  });
763
+ this.helpRequested.emit();
764
+ }
765
+ toggleCodeView() {
766
+ if (!this._showingCode) {
767
+ const json = JSON.stringify({ commands: this.getScriptCommands() }, null, 2);
768
+ this._codeView.update(json);
769
+ this._showCodeView(true);
770
+ return;
771
+ }
772
+ const parsed = this._parseCodeViewScript();
773
+ if (!parsed)
774
+ return;
775
+ this._showCodeView(false);
776
+ this.loadScript(parsed);
777
+ }
778
+ _showCodeView(show) {
779
+ this._codeView.classList.toggle("visible", show);
780
+ this._commandListDiv.classList.toggle("hidden", show);
781
+ this._splitView.classList.toggle("code-mode", show);
782
+ this._addCommandBtn.disabled = show;
783
+ this._codeToggleBtn.innerText = show ? "Visual" : "Code";
784
+ this._showingCode = show;
785
+ if (show)
786
+ requestAnimationFrame(() => this._codeView.activated());
210
787
  }
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);
788
+ /** Parses+validates the code view's JSON. Returns null (and alerts) on invalid JSON. */
789
+ _parseCodeViewScript() {
790
+ let parsed;
791
+ try {
792
+ parsed = JSON.parse(this._codeView.getText());
220
793
  }
794
+ catch (e) {
795
+ alert("Invalid JSON:\n" + e.message);
796
+ return null;
797
+ }
798
+ if (!Array.isArray(parsed?.commands)) {
799
+ alert('The JSON must have a "commands" array.');
800
+ return null;
801
+ }
802
+ return parsed;
221
803
  }
222
804
  loadScript(script) {
223
805
  this._script = script;
806
+ if (this._commandListFancyTree) {
807
+ this._commandListFancyTree.destroy();
808
+ this._commandListDiv = this._getDomElement("commandList");
809
+ }
224
810
  let commandListTreeItems = [];
225
811
  for (let c of this._script.commands) {
226
812
  c = ScriptUpgrades.upgradeScriptCommand(c);
227
813
  commandListTreeItems.push(this.createTreeItem(c));
228
814
  }
229
- ;
230
815
  this._commandListFancyTree = new Wunderbaum({
231
816
  ...defaultOptions,
232
817
  element: this._commandListDiv,
233
818
  icon: false,
234
819
  source: commandListTreeItems,
235
820
  activate: (e) => {
236
- this._propertygrid.selectedObject = e.node.data.data.item;
821
+ this._propertygrid.selectedObject = e.node.data.data.item ?? null;
237
822
  },
238
823
  render: (e) => {
824
+ const isBranch = !!e.node.data.data?.branch;
239
825
  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
- }
826
+ e.nodeElem.oncontextmenu = (ev) => {
246
827
  ev.preventDefault();
247
828
  return false;
248
829
  };
830
+ const leadingSpacer = document.createElement("span");
831
+ leadingSpacer.style.cssText = "display:inline-block; width:24px;";
832
+ e.nodeElem.prepend(leadingSpacer);
833
+ if (!isBranch) {
834
+ const handle = document.createElement("div");
835
+ handle.className = "drag-handle";
836
+ handle.title = "Drag to reorder";
837
+ handle.innerHTML =
838
+ '<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>';
839
+ e.nodeElem.prepend(handle);
840
+ const rowElem = e.nodeElem.parentElement;
841
+ const spacer = document.createElement("span");
842
+ spacer.style.cssText = "display:inline-block; width:32px;";
843
+ e.nodeElem.appendChild(spacer);
844
+ const cmd = document.createElement("div");
845
+ cmd.className = "cmd";
846
+ const copyImg = document.createElement("img");
847
+ copyImg.src = assetsPath + "icons/copy.svg";
848
+ copyImg.title = "Duplicate";
849
+ copyImg.onclick = () => {
850
+ const clone = structuredClone(e.node.data.data.item);
851
+ const newNode = e.node.parent.addChildren(this.createTreeItem(clone));
852
+ newNode.moveTo(e.node, "after");
853
+ };
854
+ const delImg = document.createElement("img");
855
+ delImg.src = assetsPath + "icons/delete.svg";
856
+ delImg.title = "Remove";
857
+ delImg.onclick = () => e.node.remove();
858
+ cmd.append(copyImg, delImg);
859
+ rowElem.appendChild(cmd);
860
+ //@ts-ignore
861
+ e.nodeElem._reservedRowWidth =
862
+ leadingSpacer.offsetWidth + spacer.offsetWidth;
863
+ }
864
+ else {
865
+ //@ts-ignore
866
+ e.nodeElem._reservedRowWidth = leadingSpacer.offsetWidth;
867
+ }
868
+ }
869
+ //@ts-ignore
870
+ const reserved = e.nodeElem._reservedRowWidth ?? 0;
871
+ if (reserved) {
872
+ const titleSpan = e.nodeElem.querySelector("span.wb-title");
873
+ if (titleSpan) {
874
+ const w = parseFloat(titleSpan.style.width);
875
+ if (!isNaN(w))
876
+ titleSpan.style.width = Math.max(0, w - reserved) + "px";
877
+ }
249
878
  }
250
879
  },
251
880
  dnd: {
@@ -254,42 +883,147 @@ export class SimpleScriptEditor extends BaseCustomWebComponentConstructorAppend
254
883
  preventVoidMoves: false,
255
884
  serializeClipboardData: false,
256
885
  dragStart: (e) => {
886
+ if (!this._dragFromHandle)
887
+ return false;
888
+ this._draggedNode = e.node;
257
889
  e.event.dataTransfer.effectAllowed = "move";
258
890
  e.event.dataTransfer.dropEffect = "move";
891
+ e.event.dataTransfer.setData(SAME_TREE_MOVE_MIME, "1");
259
892
  return true;
260
893
  },
894
+ dragEnd: () => {
895
+ this._draggedNode = null;
896
+ },
261
897
  dragEnter: (e) => {
262
- e.event.dataTransfer.dropEffect = 'move';
263
- return true;
898
+ const isMove = e.event.dataTransfer?.types.includes(SAME_TREE_MOVE_MIME);
899
+ e.event.dataTransfer.dropEffect = isMove ? "move" : "copy";
900
+ const isBranchTarget = !!e.node.data.data?.branch;
901
+ return isBranchTarget ? new Set(["over"]) : new Set(["before", "after"]);
264
902
  },
265
903
  dragOver: (e) => {
266
- e.event.dataTransfer.dropEffect = 'move';
904
+ const isMove = e.event.dataTransfer?.types.includes(SAME_TREE_MOVE_MIME);
905
+ e.event.dataTransfer.dropEffect = isMove ? "move" : "copy";
267
906
  },
268
907
  drop: async (e) => {
269
- e.sourceNode.moveTo(e.node, e.region == 'before' ? 'before' : 'after');
270
- }
271
- }
908
+ const isMove = e.event.dataTransfer?.types.includes(SAME_TREE_MOVE_MIME);
909
+ if (e.region === "over") {
910
+ if (isMove && this._draggedNode) {
911
+ this._draggedNode.moveTo(e.node, "appendChild");
912
+ this._draggedNode.setClass("wb-drag-source", false);
913
+ }
914
+ else {
915
+ const type = e.event.dataTransfer?.getData("text/plain");
916
+ if (type) {
917
+ e.node.addChildren(this.createTreeItem({ type }));
918
+ }
919
+ }
920
+ return;
921
+ }
922
+ const region = e.region == "before" ? "before" : "after";
923
+ if (isMove && this._draggedNode) {
924
+ this._draggedNode.moveTo(e.node, region);
925
+ this._draggedNode.setClass("wb-drag-source", false);
926
+ }
927
+ else {
928
+ const type = e.event.dataTransfer?.getData("text/plain");
929
+ if (type) {
930
+ const command = { type };
931
+ const ti = this.createTreeItem(command);
932
+ const newNode = e.node.parent.addChildren(ti);
933
+ newNode.moveTo(e.node, region);
934
+ }
935
+ }
936
+ },
937
+ },
272
938
  });
939
+ this._commandListFancyTree.root.children?.[0]?.setActive();
273
940
  }
274
941
  createTreeItem(currentItem) {
275
- let cti = {
942
+ if (currentItem.type === "If") {
943
+ const ifCommand = currentItem;
944
+ ifCommand.trueCommands ??= [];
945
+ ifCommand.elseCommands ??= [];
946
+ return {
947
+ title: "If",
948
+ expanded: true,
949
+ data: { item: currentItem },
950
+ children: [
951
+ {
952
+ title: "true",
953
+ expanded: true,
954
+ data: { branch: "true" },
955
+ children: ifCommand.trueCommands.map((c) => this.createTreeItem(c)),
956
+ },
957
+ {
958
+ title: "else",
959
+ expanded: true,
960
+ data: { branch: "else" },
961
+ children: ifCommand.elseCommands.map((c) => this.createTreeItem(c)),
962
+ },
963
+ ],
964
+ };
965
+ }
966
+ return {
276
967
  title: currentItem.type,
277
968
  data: { item: currentItem },
278
- contextMenu: (e, data, node) => {
279
- ContextMenu.show([{ title: 'Remove Item', action: (e) => node.remove() }], e);
280
- }
281
969
  };
282
- return cti;
283
970
  }
284
- addItem() {
285
- const cmdName = this._possibleCommands.value;
286
- const command = { type: cmdName };
287
- const ti = this.createTreeItem(command);
288
- this._commandListFancyTree.addChildren(ti);
971
+ /** Reconstructs a ScriptCommand (and, for "If", its true/else branches) from a tree node. */
972
+ nodeToCommand(node) {
973
+ const item = node.data.data.item;
974
+ if (item.type === "If") {
975
+ const trueNode = node.children?.find((c) => c.data.data?.branch === "true");
976
+ const elseNode = node.children?.find((c) => c.data.data?.branch === "else");
977
+ item.trueCommands = (trueNode?.children ?? []).map((c) => this.nodeToCommand(c));
978
+ item.elseCommands = (elseNode?.children ?? []).map((c) => this.nodeToCommand(c));
979
+ }
980
+ return item;
981
+ }
982
+ /** Walks up from `node` to the nearest branch ("true"/"else") container node, if any. */
983
+ _getBranchContainerNode(node) {
984
+ let n = node;
985
+ while (n) {
986
+ if (n.data?.data?.branch)
987
+ return n;
988
+ n = n.parent;
989
+ }
990
+ return null;
991
+ }
992
+ async addCommand() {
993
+ const picker = new SimpleScriptCommandPicker();
994
+ picker.loadCommands(this.scriptCommandsTypeInfo, this.commandDescriptions);
995
+ picker.title = "Add Script Command";
996
+ picker.commandTypeChosen.on(() => {
997
+ if (picker.selectedType) {
998
+ const command = { type: picker.selectedType };
999
+ const ti = this.createTreeItem(command);
1000
+ const branchContainer = this._getBranchContainerNode(this._commandListFancyTree.activeNode);
1001
+ if (branchContainer)
1002
+ branchContainer.addChildren(ti);
1003
+ else
1004
+ this._commandListFancyTree.addChildren(ti);
1005
+ }
1006
+ });
1007
+ const abortController = new AbortController();
1008
+ picker.closeRequested.on(() => abortController.abort());
1009
+ await this.visualizationShell.openModal(picker, {
1010
+ title: "Add Script Command",
1011
+ x: 300,
1012
+ y: 50,
1013
+ width: 620,
1014
+ height: 400,
1015
+ parent: this,
1016
+ abortSignal: abortController.signal,
1017
+ });
289
1018
  }
290
1019
  getScriptCommands() {
1020
+ if (this._showingCode) {
1021
+ const parsed = this._parseCodeViewScript();
1022
+ if (parsed)
1023
+ this.loadScript(parsed);
1024
+ }
291
1025
  let children = this._commandListFancyTree.root.children;
292
- return children.map(x => x.data.data.item);
1026
+ return children.map((x) => this.nodeToCommand(x));
293
1027
  }
294
1028
  }
295
1029
  customElements.define(SimpleScriptEditor.is, SimpleScriptEditor);