@node-projects/web-component-designer-visualization-addons 0.1.148 → 0.1.149

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,174 +1,319 @@
1
1
  import { BaseCustomWebComponentConstructorAppend, html, css } from '@node-projects/base-custom-webcomponent';
2
- import { BindingMode, BindingTarget } from '@node-projects/web-component-designer';
2
+ import { BindingMode, BindingTarget, assetsPath } from '@node-projects/web-component-designer';
3
3
  import { BindingsEditorHistoric } from './BindingsEditorHistoric.js';
4
+ import { bindingsEditorHelpHtml } from './BindingsEditorHelp.js';
4
5
  export class BindingsEditor extends BaseCustomWebComponentConstructorAppend {
5
- static template = html `
6
- <div id="root">
7
- <div class="vertical-grid">
8
- <div style="grid-column: 1/3">
9
- <div style="display: flex; flex-direction: column;">
10
- <div class="row">
11
- <span style="cursor: pointer;" title="to use multiple objects, seprate them with semicolon (;).&#010;Access signal objects in properties via ?propertyName, access the propertyValue via ??propertyName.&#010;Access signal objects in properties of the target via #propertyName, access a propertyValue of the target via ##propertyName.&#010;Bind to signal configurations via $objectId.&#010;Bind to special values via §name (if supported by your framework).&#010;You could also use signals inside of a Signal Name via {name}">objects</span>
12
- </div>
13
- <div id="groupObjectName" style="display:flex;align-items: flex-end;">
14
- <input id="objectName" class="row" value="{{?this.objectNames::change}}" style="flex-grow: 1;">
15
- <button @click="_clear" style="height: 22px">X</button>
16
- <button @click="_select" style="height: 22px">...</button>
17
- </div>
18
- <div id="groupObjectType" class="row">
19
- <label style="white-space: nowrap; margin-right: 4px;" title="if set, the value is converted to the type before binding is applied">type :</label>
20
- <select class="row" value="{{?this.objectValueType}}">
21
- <option selected value="">ignore</option>
22
- <option value="number">number</option>
23
- <option value="boolean">boolean</option>
24
- <option value="string">string</string>
25
- </select>
26
- </div>
27
- <div id="groupBindingMode" class="row">
28
- <input type="checkbox" disabled="[[!this.twoWayPossible]]" checked="{{this.twoWay::change}}" @change="_refresh">
29
- <span>two way binding</span>
30
- <span css:display="[[this.twoWay ? 'inline' : 'none']]" style="margin-left: 15px">events:&nbsp;</span>
31
- <input css:display="[[this.twoWay ? 'inline-block' : 'none']]" title="to use multiple events, seprate them with semicolon (;)" class="row" value="{{?this.events::change}}" style="flex-grow: 1;">
32
- </div>
33
- <div id="groupinvert" class="row" style="position: relative">
34
- <input type="checkbox" checked="{{this.invert::change}}">
35
- <span>invert logic</span>
36
- <button css:border="[[this.historic ? 'solid lime 5px' : 'none']]" style="position: absolute; right: 1px; top: 5px; padding: 10px;" @click="showHistoric">historic</button>
37
- </div>
38
- <div class="row">
39
- <span style="cursor: pointer;" title="javascript expression. access context with __ctx, result with __res, objects with __0, __1, ...">formula</span>
40
- </div>
41
- <div class="row">
42
- <node-projects-code-view-monaco id="expression" single-row language="javascript" style="width: 100%; min-height: 17px; height: 17px; position: relative; overflow: hidden; resize: vertical;" .code="{{?this.expression}}" @code-changed="_refresh"></node-projects-code-view-monaco>
43
- </div>
44
- <div class="row">
45
- <span style="text-wrap: nowrap" style="cursor: pointer;" title="write back the value build by a formula to a signal. maybe only usefull when a formula is used.">write back signal :</span>
46
- <input style="width: 100%; margin-left: 5px;" .disabled="[[!this.expression]]" value="{{?this.writeBackSignal::change}}">
47
- </div>
48
- <div class="row">
49
- <span style="cursor: pointer;" title="javascript expression. access property with 'value'">formula write back (two way)</span>
50
- </div>
51
- <div class="row">
52
- <node-projects-code-view-monaco id="expression2way" .read-only="[[!this.twoWay]]" $readonly="[[!this.twoWay]]" single-row language="javascript" style="width: 100%; min-height: 17px; height: 17px; position: relative; overflow: hidden; resize: vertical;" .code="{{?this.expressionTwoWay}}"></node-projects-code-view-monaco>
53
- </div>
54
- </div>
55
- </div>
56
- </div>
57
- <div class="vertical-grid" style="margin-top: 10px;">
58
- <div>
59
- <div class="input-headline" style="display: flex;align-items: center;">
60
- <span>converter</span>:<input id="namedConverterInput" style="width: 100%; margin-left: 15px;" value="{{?this.convertersString::change}}">
61
- </div>
62
- </div>
63
- </div>
64
- <div class="vertical-grid" style="border: solid 1px black; padding: 10px; overflow-y: auto;">
65
- <div class="bottomleft">
66
- <div id="converterGrid" style="height: 100%;">
67
- <div style="width: 100%; height: 20px; display: flex;">
68
- <div style="width: 39%">condition</div>
69
- <div style="width: 59%">value</div>
70
- </div>
71
- <template repeat:item="[[this.converters]]">
72
- <div css:background-color="[[item.activeRow ? 'gray' : '']]" style="width: 100%; display: flex; height: 26px; justify-content: center; align-items: center; gap: 5px;">
73
- <input type="text" value="{{item.key}}" @focus="[[this._focusRow(index)]]" style="width: 39%">
74
- <input type="[[this._property.type == 'color' ? 'color' : 'text']]" value="{{item.value}}" @focus="[[this._focusRow(index)]]" style="width: 59%">
75
- </div>
76
- </template>
77
- </div>
78
- </div>
79
- <div class="controlbox" id="grid-controls">
80
- <button type="button" id="add-row-button" value="add" @click="addConverter">
81
- <span>add</span>
82
- </button>
83
- <button id="remove-row-button" value="remove" style="margin-top: 6px;" @click="removeConverter">
84
- <span>remove</span>
85
- </button>
86
- </div>
87
- </div>
6
+ static template = html `
7
+ <div id="root">
8
+ <div class="vertical-grid">
9
+ <div id="full-width-col">
10
+ <div id="signals-area">
11
+ <div class="row">
12
+ <span class="section-label" title="Add signals by name and path. The variable name is used in the formula (__0, __1,... or a custom name).&#010;Access signal objects in properties via ?propertyName, access the propertyValue via ??propertyName.&#010;Access signal objects in properties of the target via #propertyName, access a propertyValue of the target via ##propertyName.&#010;Bind to signal configurations via $objectId.&#010;Bind to special values via §name (if supported by your framework).&#010;You could also use signals inside of a Signal Name via {name}">objects</span>
13
+ <button id="helpBtn" class="info-btn" title="Help"><svg viewBox="0 0 512 512" width="16" height="16"><path 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"/></svg></button>
14
+ </div>
15
+ <div class="bordered-section">
16
+ <div id="signalListHeader" class="signal-row signal-header">
17
+ <span class="signal-header-name">name</span>
18
+ <span class="signal-header-signal">signal</span>
19
+ <span class="signal-header-spacer"></span>
20
+ </div>
21
+ <div id="signalListContainer"></div>
22
+ <button id="addSignalBtn" class="add-signal-btn" @click="[[this._addSignal()]]">+ Add signal</button>
23
+ </div>
24
+ <div id="groupinvert" class="row">
25
+ <input type="checkbox" checked="{{this.invert::change}}">
26
+ <span>invert logic</span>
27
+ <button id="historicBtn" css:border="[[this.historic ? 'solid lime 5px' : 'none']]" @click="[[this.showHistoric()]]">historic</button>
28
+ </div>
29
+ <div class="row">
30
+ <span class="section-label" title="javascript expression. access context with __ctx, result with __res, objects by name (__0, __1,... or custom name)">formula</span>
31
+ </div>
32
+ <div class="row">
33
+ <node-projects-code-view-monaco id="expression" single-row language="javascript" .code="{{?this.expression}}" @code-changed="_refresh"></node-projects-code-view-monaco>
34
+ </div>
35
+ <div class="row">
36
+ <span class="section-label writeback-label" title="write back the value build by a formula to a signal. maybe only usefull when a formula is used.">write back signal :</span>
37
+ <input class="writeback-input" .disabled="[[!this.expression]]" value="{{?this.writeBackSignal::change}}">
38
+ </div>
39
+ <div id="groupBindingMode" class="row">
40
+ <input type="checkbox" disabled="[[!this.twoWayPossible]]" checked="{{this.twoWay::change}}" @change="_refresh">
41
+ <span>two way binding</span>
42
+ <span class="events-label" css:display="[[this.twoWay ? 'inline' : 'none']]">events:&nbsp;</span>
43
+ <input class="events-input" css:display="[[this.twoWay ? 'inline-block' : 'none']]" title="to use multiple events, seprate them with semicolon (;)" value="{{?this.events::change}}">
44
+ </div>
45
+ <div css:display="[[this.twoWay ? 'block' : 'none']]">
46
+ <div class="row">
47
+ <span class="section-label" title="javascript expression. access property with 'value'">formula write back (two way)</span>
48
+ </div>
49
+ <div class="row">
50
+ <node-projects-code-view-monaco id="expression2way" .read-only="[[!this.twoWay]]" $readonly="[[!this.twoWay]]" single-row language="javascript" .code="{{?this.expressionTwoWay}}"></node-projects-code-view-monaco>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ <div id="namedConverterRow" class="row">
57
+ <span class="converter-label">converter :</span>
58
+ <input id="namedConverterInput" value="{{?this.convertersString::change}}">
59
+ <button id="namedConverterBrowse" class="signal-select-btn" @click="[[this._selectNamedConverter()]]">...</button>
60
+ </div>
61
+ <div class="bordered-section">
62
+ <div class="signal-row signal-header">
63
+ <span class="converter-header-col">condition</span>
64
+ <span class="converter-header-col">value</span>
65
+ <span class="converter-header-spacer"></span>
66
+ </div>
67
+ <div id="converterListContainer"></div>
68
+ <button class="add-signal-btn" @click="[[this._addConverter()]]">+ Add entry</button>
69
+ </div>
88
70
  </div>`;
89
- static style = css `
90
- :host {
91
- box-sizing: border-box;
92
- }
93
-
94
- #converterGrid {
95
- display: flex;
96
- gap: 2px;
97
- flex-direction: column;
98
- }
99
- #converterGrid input {
100
- height: 20px;
101
- box-sizing: border-box;
102
- }
103
-
104
- .padding_top {
105
- padding-top: 30px;
106
- }
107
-
108
- .row{
109
- margin-top: 3px;
110
- display: flex;
111
- align-items: center;
112
- }
113
-
114
- .controlbox {
115
- display: flex;
116
- flex-direction: column;
117
- }
118
-
119
- .input-headline {
120
- height: 30px;
121
- }
122
-
123
- input[type="checkbox"] {
124
- margin-right: 15px;
125
- width: 15px;
126
- height: 15px;
127
- }
128
-
129
- select {
130
- width: 100%;
131
- }
132
-
133
- #root {
134
- padding: 2px 10px;
135
- display: grid;
136
- grid-template-rows: min-content min-content;
137
- overflow: auto;
138
- height: calc(100% - 4px)
139
- }
140
-
141
- .vertical-grid {
142
- display: grid;
143
- grid-template-columns: calc((100% - 150px) - 30px) 150px;
144
- gap: 30px;
145
- }
146
-
147
- #grid input, #list input {
148
- border:0px;
149
- }
150
-
151
- #tagdata_type {
152
- height: 24px;
153
- font-size: inherit;
154
- }
155
-
156
- node-projects-code-view-monaco:not([readonly]) {
157
- border: 1px black solid;
158
- }
159
-
160
- node-projects-code-view-monaco[readonly] {
161
- border: 1px lightgray solid;
71
+ static style = css `
72
+ :host {
73
+ box-sizing: border-box;
74
+ }
75
+
76
+ .padding_top {
77
+ padding-top: 30px;
78
+ }
79
+
80
+ .row{
81
+ margin-top: 3px;
82
+ display: flex;
83
+ align-items: center;
84
+ }
85
+
86
+ #converterListContainer {
87
+ max-height: 72px;
88
+ overflow-y: auto;
89
+ }
90
+
91
+ .input-headline {
92
+ height: 30px;
93
+ }
94
+
95
+ input[type="checkbox"] {
96
+ margin-right: 15px;
97
+ width: 15px;
98
+ height: 15px;
99
+ }
100
+
101
+ select {
102
+ width: 100%;
103
+ }
104
+
105
+ #root {
106
+ padding: 2px 10px;
107
+ display: grid;
108
+ grid-template-rows: min-content min-content;
109
+ overflow: auto;
110
+ height: calc(100% - 4px)
111
+ }
112
+
113
+ .vertical-grid {
114
+ display: grid;
115
+ grid-template-columns: calc((100% - 150px) - 30px) 150px;
116
+ gap: 30px;
117
+ }
118
+
119
+ #full-width-col {
120
+ grid-column: 1/3;
121
+ }
122
+
123
+ #signals-area {
124
+ display: flex;
125
+ flex-direction: column;
126
+ }
127
+
128
+ .section-label {
129
+ cursor: pointer;
130
+ }
131
+
132
+ .bordered-section {
133
+ border: solid 1px black;
134
+ padding: 10px;
135
+ }
136
+
137
+ .signal-header-name {
138
+ flex: 0 0 80px;
139
+ }
140
+
141
+ .signal-header-signal, .converter-header-col {
142
+ flex: 1 1 auto;
143
+ }
144
+
145
+ .signal-header-spacer {
146
+ flex: 0 0 72px;
147
+ }
148
+
149
+ .converter-header-spacer {
150
+ flex: 0 0 47px;
151
+ }
152
+
153
+ #groupinvert {
154
+ position: relative;
155
+ }
156
+
157
+ #historicBtn {
158
+ position: absolute;
159
+ right: 1px;
160
+ top: 5px;
161
+ padding: 10px;
162
+ }
163
+
164
+ node-projects-code-view-monaco {
165
+ width: 100%;
166
+ min-height: 17px;
167
+ height: 17px;
168
+ position: relative;
169
+ overflow: hidden;
170
+ resize: vertical;
171
+ }
172
+
173
+ .writeback-label {
174
+ white-space: nowrap;
175
+ }
176
+
177
+ .writeback-input {
178
+ width: 100%;
179
+ margin-left: 5px;
180
+ }
181
+
182
+ .events-label {
183
+ margin-left: 15px;
184
+ }
185
+
186
+ .events-input {
187
+ flex-grow: 1;
188
+ }
189
+
190
+ #namedConverterRow {
191
+ margin-top: 6px;
192
+ }
193
+
194
+ .converter-label {
195
+ white-space: nowrap;
196
+ }
197
+
198
+ #namedConverterInput {
199
+ flex: 1;
200
+ margin-left: 5px;
201
+ }
202
+
203
+ #namedConverterBrowse {
204
+ margin-left: 3px;
205
+ }
206
+
207
+ #grid input, #list input {
208
+ border:0px;
209
+ }
210
+
211
+ #tagdata_type {
212
+ height: 24px;
213
+ font-size: inherit;
214
+ }
215
+
216
+ node-projects-code-view-monaco:not([readonly]) {
217
+ border: 1px black solid;
218
+ }
219
+
220
+ node-projects-code-view-monaco[readonly] {
221
+ border: 1px lightgray solid;
222
+ }
223
+
224
+ .signal-header {
225
+ color: #888;
226
+ font-size: 11px;
227
+ margin-top: 3px;
228
+ }
229
+
230
+ .signal-row {
231
+ display: flex;
232
+ align-items: center;
233
+ gap: 3px;
234
+ }
235
+
236
+ .signal-row input {
237
+ height: 22px;
238
+ box-sizing: border-box;
239
+ }
240
+
241
+ .signal-name-input {
242
+ min-width: 80px;
243
+ flex: 0 0 80px;
244
+ font-family: monospace;
245
+ }
246
+
247
+ .signal-path-input {
248
+ flex: 1 1 auto;
249
+ }
250
+
251
+ .signal-icon-btn {
252
+ flex: 0 0 22px;
253
+ width: 22px;
254
+ height: 22px;
255
+ padding: 0;
256
+ cursor: pointer;
257
+ display: flex;
258
+ align-items: center;
259
+ justify-content: center;
260
+ }
261
+
262
+ .signal-icon-btn img {
263
+ width: 12px;
264
+ height: 12px;
265
+ }
266
+
267
+ .signal-select-btn {
268
+ flex: 0 0 26px;
269
+ width: 26px;
270
+ height: 22px;
271
+ padding: 0;
272
+ cursor: pointer;
273
+ font-size: 11px;
274
+ }
275
+
276
+ #signalListContainer {
277
+ display: flex;
278
+ flex-direction: column;
279
+ gap: 3px;
280
+ max-height: 72px;
281
+ overflow-y: auto;
282
+ }
283
+
284
+ .add-signal-btn {
285
+ align-self: flex-start;
286
+ margin-top: 4px;
287
+ height: 22px;
288
+ cursor: pointer;
289
+ font-size: 11px;
290
+ }
291
+
292
+ .info-btn {
293
+ display: flex;
294
+ align-items: center;
295
+ justify-content: center;
296
+ width: 22px;
297
+ height: 22px;
298
+ padding: 0;
299
+ border: none;
300
+ background: transparent;
301
+ cursor: pointer;
302
+ flex-shrink: 0;
303
+ margin-left: auto;
304
+ }
305
+
306
+ .info-btn:hover {
307
+ background: #eee;
308
+ border-radius: 3px;
162
309
  }`;
163
310
  static is = 'node-projects-visualization-bindings-editor';
164
311
  static properties = {
165
312
  twoWayPossible: Boolean,
166
313
  twoWay: Boolean,
167
314
  expression: String,
168
- objectNames: String,
169
315
  events: String,
170
316
  invert: Boolean,
171
- converters: Array,
172
317
  historic: Object
173
318
  };
174
319
  twoWayPossible = false;
@@ -181,36 +326,57 @@ export class BindingsEditor extends BaseCustomWebComponentConstructorAppend {
181
326
  events = '';
182
327
  invert = false;
183
328
  converters = [];
329
+ static namedConverters = [];
330
+ /** Extra help-page sections (raw HTML, same markup/classes as BindingsEditorHelp.ts) contributed by the host application.
331
+ * Left empty by default so the plain designer help page is unaffected. */
332
+ static helpSections = [];
184
333
  convertersString;
185
- objectValueType;
334
+ /** Unified converter value for use in IBinding.converters when saving. Returns the named converter
335
+ * string when set, otherwise converts the inline key/value table to a Record, or undefined if empty. */
336
+ get converter() {
337
+ if (this.convertersString)
338
+ return this.convertersString;
339
+ if (this.converters.length > 0)
340
+ return Object.fromEntries(this.converters.map(c => [c.key, c.value]));
341
+ return undefined;
342
+ }
186
343
  _property;
187
344
  _binding;
188
345
  _bindingTarget;
189
346
  _serviceContainer;
190
347
  _instanceServiceContainer;
191
348
  _shell;
192
- _activeRow = -1;
193
- _objNmInput;
194
- constructor(property, binding, bindingTarget, serviceContainer, instanceServiceContainer, shell, config = { namedConverters: true }) {
349
+ _signals = [];
350
+ _signalListContainer;
351
+ _converterListContainer;
352
+ constructor(property, binding, bindingTarget, serviceContainer, instanceServiceContainer, shell, config = {}) {
195
353
  super();
196
354
  super._restoreCachedInititalValues();
197
- //TODOS:
198
- //typed values for converters
199
- //default value for converter
200
- this._objNmInput = this._getDomElement('objectName');
201
355
  this._property = property;
202
356
  this._binding = binding;
203
357
  this._bindingTarget = bindingTarget;
204
358
  this._serviceContainer = serviceContainer;
205
359
  this._instanceServiceContainer = instanceServiceContainer;
206
360
  this._shell = shell;
207
- if (!config?.namedConverters) {
208
- this._getDomElement('namedConverterInput').style.display = 'none';
361
+ if (config?.namedConverters === false) {
362
+ this._getDomElement('namedConverterRow').style.display = 'none';
363
+ }
364
+ const historicBtn = this._getDomElement('historicBtn');
365
+ if (config?.showHistoric === false) {
366
+ historicBtn.style.display = 'none';
367
+ }
368
+ else if (config?.historicLabel) {
369
+ historicBtn.textContent = config.historicLabel;
209
370
  }
210
371
  }
211
372
  ready() {
212
373
  this._parseAttributesToProperties();
213
374
  this._assignEvents();
375
+ this._signalListContainer = this._getDomElement('signalListContainer');
376
+ this._converterListContainer = this._getDomElement('converterListContainer');
377
+ this._getDomElement('helpBtn').onclick = () => this._showHelp();
378
+ if (BindingsEditor.namedConverters.length === 0)
379
+ this._getDomElement('namedConverterBrowse').style.display = 'none';
214
380
  this.twoWayPossible = false;
215
381
  if (this._bindingTarget == BindingTarget.property || this._bindingTarget == BindingTarget.attribute)
216
382
  this.twoWayPossible = true;
@@ -221,7 +387,6 @@ export class BindingsEditor extends BaseCustomWebComponentConstructorAppend {
221
387
  this.expressionTwoWay = this._binding.expressionTwoWay;
222
388
  this.historic = this._binding.historic;
223
389
  this.invert = this._binding.invert;
224
- this.objectValueType = this._binding.type;
225
390
  if (this._binding.bindableObjectNames)
226
391
  this.objectNames = this._binding.bindableObjectNames.join(';');
227
392
  if (this._binding.converter) {
@@ -249,54 +414,257 @@ export class BindingsEditor extends BaseCustomWebComponentConstructorAppend {
249
414
  edt.style.height = (3 * 17) + 'px';
250
415
  }
251
416
  }
417
+ this._signals = this._parseObjectNames(this.objectNames);
418
+ if (this._signals.length === 0)
419
+ this._signals.push({ name: '', signal: '' });
420
+ this._renderSignalList();
421
+ this._renderConverterList();
252
422
  this._bindingsParse();
253
- this._objNmInput.focus();
254
423
  }
255
- _focusRow(index) {
256
- this._activeRow = index;
257
- this._updatefocusedRow();
424
+ _parseObjectNames(str) {
425
+ if (!str)
426
+ return [];
427
+ return str.split(';').filter(s => s.length > 0).map(s => {
428
+ const colonIdx = s.indexOf(':');
429
+ if (colonIdx > 0) {
430
+ return { name: s.slice(0, colonIdx), signal: s.slice(colonIdx + 1) };
431
+ }
432
+ return { name: '', signal: s };
433
+ });
434
+ }
435
+ _signalsToObjectNames() {
436
+ return this._signals.map(s => s.name ? s.name + ':' + s.signal : s.signal).join(';');
258
437
  }
259
- _updatefocusedRow() {
260
- let g = this._getDomElement('converterGrid');
261
- g.querySelectorAll('div').forEach(x => x.style.background = '');
262
- if (this._activeRow >= 0)
263
- g.children[this._activeRow + 1].style.background = 'gray';
438
+ _renderSignalList() {
439
+ const container = this._signalListContainer;
440
+ container.innerHTML = '';
441
+ this._signals.forEach((sig, i) => {
442
+ const row = document.createElement('div');
443
+ row.className = 'signal-row';
444
+ const nameInput = document.createElement('input');
445
+ nameInput.className = 'signal-name-input';
446
+ nameInput.placeholder = `__${i}`;
447
+ nameInput.value = sig.name;
448
+ nameInput.title = 'variable name used in the formula (defaults to __' + i + ' if empty)';
449
+ nameInput.oninput = () => {
450
+ this._signals[i].name = nameInput.value;
451
+ this._syncSignals();
452
+ };
453
+ const signalInput = document.createElement('input');
454
+ signalInput.className = 'signal-path-input';
455
+ signalInput.value = sig.signal;
456
+ signalInput.placeholder = 'signal path';
457
+ signalInput.title = 'signal path (prefix: ? = signalProperty, ?? = property value, # = target property, ## = target property value)';
458
+ signalInput.oninput = () => {
459
+ this._signals[i].signal = signalInput.value;
460
+ this._syncSignals();
461
+ };
462
+ const copyBtn = document.createElement('button');
463
+ copyBtn.className = 'signal-icon-btn';
464
+ copyBtn.title = 'Duplicate';
465
+ const copyIcon = document.createElement('img');
466
+ copyIcon.src = assetsPath + 'icons/copy.svg';
467
+ copyIcon.style.cssText = 'width:12px; height:12px;';
468
+ copyBtn.appendChild(copyIcon);
469
+ copyBtn.onclick = () => {
470
+ this._signals.splice(i + 1, 0, { ...this._signals[i] });
471
+ this._syncSignals();
472
+ this._renderSignalList();
473
+ };
474
+ const selectBtn = document.createElement('button');
475
+ selectBtn.className = 'signal-select-btn';
476
+ selectBtn.textContent = '...';
477
+ selectBtn.title = 'Browse signal';
478
+ selectBtn.onclick = () => this._selectForRow(i, signalInput);
479
+ const delBtn = document.createElement('button');
480
+ delBtn.className = 'signal-icon-btn';
481
+ delBtn.title = 'Remove';
482
+ const delIcon = document.createElement('img');
483
+ delIcon.src = assetsPath + 'icons/delete.svg';
484
+ delIcon.style.cssText = 'width:12px; height:12px;';
485
+ delBtn.appendChild(delIcon);
486
+ delBtn.onclick = () => {
487
+ this._signals.splice(i, 1);
488
+ this._syncSignals();
489
+ this._renderSignalList();
490
+ };
491
+ row.appendChild(nameInput);
492
+ row.appendChild(signalInput);
493
+ row.appendChild(selectBtn);
494
+ row.appendChild(copyBtn);
495
+ row.appendChild(delBtn);
496
+ container.appendChild(row);
497
+ });
264
498
  }
265
- _clear() {
266
- this.objectNames = '';
499
+ _syncSignals() {
500
+ this.objectNames = this._signalsToObjectNames();
267
501
  this._bindingsRefresh();
268
502
  }
269
- _refresh() {
270
- requestAnimationFrame(() => {
271
- this._bindingsRefresh();
272
- });
503
+ _addSignal() {
504
+ this._signals.push({ name: '', signal: '' });
505
+ this._renderSignalList();
273
506
  }
274
- async _select() {
507
+ async _selectForRow(index, signalInput) {
275
508
  let b = this._shell.createBindableObjectBrowser();
276
509
  b.initialize(this._serviceContainer, this._instanceServiceContainer, 'binding');
277
510
  b.title = 'select signal...';
278
511
  const abortController = new AbortController();
279
512
  b.objectDoubleclicked.on(() => {
280
513
  abortController.abort();
281
- if (this.objectNames != '')
282
- this.objectNames += ';';
514
+ let prefix = '';
283
515
  if (b.selectedObject.specialType == 'signalProperty') {
284
- this.objectNames += '?';
516
+ prefix = '?';
285
517
  }
286
518
  else if (b.selectedObject.bindabletype === 'property') {
287
- this.objectNames += '??';
519
+ prefix = '??';
288
520
  }
289
- this.objectNames += b.selectedObject.fullName;
290
- this._bindingsRefresh();
521
+ this._signals[index].signal = prefix + b.selectedObject.fullName;
522
+ signalInput.value = this._signals[index].signal;
523
+ this._syncSignals();
291
524
  });
292
525
  let res = await this._shell.openConfirmation(b, { x: 100, y: 100, width: 400, height: 300, parent: this, abortSignal: abortController.signal });
293
526
  if (res) {
294
- if (this.objectNames != '')
295
- this.objectNames += ';';
296
- this.objectNames += b.selectedObject.fullName;
527
+ let prefix = '';
528
+ if (b.selectedObject.specialType == 'signalProperty') {
529
+ prefix = '?';
530
+ }
531
+ else if (b.selectedObject.bindabletype === 'property') {
532
+ prefix = '??';
533
+ }
534
+ this._signals[index].signal = prefix + b.selectedObject.fullName;
535
+ signalInput.value = this._signals[index].signal;
536
+ this._syncSignals();
537
+ }
538
+ }
539
+ _renderConverterList() {
540
+ const container = this._converterListContainer;
541
+ container.innerHTML = '';
542
+ const valueType = this._property?.type === 'color' ? 'color' : 'text';
543
+ this.converters.forEach((conv, i) => {
544
+ const row = document.createElement('div');
545
+ row.className = 'signal-row';
546
+ const keyInput = document.createElement('input');
547
+ keyInput.style.cssText = 'flex:1 1 auto; height:22px; box-sizing:border-box;';
548
+ keyInput.value = conv.key;
549
+ keyInput.placeholder = 'condition';
550
+ keyInput.oninput = () => { this.converters[i].key = keyInput.value; };
551
+ const valInput = document.createElement('input');
552
+ valInput.type = valueType;
553
+ valInput.style.cssText = 'flex:1 1 auto; height:22px; box-sizing:border-box;';
554
+ valInput.value = conv.value;
555
+ valInput.placeholder = 'value';
556
+ valInput.oninput = () => { this.converters[i].value = valInput.value; };
557
+ const copyBtn = document.createElement('button');
558
+ copyBtn.className = 'signal-icon-btn';
559
+ copyBtn.title = 'Duplicate';
560
+ const copyIcon = document.createElement('img');
561
+ copyIcon.src = assetsPath + 'icons/copy.svg';
562
+ copyIcon.style.cssText = 'width:12px; height:12px;';
563
+ copyBtn.appendChild(copyIcon);
564
+ copyBtn.onclick = () => {
565
+ this.converters.splice(i + 1, 0, { ...this.converters[i] });
566
+ this._renderConverterList();
567
+ };
568
+ const delBtn = document.createElement('button');
569
+ delBtn.className = 'signal-icon-btn';
570
+ delBtn.title = 'Remove';
571
+ const delIcon = document.createElement('img');
572
+ delIcon.src = assetsPath + 'icons/delete.svg';
573
+ delIcon.style.cssText = 'width:12px; height:12px;';
574
+ delBtn.appendChild(delIcon);
575
+ delBtn.onclick = () => {
576
+ this.converters.splice(i, 1);
577
+ this._renderConverterList();
578
+ };
579
+ row.appendChild(keyInput);
580
+ row.appendChild(valInput);
581
+ row.appendChild(copyBtn);
582
+ row.appendChild(delBtn);
583
+ container.appendChild(row);
584
+ });
585
+ }
586
+ _addConverter() {
587
+ this.converters.push({ key: '', value: '' });
588
+ this._renderConverterList();
589
+ }
590
+ async _selectNamedConverter() {
591
+ const list = BindingsEditor.namedConverters;
592
+ if (list.length === 0)
593
+ return;
594
+ let selectedName = '';
595
+ let selectedRow = null;
596
+ const outer = document.createElement('div');
597
+ outer.style.cssText = 'display:flex; flex-direction:column; height:100%; box-sizing:border-box; font-size:12px;';
598
+ const hint = document.createElement('div');
599
+ hint.style.cssText = 'padding:6px 8px 4px; color:#666; font-size:11px; border-bottom:1px solid #e0e0e0; flex-shrink:0;';
600
+ hint.textContent = 'Select a converter and confirm with OK.';
601
+ outer.appendChild(hint);
602
+ const listEl = document.createElement('div');
603
+ listEl.style.cssText = 'flex:1; overflow-y:auto; padding:4px;';
604
+ outer.appendChild(listEl);
605
+ const deselect = (row) => {
606
+ row.style.background = '';
607
+ row.style.borderLeftColor = 'transparent';
608
+ };
609
+ const select = (row, name) => {
610
+ if (selectedRow)
611
+ deselect(selectedRow);
612
+ selectedRow = row;
613
+ row.style.background = '#e8f4fd';
614
+ row.style.borderLeftColor = '#0078d7';
615
+ selectedName = name;
616
+ };
617
+ for (const nc of list) {
618
+ const row = document.createElement('div');
619
+ row.style.cssText = 'padding:5px 6px 5px 9px; cursor:pointer; border-radius:3px; border-left:3px solid transparent; margin-bottom:2px;';
620
+ const nameEl = document.createElement('div');
621
+ nameEl.style.cssText = 'font-family:monospace; font-weight:bold; color:#111;';
622
+ nameEl.textContent = nc.name;
623
+ row.appendChild(nameEl);
624
+ if (nc.description) {
625
+ const descEl = document.createElement('div');
626
+ descEl.style.cssText = 'color:#555; font-size:11px; margin-top:2px;';
627
+ descEl.textContent = nc.description;
628
+ row.appendChild(descEl);
629
+ }
630
+ if (nc.converter && Object.keys(nc.converter).length > 0) {
631
+ const table = document.createElement('div');
632
+ table.style.cssText = 'display:grid; grid-template-columns:max-content max-content 1fr; gap:1px 6px; margin-top:4px; padding:4px 6px; background:#f5f5f5; border-radius:2px; font-family:monospace; font-size:11px; color:#333;';
633
+ for (const [k, v] of Object.entries(nc.converter)) {
634
+ const keyEl = document.createElement('span');
635
+ keyEl.style.cssText = 'color:#555; text-align:right;';
636
+ keyEl.textContent = k;
637
+ const arrow = document.createElement('span');
638
+ arrow.style.cssText = 'color:#aaa;';
639
+ arrow.textContent = '→';
640
+ const valEl = document.createElement('span');
641
+ valEl.style.cssText = 'color:#0078d7; font-weight:bold;';
642
+ valEl.textContent = String(v);
643
+ table.appendChild(keyEl);
644
+ table.appendChild(arrow);
645
+ table.appendChild(valEl);
646
+ }
647
+ row.appendChild(table);
648
+ }
649
+ row.onmouseenter = () => { if (row !== selectedRow)
650
+ row.style.background = '#f5f5f5'; };
651
+ row.onmouseleave = () => { if (row !== selectedRow)
652
+ row.style.background = ''; };
653
+ row.onclick = () => select(row, nc.name);
654
+ listEl.appendChild(row);
655
+ }
656
+ const res = await this._shell.openConfirmation(outer, { x: 100, y: 100, width: 320, height: 300, parent: this });
657
+ if (res && selectedName) {
658
+ this.convertersString = selectedName;
659
+ this._getDomElement('namedConverterInput').value = selectedName;
297
660
  this._bindingsRefresh();
298
661
  }
299
662
  }
663
+ _refresh() {
664
+ requestAnimationFrame(() => {
665
+ this._bindingsRefresh();
666
+ });
667
+ }
300
668
  async showHistoric() {
301
669
  let h = new BindingsEditorHistoric(this.historic);
302
670
  const abortController = new AbortController();
@@ -310,17 +678,13 @@ export class BindingsEditor extends BaseCustomWebComponentConstructorAppend {
310
678
  }
311
679
  this._bindingsRefresh();
312
680
  }
313
- addConverter() {
314
- this.converters.push({ key: '', value: '' });
315
- this._activeRow = this.converters.length - 1;
316
- this._bindingsRefresh();
317
- this._updatefocusedRow();
318
- }
319
- removeConverter() {
320
- this.converters.splice(this._activeRow, 1);
321
- this._activeRow = -1;
322
- this._bindingsRefresh();
323
- this._updatefocusedRow();
681
+ _showHelp() {
682
+ const iframe = document.createElement('iframe');
683
+ iframe.srcdoc = BindingsEditor.helpSections.length > 0
684
+ ? bindingsEditorHelpHtml.replace('</div>\n</body>', BindingsEditor.helpSections.join('') + '</div>\n</body>')
685
+ : bindingsEditorHelpHtml;
686
+ iframe.style.cssText = 'width:100%; height:100%; border:none; display:block;';
687
+ this._shell.openModal(iframe, { title: 'Bindings Editor Help', parent: this, x: -660, y: -80, width: 630, height: 580 });
324
688
  }
325
689
  }
326
690
  customElements.define(BindingsEditor.is, BindingsEditor);