iobroker.mywebui 1.42.38 → 1.42.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/io-package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "mywebui",
4
- "version": "1.42.38",
4
+ "version": "1.42.40",
5
5
  "titleLang": {
6
6
  "en": "mywebui",
7
7
  "de": "mywebui",
@@ -29,6 +29,13 @@
29
29
  "zh-cn": "使用万维网传送器的高锰用户接口"
30
30
  },
31
31
  "news": {
32
+ "1.42.40": {
33
+ "en": "feature: per-instance dynamic properties for custom controls (add/edit name, type, default via properties tab) and extended Lit widget property support (Array/Object/converter types, webuiProperties metadata for signal/color/screen/enum editors)",
34
+ "az": "yenilik: custom control-lar üçün instance-əsaslı dinamik property-lər (properties tabından ad, tip, default əlavə/redaktə) və Lit widget-lər üçün genişləndirilmiş property dəstəyi (Array/Object/converter tipləri, signal/color/screen/enum editorları üçün webuiProperties metadata)",
35
+ "tr": "yenilik: custom control'ler için instance bazlı dinamik property'ler (properties sekmesinden ad, tip, varsayılan ekleme/düzenleme) ve Lit widget'lar için genişletilmiş property desteği (Array/Object/converter tipleri, signal/color/screen/enum editörleri için webuiProperties metadata)",
36
+ "ru": "новое: динамические свойства экземпляров для custom control (добавление/редактирование имени, типа, значения по умолчанию во вкладке properties) и расширенная поддержка свойств Lit-виджетов (типы Array/Object/converter, метаданные webuiProperties для редакторов signal/color/screen/enum)",
37
+ "de": "Neu: instanzbasierte dynamische Properties für Custom Controls (Name, Typ, Standardwert über den Properties-Tab hinzufügen/bearbeiten) und erweiterte Lit-Widget-Property-Unterstützung (Array/Object/Converter-Typen, webuiProperties-Metadaten für Signal/Color/Screen/Enum-Editoren)"
38
+ },
32
39
  "1.37.30": {
33
40
  "en": "cleanup: remove unused grafanaLangSync onMessage handler and helper functions from main.js",
34
41
  "az": "təmizlik: istifadəsiz grafanaLangSync onMessage handler və köməkçi funksiyalar main.js-dən silindi",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.mywebui",
3
- "version": "1.42.38",
3
+ "version": "1.42.40",
4
4
  "description": "ioBroker mywebui - Custom edited mywebui by gokturk413 with 3D Editor",
5
5
  "type": "module",
6
6
  "main": "dist/backend/main.js",
@@ -14,6 +14,7 @@ import { IobrokerWebuiConfigButtonProvider } from "../services/IobrokerWebuiConf
14
14
  import { IobrokerWebuiCustomElementContextMenu } from "../services/IobrokerWebuiCustomElementContextMenu.js";
15
15
  import { IobrokerWebuiRefactorService } from "../services/IobrokerWebuiRefactorService.js";
16
16
  import { IobrokerWebuiSpecialPropertiesService } from "../services/IobrokerWebuiSpecialPropertiesService.js";
17
+ import { IobrokerWebuiLitPropertiesService } from "../services/IobrokerWebuiLitPropertiesService.js";
17
18
  // import { IobrokerWebuiVisibilityPropertiesService } from "../services/IobrokerWebuiVisibilityPropertiesService.js";
18
19
  import { iobrokerHandler } from "../common/IobrokerHandler.js";
19
20
  import { ExpandCollapseContextMenu } from "@gokturk413/web-component-designer-widgets-wunderbaum";
@@ -49,6 +50,7 @@ export function configureDesigner(bindingsHelper) {
49
50
  serviceContainer.register('elementsService', new JsonFileElementsService('native', './node_modules/@gokturk413/web-component-designer/config/elements-native.json'));
50
51
  serviceContainer.register('propertyService', new IobrokerWebuiPropertiesService());
51
52
  serviceContainer.register('propertyService', new IobrokerWebuiSpecialPropertiesService());
53
+ serviceContainer.register('propertyService', new IobrokerWebuiLitPropertiesService());
52
54
  // Visibility is now handled directly in PropertyGrid
53
55
  // serviceContainer.register('propertyService', new IobrokerWebuiVisibilityPropertiesService());
54
56
  serviceContainer.designViewConfigButtons.push(new IobrokerWebuiConfigButtonProvider());
@@ -1,19 +1,12 @@
1
1
  import { BaseCustomWebComponentConstructorAppend, css, html } from "@gokturk413/base-custom-webcomponent";
2
- import { iobrokerHandler } from "../common/IobrokerHandler.js";
3
2
 
4
3
  const EDITOR_BASE = new URL('../../../3d-editor/', import.meta.url).href;
5
4
 
6
- /**
7
- * IobrokerWebui3DScreenPropertiesPanel
8
- * Injected into the webui's "Properties" (attributeDock) when a 3D screen editor is active.
9
- * Has sub-tabs: Scene | Object | Geom | Mater — no own bottom tab bar.
10
- * connect(editor, editorHost) must be called after Three.js editor is ready.
11
- */
12
5
  export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponentConstructorAppend {
13
6
 
14
7
  static template = html`
15
8
  <div id="root">
16
- <!-- Sub-tab bar: Scene / Object / Geom / Mater -->
9
+ <!-- Sub-tab bar -->
17
10
  <div id="subBar">
18
11
  <span class="stab active" data-sub="scene">Scene</span>
19
12
  <span class="stab" data-sub="object">Object</span>
@@ -21,45 +14,96 @@ export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponent
21
14
  <span class="stab" data-sub="mater">Mater</span>
22
15
  </div>
23
16
 
24
- <!-- Sub-tab panels -->
17
+ <!-- Scene tab -->
25
18
  <div id="subScene" class="spanel active"></div>
26
19
 
20
+ <!-- Object tab -->
27
21
  <div id="subObject" class="spanel" style="display:none;">
28
- <!-- Three.js SidebarObject embedded here -->
29
22
  <div id="objThreeWrap"></div>
30
- <!-- ioBroker bindings section -->
31
- <div id="bindSection">
32
- <div class="sechdr">Bindings (ioBroker states)</div>
33
- <div id="bindRows"></div>
34
- </div>
23
+ <div class="sechdr">BINDINGS (IOBROKER STATES)</div>
24
+ <div id="bindRows"></div>
35
25
  </div>
36
26
 
27
+ <!-- Geom tab -->
37
28
  <div id="subGeom" class="spanel" style="display:none;"></div>
29
+
30
+ <!-- Mater tab -->
38
31
  <div id="subMater" class="spanel" style="display:none;"></div>
39
32
 
40
- <!-- Binding dialog (inline overlay) -->
33
+ <!-- Context menu -->
34
+ <div id="ctxMenu">
35
+ <div class="cmItem" data-action="clear">clear</div>
36
+ <div class="cmItem" data-action="edittext">edit as text</div>
37
+ <div class="cmItem" data-action="editbind">edit binding</div>
38
+ </div>
39
+
40
+ <!-- Binding dialog -->
41
41
  <div id="bdOverlay">
42
42
  <div id="bdBox">
43
- <div id="bdTitle">Binding <span id="bdPropName"></span></div>
44
- <div class="bdr">
45
- <div class="bdl">ioBroker State ID</div>
46
- <div style="display:flex;gap:4px;">
47
- <input id="bdSig" type="text" placeholder="mywebui.0.data.value" autocomplete="off" />
48
- <button id="bdBrowse">...</button>
43
+ <div id="bdTitle">Edit Binding of '<span id="bdPropLabel"></span>' - 3DScreen</div>
44
+
45
+ <div class="bdRow">
46
+ <div class="bdLbl">objects</div>
47
+ <div style="display:flex;gap:3px;align-items:center;">
48
+ <input id="bdObj" type="text" class="bdInput" style="flex:1;" placeholder="state id" autocomplete="off"/>
49
+ <button class="bdBtn sm" id="bdObjX">X</button>
50
+ <button class="bdBtn sm" id="bdObjBrowse">...</button>
51
+ <button class="bdBtn sm accent" id="bdObjIOB">IOB</button>
49
52
  </div>
50
53
  </div>
51
- <div class="bdr">
52
- <div class="bdl">Formula <small>(use <b>val</b> for state value)</small></div>
53
- <input id="bdForm" type="text" placeholder="val" />
54
+
55
+ <div class="bdRow" style="display:flex;align-items:center;gap:8px;">
56
+ <span class="bdLbl" style="margin-bottom:0;">type :</span>
57
+ <select id="bdType" class="bdSel">
58
+ <option value="signal">signal</option>
59
+ <option value="ignore" selected>ignore</option>
60
+ <option value="css">css</option>
61
+ <option value="attribute">attribute</option>
62
+ </select>
54
63
  </div>
55
- <div class="bdr" style="display:flex;align-items:center;gap:8px;">
56
- <input id="bdTw" type="checkbox" />
57
- <span style="color:#ccc;font-size:11px;">Two-way (write back)</span>
64
+
65
+ <div class="bdRow" style="display:flex;align-items:center;gap:8px;">
66
+ <input id="bdTwoWay" type="checkbox"/>
67
+ <span class="bdChkLbl">two way binding</span>
68
+ </div>
69
+ <div class="bdRow" style="display:flex;align-items:center;gap:8px;">
70
+ <input id="bdInvert" type="checkbox"/>
71
+ <span class="bdChkLbl">invert logic</span>
72
+ </div>
73
+
74
+ <div class="bdRow" style="display:flex;align-items:center;justify-content:space-between;">
75
+ <span class="bdLbl" style="margin-bottom:0;">formula</span>
76
+ <button class="bdBtn sm" id="bdHistoric">historic</button>
77
+ </div>
78
+ <div class="bdRow">
79
+ <textarea id="bdFormula" class="bdTa" rows="2" placeholder="val"></textarea>
80
+ </div>
81
+
82
+ <div class="bdRow">
83
+ <span class="bdLbl">write back signal :</span>
84
+ <input id="bdWriteBack" type="text" class="bdInput" placeholder=""/>
85
+ </div>
86
+
87
+ <div class="bdRow">
88
+ <span class="bdLbl">formula write back (two way)</span>
89
+ <textarea id="bdFormulaWB" class="bdTa" rows="2" placeholder="val"></textarea>
90
+ </div>
91
+
92
+ <div class="bdRow">
93
+ <span class="bdLbl">converter:</span>
94
+ <table id="bdConvTable" class="bdTable">
95
+ <thead><tr><th>condition</th><th>value</th></tr></thead>
96
+ <tbody id="bdConvBody"></tbody>
97
+ </table>
98
+ <div style="display:flex;gap:4px;margin-top:4px;justify-content:flex-end;">
99
+ <button class="bdBtn" id="bdConvAdd">add</button>
100
+ <button class="bdBtn" id="bdConvRemove">remove</button>
101
+ </div>
58
102
  </div>
103
+
59
104
  <div id="bdFoot">
60
- <button id="bdClear">Clear</button>
61
- <button id="bdCancel">Cancel</button>
62
- <button id="bdOk" class="primary">OK</button>
105
+ <button class="bdBtn accent-ok" id="bdOk">Ok</button>
106
+ <button class="bdBtn" id="bdCancel">Cancel</button>
63
107
  </div>
64
108
  </div>
65
109
  </div>
@@ -72,52 +116,69 @@ export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponent
72
116
 
73
117
  /* Sub-tab bar */
74
118
  #subBar { display:flex;background:#2d2d30;border-bottom:1px solid #3c3c3c;flex-shrink:0; }
75
- .stab { padding:6px 12px;cursor:pointer;font-size:11px;color:#888;border-right:1px solid #3c3c3c;white-space:nowrap;user-select:none; }
119
+ .stab { padding:5px 10px;cursor:pointer;font-size:11px;color:#888;border-right:1px solid #3c3c3c;white-space:nowrap;user-select:none; }
76
120
  .stab:hover { color:#ddd;background:#3e3e42; }
77
121
  .stab.active { color:#fff;background:#1e1e1e;border-top:2px solid #4ec9b0; }
78
122
 
79
123
  /* Sub-tab panels */
80
124
  .spanel { flex:1;overflow:auto;min-height:0; }
125
+ #subScene,#subGeom,#subMater { overflow:auto; }
126
+ #subObject { display:flex;flex-direction:column; }
127
+ #objThreeWrap { overflow:auto; }
81
128
 
82
129
  /* Section header */
83
- .sechdr { background:#2d2d30;color:#9cdcfe;padding:5px 8px;font-size:10px;font-weight:bold;text-transform:uppercase;border-top:1px solid #3c3c3c;border-bottom:1px solid #3c3c3c;letter-spacing:.5px; }
130
+ .sechdr { background:#252526;color:#9cdcfe;padding:4px 8px;font-size:10px;font-weight:bold;text-transform:uppercase;border-top:1px solid #3c3c3c;border-bottom:1px solid #3c3c3c;letter-spacing:.5px;flex-shrink:0; }
84
131
 
85
- /* Binding rows */
86
- #bindRows { padding:2px 0; }
87
- .brow { display:flex;align-items:center;padding:3px 6px;gap:5px;border-bottom:1px solid #252526;min-height:22px; }
132
+ /* Binding rows — match 2D property grid style */
133
+ #bindRows { flex:1;overflow:auto; }
134
+ .brow { display:flex;align-items:center;padding:2px 4px;gap:0;border-bottom:1px solid #2a2a2a;min-height:20px; }
88
135
  .brow:hover { background:#252526; }
89
- .blbl { width:96px;color:#888;font-size:10px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
90
- .bsig { flex:1;color:#4ec9b0;font-size:10px;font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
91
- .bsig.none { color:#555; }
92
- .bedit { padding:1px 6px;background:#2d2d30;border:1px solid #555;color:#ccc;cursor:pointer;font-size:10px;flex-shrink:0; }
93
- .bedit:hover { border-color:#4ec9b0;color:#4ec9b0; }
94
-
95
- /* Three.js panel host — reset some Three.js CSS for dark bg */
96
- #objThreeWrap { overflow:auto; }
97
- #subScene { overflow:auto; }
98
- #subGeom { overflow:auto; }
99
- #subMater { overflow:auto; }
100
-
101
- /* Binding dialog */
102
- #bdOverlay { display:none;position:absolute;inset:0;background:rgba(0,0,0,0.78);z-index:300;align-items:center;justify-content:center; }
136
+ /* square button */
137
+ .bsq { width:14px;height:14px;min-width:14px;border:1px solid #555;background:#2d2d30;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:8px;color:#888;flex-shrink:0;margin-right:4px;padding:0; }
138
+ .bsq:hover { border-color:#4ec9b0; }
139
+ .bsq.bound { background:#0e639c;border-color:#1177bb;color:#fff; }
140
+ /* property name */
141
+ .blbl { width:90px;color:#9cdcfe;font-size:11px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
142
+ /* signal display */
143
+ .bval { flex:1;color:#4ec9b0;font-size:10px;font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:0 2px; }
144
+ .bval.none { color:#555; }
145
+
146
+ /* Context menu */
147
+ #ctxMenu { display:none;position:fixed;z-index:9999;background:#2d2d30;border:1px solid #555;min-width:120px;box-shadow:2px 2px 8px rgba(0,0,0,0.6); }
148
+ #ctxMenu.open { display:block; }
149
+ .cmItem { padding:5px 12px;cursor:pointer;font-size:11px;color:#ccc; }
150
+ .cmItem:hover { background:#3e3e42;color:#fff; }
151
+
152
+ /* Binding dialog overlay */
153
+ #bdOverlay { display:none;position:absolute;inset:0;background:rgba(0,0,0,0.75);z-index:500;align-items:center;justify-content:center; }
103
154
  #bdOverlay.open { display:flex; }
104
- #bdBox { background:#252526;border:1px solid #555;border-radius:4px;padding:14px;width:90%;max-width:310px;color:#ccc;font-size:12px; }
105
- #bdTitle { font-weight:bold;color:#9cdcfe;margin-bottom:10px;font-size:12px; }
106
- .bdr { margin-bottom:8px; }
107
- .bdl { color:#888;font-size:10px;margin-bottom:3px; }
108
- .bdr input[type="text"] { width:100%;background:#3c3c3c;border:1px solid #555;color:#ccc;padding:4px;font-size:11px;box-sizing:border-box; }
109
- .bdr input[type="text"]:focus { border-color:#4ec9b0;outline:none; }
110
- #bdBrowse { padding:3px 8px;background:#3c3c3c;border:1px solid #555;color:#ccc;cursor:pointer;font-size:11px; }
111
- #bdFoot { display:flex;justify-content:flex-end;gap:6px;margin-top:12px; }
112
- #bdFoot button { padding:4px 12px;border:1px solid #555;background:#3c3c3c;color:#ccc;cursor:pointer;font-size:11px; }
113
- #bdFoot button.primary { background:#0e639c;border-color:#1177bb;color:#fff; }
155
+ #bdBox { background:#2d2d30;border:1px solid #555;padding:0;width:340px;max-height:90%;overflow-y:auto;box-shadow:4px 4px 16px rgba(0,0,0,0.7); }
156
+ #bdTitle { background:#3e3e42;padding:7px 10px;font-size:12px;font-weight:bold;color:#9cdcfe;border-bottom:1px solid #555;margin-bottom:0; }
157
+ .bdRow { padding:4px 10px 2px; }
158
+ .bdLbl { color:#888;font-size:10px;margin-bottom:2px;display:block; }
159
+ .bdInput { width:100%;background:#3c3c3c;border:1px solid #555;color:#ccc;padding:3px 5px;font-size:11px;box-sizing:border-box; }
160
+ .bdInput:focus { border-color:#4ec9b0;outline:none; }
161
+ .bdSel { background:#3c3c3c;border:1px solid #555;color:#ccc;padding:2px 4px;font-size:11px; }
162
+ .bdTa { width:100%;background:#3c3c3c;border:1px solid #555;color:#ccc;padding:3px 5px;font-size:11px;box-sizing:border-box;resize:vertical;font-family:monospace; }
163
+ .bdTa:focus { border-color:#4ec9b0;outline:none; }
164
+ .bdChkLbl { color:#ccc;font-size:11px; }
165
+ .bdTable { width:100%;border-collapse:collapse;font-size:10px;color:#ccc; }
166
+ .bdTable th { background:#252526;padding:2px 6px;text-align:left;color:#888;border:1px solid #3c3c3c; }
167
+ .bdTable td { padding:2px 4px;border:1px solid #3c3c3c; }
168
+ .bdTable td input { width:100%;background:#3c3c3c;border:none;color:#ccc;padding:1px 3px;font-size:10px; }
169
+ .bdBtn { padding:3px 10px;background:#3c3c3c;border:1px solid #555;color:#ccc;cursor:pointer;font-size:11px; }
170
+ .bdBtn:hover { border-color:#999; }
171
+ .bdBtn.sm { padding:2px 6px;font-size:10px; }
172
+ .bdBtn.accent { background:#0e639c;border-color:#1177bb;color:#fff; }
173
+ .bdBtn.accent-ok { background:#0e639c;border-color:#1177bb;color:#fff; }
174
+ #bdFoot { display:flex;justify-content:flex-end;gap:6px;padding:8px 10px;border-top:1px solid #3c3c3c;margin-top:4px; }
114
175
  `;
115
176
 
116
177
  _editor = null;
117
178
  _editorHost = null;
118
179
  _bindings = {};
119
180
  _selectedObj = null;
120
- _bindTarget = null;
181
+ _ctxTarget = null; // { uuid, key }
121
182
 
122
183
  // ── Public API ─────────────────────────────────────────────────────────────
123
184
 
@@ -129,7 +190,8 @@ export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponent
129
190
  await this._injectThreeCSS();
130
191
  await this._mountThreePanels();
131
192
  this._setupSubTabs();
132
- this._setupBindingRows();
193
+ this._buildBindingRows();
194
+ this._setupContextMenu();
133
195
  this._setupBindingDialog();
134
196
 
135
197
  editor.signals.objectSelected.add(obj => {
@@ -161,7 +223,7 @@ export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponent
161
223
  sr.appendChild(l);
162
224
  }
163
225
 
164
- // ── Mount Three.js sidebar sub-panels ──────────────────────────────────────
226
+ // ── Mount Three.js sidebar panels ──────────────────────────────────────────
165
227
 
166
228
  async _mountThreePanels() {
167
229
  const base = EDITOR_BASE + 'js/';
@@ -200,12 +262,12 @@ export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponent
200
262
  btn.classList.add('active');
201
263
  Object.values(panels).forEach(p => { if (p) p.style.display = 'none'; });
202
264
  const p = panels[btn.dataset.sub];
203
- if (p) p.style.display = 'block';
265
+ if (p) p.style.display = btn.dataset.sub === 'object' ? 'flex' : 'block';
204
266
  });
205
267
  });
206
268
  }
207
269
 
208
- // ── Bindable properties list ────────────────────────────────────────────────
270
+ // ── Bindable properties ─────────────────────────────────────────────────────
209
271
 
210
272
  _BINDABLE = [
211
273
  { key: 'position.x', label: 'Position X' },
@@ -222,7 +284,7 @@ export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponent
222
284
  { key: 'material.color', label: 'Color (hex)' },
223
285
  ];
224
286
 
225
- _setupBindingRows() {
287
+ _buildBindingRows() {
226
288
  const cont = this._getDomElement('bindRows');
227
289
  if (!cont) return;
228
290
  cont.innerHTML = '';
@@ -231,22 +293,33 @@ export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponent
231
293
  row.className = 'brow';
232
294
  row.dataset.key = key;
233
295
 
296
+ // □ square button (left of label)
297
+ const sq = document.createElement('button');
298
+ sq.className = 'bsq';
299
+ sq.title = 'right-click to bind / clear';
300
+ sq.textContent = '■';
301
+ sq.addEventListener('contextmenu', e => {
302
+ e.preventDefault();
303
+ this._showCtxMenu(e, key);
304
+ });
305
+ sq.addEventListener('click', e => {
306
+ e.preventDefault();
307
+ this._showCtxMenu(e, key);
308
+ });
309
+
310
+ // property name
234
311
  const lbl = document.createElement('span');
235
312
  lbl.className = 'blbl';
236
313
  lbl.textContent = label;
237
314
 
238
- const sig = document.createElement('span');
239
- sig.className = 'bsig none';
240
- sig.textContent = '';
241
-
242
- const btn = document.createElement('button');
243
- btn.className = 'bedit';
244
- btn.textContent = '□ bind';
245
- btn.addEventListener('click', () => this._openBindDlg(key, label));
315
+ // current signal display
316
+ const val = document.createElement('span');
317
+ val.className = 'bval none';
318
+ val.textContent = '';
246
319
 
320
+ row.appendChild(sq);
247
321
  row.appendChild(lbl);
248
- row.appendChild(sig);
249
- row.appendChild(btn);
322
+ row.appendChild(val);
250
323
  cont.appendChild(row);
251
324
  });
252
325
  }
@@ -257,68 +330,190 @@ export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponent
257
330
  const uuid = this._selectedObj?.uuid;
258
331
  cont.querySelectorAll('.brow').forEach(row => {
259
332
  const key = row.dataset.key;
260
- const sig = row.querySelector('.bsig');
261
- const btn = row.querySelector('.bedit');
333
+ const sq = row.querySelector('.bsq');
334
+ const val = row.querySelector('.bval');
262
335
  const bind = uuid ? this._bindings[uuid]?.[key] : null;
263
- if (sig) { sig.textContent = bind?.signal || '—'; sig.className = 'bsig' + (bind?.signal ? '' : ' none'); }
264
- if (btn) { btn.textContent = bind?.signal ? '■ bound' : '□ bind'; btn.style.color = bind?.signal ? '#4ec9b0' : ''; }
336
+
337
+ if (val) {
338
+ val.textContent = bind?.signal || '';
339
+ val.className = 'bval' + (bind?.signal ? '' : ' none');
340
+ }
341
+ if (sq) {
342
+ sq.className = 'bsq' + (bind?.signal ? ' bound' : '');
343
+ }
344
+ });
345
+ }
346
+
347
+ // ── Context menu ────────────────────────────────────────────────────────────
348
+
349
+ _setupContextMenu() {
350
+ const menu = this._getDomElement('ctxMenu');
351
+ if (!menu) return;
352
+
353
+ menu.addEventListener('click', e => {
354
+ const item = e.target.closest('.cmItem');
355
+ if (!item) return;
356
+ const action = item.dataset.action;
357
+ menu.classList.remove('open');
358
+
359
+ if (action === 'clear') this._clearBinding(this._ctxTarget);
360
+ if (action === 'editbind') this._openBindDialog(this._ctxTarget);
361
+ if (action === 'edittext') this._openTextEditor(this._ctxTarget);
362
+ });
363
+
364
+ // close on outside click
365
+ document.addEventListener('click', e => {
366
+ if (!menu.contains(e.target)) menu.classList.remove('open');
265
367
  });
266
368
  }
267
369
 
370
+ _showCtxMenu(e, key) {
371
+ if (!this._selectedObj) {
372
+ alert('Select a 3D object first.');
373
+ return;
374
+ }
375
+ this._ctxTarget = { uuid: this._selectedObj.uuid, key };
376
+ const menu = this._getDomElement('ctxMenu');
377
+ if (!menu) return;
378
+ // position near click
379
+ menu.style.left = e.clientX + 'px';
380
+ menu.style.top = e.clientY + 'px';
381
+ menu.classList.add('open');
382
+ }
383
+
384
+ _clearBinding({ uuid, key } = {}) {
385
+ if (!uuid || !key) return;
386
+ delete this._bindings[uuid]?.[key];
387
+ if (this._bindings[uuid] && !Object.keys(this._bindings[uuid]).length)
388
+ delete this._bindings[uuid];
389
+ this._syncHost();
390
+ this._refreshBindRows();
391
+ }
392
+
393
+ _openTextEditor({ uuid, key } = {}) {
394
+ if (!uuid || !key) return;
395
+ const ex = this._bindings[uuid]?.[key];
396
+ const val = ex?.signal || '';
397
+ const nv = window.prompt(`State ID for "${key}":`, val);
398
+ if (nv === null) return;
399
+ if (!nv.trim()) { this._clearBinding({ uuid, key }); return; }
400
+ if (!this._bindings[uuid]) this._bindings[uuid] = {};
401
+ this._bindings[uuid][key] = { signal: nv.trim(), formula: ex?.formula || 'val', twoWay: ex?.twoWay || false };
402
+ this._syncHost();
403
+ this._refreshBindRows();
404
+ }
405
+
268
406
  // ── Binding dialog ──────────────────────────────────────────────────────────
269
407
 
270
408
  _setupBindingDialog() {
271
409
  const ov = this._getDomElement('bdOverlay');
272
410
 
273
411
  this._getDomElement('bdCancel')?.addEventListener('click', () => {
274
- ov?.classList.remove('open'); this._bindTarget = null;
412
+ ov?.classList.remove('open');
275
413
  });
276
414
 
277
- this._getDomElement('bdClear')?.addEventListener('click', () => {
278
- const { uuid, key } = this._bindTarget || {};
279
- if (uuid && key) {
280
- delete this._bindings[uuid]?.[key];
281
- if (this._bindings[uuid] && !Object.keys(this._bindings[uuid]).length) delete this._bindings[uuid];
282
- this._syncHost(); this._refreshBindRows();
283
- }
284
- ov?.classList.remove('open'); this._bindTarget = null;
415
+ this._getDomElement('bdObjX')?.addEventListener('click', () => {
416
+ const inp = this._getDomElement('bdObj');
417
+ if (inp) inp.value = '';
418
+ });
419
+
420
+ this._getDomElement('bdObjBrowse')?.addEventListener('click', async () => {
421
+ try {
422
+ const { openSelectIdDialog } = await import('@iobroker/webcomponent-selectid-dialog/dist/selectIdHelper.js');
423
+ const connection = (await import('../common/IobrokerHandler.js')).iobrokerHandler.connection;
424
+ const id = await openSelectIdDialog(connection, null, false);
425
+ if (id) { const inp = this._getDomElement('bdObj'); if (inp) inp.value = id; }
426
+ } catch (_) {}
427
+ });
428
+
429
+ this._getDomElement('bdObjIOB')?.addEventListener('click', async () => {
430
+ try {
431
+ const { openSelectIdDialog } = await import('@iobroker/webcomponent-selectid-dialog/dist/selectIdHelper.js');
432
+ const connection = (await import('../common/IobrokerHandler.js')).iobrokerHandler.connection;
433
+ const id = await openSelectIdDialog(connection, null, false);
434
+ if (id) { const inp = this._getDomElement('bdObj'); if (inp) inp.value = id; }
435
+ } catch (_) {}
436
+ });
437
+
438
+ this._getDomElement('bdHistoric')?.addEventListener('click', () => {
439
+ // historic signals - show simple list if available
440
+ });
441
+
442
+ this._getDomElement('bdConvAdd')?.addEventListener('click', () => {
443
+ const tbody = this._getDomElement('bdConvBody');
444
+ if (!tbody) return;
445
+ const tr = document.createElement('tr');
446
+ tr.innerHTML = '<td><input type="text" placeholder="condition"/></td><td><input type="text" placeholder="value"/></td>';
447
+ tbody.appendChild(tr);
448
+ });
449
+
450
+ this._getDomElement('bdConvRemove')?.addEventListener('click', () => {
451
+ const tbody = this._getDomElement('bdConvBody');
452
+ if (!tbody) return;
453
+ const last = tbody.querySelector('tr:last-child');
454
+ if (last) last.remove();
285
455
  });
286
456
 
287
457
  this._getDomElement('bdOk')?.addEventListener('click', () => {
288
- const { uuid, key } = this._bindTarget || {};
289
- const signal = this._getDomElement('bdSig')?.value?.trim();
290
- const formula = this._getDomElement('bdForm')?.value?.trim() || 'val';
291
- const twoWay = this._getDomElement('bdTw')?.checked ?? false;
458
+ const { uuid, key } = this._ctxTarget || {};
459
+ const signal = this._getDomElement('bdObj')?.value?.trim();
460
+ const formula = this._getDomElement('bdFormula')?.value?.trim() || 'val';
461
+ const formulaWB = this._getDomElement('bdFormulaWB')?.value?.trim() || '';
462
+ const writeBack = this._getDomElement('bdWriteBack')?.value?.trim() || '';
463
+ const twoWay = this._getDomElement('bdTwoWay')?.checked ?? false;
464
+ const invert = this._getDomElement('bdInvert')?.checked ?? false;
465
+ const type = this._getDomElement('bdType')?.value || 'signal';
466
+
467
+ // collect converter rows
468
+ const converter = [];
469
+ this._getDomElement('bdConvBody')?.querySelectorAll('tr').forEach(tr => {
470
+ const inputs = tr.querySelectorAll('input');
471
+ if (inputs[0]?.value || inputs[1]?.value)
472
+ converter.push({ condition: inputs[0]?.value || '', value: inputs[1]?.value || '' });
473
+ });
474
+
292
475
  if (uuid && key && signal) {
293
476
  if (!this._bindings[uuid]) this._bindings[uuid] = {};
294
- this._bindings[uuid][key] = { signal, formula, twoWay };
295
- this._syncHost(); this._refreshBindRows();
477
+ this._bindings[uuid][key] = { signal, formula, twoWay, invert, type, writeBack, formulaWB, converter };
478
+ this._syncHost();
479
+ this._refreshBindRows();
480
+ } else if (uuid && key && !signal) {
481
+ this._clearBinding({ uuid, key });
296
482
  }
297
- ov?.classList.remove('open'); this._bindTarget = null;
298
- });
299
483
 
300
- this._getDomElement('bdBrowse')?.addEventListener('click', async () => {
301
- try {
302
- const id = await iobrokerHandler.showSelectIdDialog?.();
303
- if (id) { const inp = this._getDomElement('bdSig'); if (inp) inp.value = id; }
304
- } catch (_) {}
484
+ ov?.classList.remove('open');
305
485
  });
306
486
  }
307
487
 
308
- _openBindDlg(key, label) {
309
- if (!this._selectedObj) { alert('Select a 3D object first.'); return; }
310
- const uuid = this._selectedObj.uuid;
311
- this._bindTarget = { uuid, key };
488
+ _openBindDialog({ uuid, key } = {}) {
489
+ if (!uuid || !key) return;
312
490
  const ex = this._bindings[uuid]?.[key];
313
- const sigEl = this._getDomElement('bdSig');
314
- const frmEl = this._getDomElement('bdForm');
315
- const twEl = this._getDomElement('bdTw');
316
- const nmEl = this._getDomElement('bdPropName');
317
- if (sigEl) sigEl.value = ex?.signal || '';
318
- if (frmEl) frmEl.value = ex?.formula || 'val';
319
- if (twEl) twEl.checked = ex?.twoWay || false;
320
- if (nmEl) nmEl.textContent = label + ' [' + (this._selectedObj.name || uuid.slice(0,8)) + ']';
321
- this._getDomElement('bdOverlay')?.classList.add('open');
491
+
492
+ const getEl = id => this._getDomElement(id);
493
+ const label = this._BINDABLE.find(b => b.key === key)?.label || key;
494
+
495
+ getEl('bdPropLabel').textContent = label;
496
+ getEl('bdObj').value = ex?.signal || '';
497
+ getEl('bdFormula').value = ex?.formula || 'val';
498
+ getEl('bdFormulaWB').value = ex?.formulaWB || '';
499
+ getEl('bdWriteBack').value = ex?.writeBack || '';
500
+ getEl('bdTwoWay').checked = ex?.twoWay || false;
501
+ getEl('bdInvert').checked = ex?.invert || false;
502
+ if (getEl('bdType')) getEl('bdType').value = ex?.type || 'signal';
503
+
504
+ // fill converter
505
+ const tbody = getEl('bdConvBody');
506
+ if (tbody) {
507
+ tbody.innerHTML = '';
508
+ (ex?.converter || []).forEach(row => {
509
+ const tr = document.createElement('tr');
510
+ tr.innerHTML = `<td><input type="text" value="${row.condition || ''}"/></td><td><input type="text" value="${row.value || ''}"/></td>`;
511
+ tbody.appendChild(tr);
512
+ });
513
+ }
514
+
515
+ this._ctxTarget = { uuid, key };
516
+ getEl('bdOverlay')?.classList.add('open');
322
517
  }
323
518
 
324
519
  _syncHost() {
@@ -108,7 +108,7 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
108
108
  <div id="visibilityDock" title="Visibility" style="overflow: auto; width: 100%;" dock-spawn-dock-to="attributeDock">
109
109
  </div>
110
110
 
111
- <div id="effectsDock" title="Effects / Project" style="overflow: auto; width: 100%;" dock-spawn-dock-to="attributeDock">
111
+ <div id="effectsDock" title="Projects" style="overflow: auto; width: 100%;" dock-spawn-dock-to="attributeDock">
112
112
  </div>
113
113
 
114
114
  <div id="animationsDock" title="Animations" style="overflow: auto; width: 100%;" dock-spawn-dock-to="attributeDock">
@@ -313,7 +313,7 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
313
313
 
314
314
  setTimeout(makeSetup('visibilityDock', () => this._setupVisibilityPanel(), 'Visibility'), 1000);
315
315
  setTimeout(makeSetup('animationsDock', () => this._setupAnimationsPanel(), 'Animations'), 1200);
316
- setTimeout(makeSetup('effectsDock', () => this._setupEffectsPanel(), 'Effects'), 1400);
316
+ setTimeout(makeSetup('effectsDock', () => this._setupEffectsPanel(), 'Projects'), 1400);
317
317
  }
318
318
 
319
319
  _setupVisibilityPanel() {
@@ -0,0 +1,159 @@
1
+ import { BasePropertyEditor } from "@gokturk413/web-component-designer";
2
+ import { readDynamicDefs, dynamicPropertyTypes } from "../services/DynamicPropertiesHelper.js";
3
+
4
+ const notAllowedChars = '!"§$%&/()=?`´-:.,;<>|\\\'#+*°^';
5
+
6
+ // property grid editor for the 'dynamic-property-defs' attribute.
7
+ // shows a button which opens a dialog to add/remove/edit dynamic properties (name, type, default)
8
+ export class IobrokerWebuiDynamicPropsEditor extends BasePropertyEditor {
9
+ constructor(property) {
10
+ super(property);
11
+ let btn = document.createElement('button');
12
+ btn.textContent = 'properties...';
13
+ btn.title = 'add/edit dynamic properties of this instance';
14
+ btn.style.width = '100%';
15
+ btn.onclick = () => this._openDialog();
16
+ this.element = btn;
17
+ }
18
+
19
+ refreshValue(valueType, value) { }
20
+
21
+ _openDialog() {
22
+ const designItem = this.designItems?.[0];
23
+ if (!designItem)
24
+ return;
25
+ const defs = readDynamicDefs(designItem.element).map(x => ({ ...x }));
26
+
27
+ const overlay = document.createElement('div');
28
+ overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:100000;display:flex;align-items:center;justify-content:center;';
29
+ const panel = document.createElement('div');
30
+ panel.style.cssText = 'background:#1e2d3d;color:#d0d8e0;border:1px solid #3e6db4;min-width:430px;max-width:600px;max-height:80vh;display:flex;flex-direction:column;font-size:12px;font-family:inherit;box-shadow:0 4px 24px rgba(0,0,0,0.6);';
31
+
32
+ const header = document.createElement('div');
33
+ header.textContent = 'dynamic properties';
34
+ header.style.cssText = 'padding:6px 10px;font-weight:bold;border-bottom:1px solid #3e6db4;color:#7c9cbf;';
35
+ panel.appendChild(header);
36
+
37
+ const headRow = document.createElement('div');
38
+ headRow.style.cssText = 'display:grid;grid-template-columns:1fr 90px 1fr 1fr 28px;gap:3px;padding:4px 10px 0 10px;color:#7c9cbf;';
39
+ for (const t of ['name', 'type', 'default', 'enum values', '']) {
40
+ const d = document.createElement('div');
41
+ d.textContent = t;
42
+ headRow.appendChild(d);
43
+ }
44
+ panel.appendChild(headRow);
45
+
46
+ const list = document.createElement('div');
47
+ list.style.cssText = 'display:flex;flex-direction:column;gap:3px;padding:4px 10px;overflow-y:auto;flex:1;';
48
+ panel.appendChild(list);
49
+
50
+ const renderRows = () => {
51
+ list.innerHTML = '';
52
+ defs.forEach((def, idx) => {
53
+ const row = document.createElement('div');
54
+ row.style.cssText = 'display:grid;grid-template-columns:1fr 90px 1fr 1fr 28px;gap:3px;';
55
+
56
+ const nameInput = document.createElement('input');
57
+ nameInput.value = def.name ?? '';
58
+ nameInput.placeholder = 'propertyName';
59
+ nameInput.oninput = () => def.name = nameInput.value;
60
+ row.appendChild(nameInput);
61
+
62
+ const typeSelect = document.createElement('select');
63
+ for (const t of dynamicPropertyTypes) {
64
+ const opt = document.createElement('option');
65
+ opt.value = t;
66
+ opt.textContent = t;
67
+ if (def.type === t)
68
+ opt.selected = true;
69
+ typeSelect.appendChild(opt);
70
+ }
71
+ typeSelect.onchange = () => {
72
+ def.type = typeSelect.value;
73
+ valuesInput.style.visibility = def.type === 'enum' ? 'visible' : 'hidden';
74
+ };
75
+ row.appendChild(typeSelect);
76
+
77
+ const defInput = document.createElement('input');
78
+ defInput.value = def.default ?? '';
79
+ defInput.placeholder = 'default';
80
+ defInput.oninput = () => def.default = defInput.value;
81
+ row.appendChild(defInput);
82
+
83
+ const valuesInput = document.createElement('input');
84
+ valuesInput.value = def.values ? JSON.stringify(def.values) : '';
85
+ valuesInput.placeholder = '["a","b"]';
86
+ valuesInput.style.visibility = def.type === 'enum' ? 'visible' : 'hidden';
87
+ valuesInput.oninput = () => {
88
+ try { def.values = JSON.parse(valuesInput.value); } catch { }
89
+ };
90
+ row.appendChild(valuesInput);
91
+
92
+ const delBtn = document.createElement('button');
93
+ delBtn.textContent = '✕';
94
+ delBtn.title = 'remove';
95
+ delBtn.onclick = () => {
96
+ defs.splice(idx, 1);
97
+ renderRows();
98
+ };
99
+ row.appendChild(delBtn);
100
+ list.appendChild(row);
101
+ });
102
+ };
103
+ renderRows();
104
+
105
+ const footer = document.createElement('div');
106
+ footer.style.cssText = 'display:flex;gap:4px;padding:8px 10px;border-top:1px solid #3e6db4;';
107
+ const addBtn = document.createElement('button');
108
+ addBtn.textContent = 'add...';
109
+ addBtn.onclick = () => {
110
+ defs.push({ name: '', type: 'string' });
111
+ renderRows();
112
+ };
113
+ const spacer = document.createElement('div');
114
+ spacer.style.flex = '1';
115
+ const okBtn = document.createElement('button');
116
+ okBtn.textContent = 'ok';
117
+ okBtn.style.minWidth = '60px';
118
+ okBtn.onclick = async () => {
119
+ const cleaned = [];
120
+ const usedNames = new Set();
121
+ for (const def of defs) {
122
+ let name = def.name ?? '';
123
+ for (const c of notAllowedChars)
124
+ name = name.replaceAll(c, '');
125
+ name = name.replaceAll(' ', '');
126
+ if (!name)
127
+ continue;
128
+ name = name[0].toLowerCase() + name.substring(1);
129
+ if (usedNames.has(name))
130
+ continue;
131
+ usedNames.add(name);
132
+ const entry = { name, type: def.type ?? 'string' };
133
+ if (def.default != null && def.default !== '')
134
+ entry.default = def.default;
135
+ if (def.type === 'enum' && def.values)
136
+ entry.values = def.values;
137
+ cleaned.push(entry);
138
+ }
139
+ overlay.remove();
140
+ await this._valueChanged(cleaned.length ? JSON.stringify(cleaned) : null);
141
+ };
142
+ const cancelBtn = document.createElement('button');
143
+ cancelBtn.textContent = 'cancel';
144
+ cancelBtn.style.minWidth = '60px';
145
+ cancelBtn.onclick = () => overlay.remove();
146
+ footer.appendChild(addBtn);
147
+ footer.appendChild(spacer);
148
+ footer.appendChild(okBtn);
149
+ footer.appendChild(cancelBtn);
150
+ panel.appendChild(footer);
151
+
152
+ overlay.appendChild(panel);
153
+ overlay.onclick = e => {
154
+ if (e.target === overlay)
155
+ overlay.remove();
156
+ };
157
+ document.body.appendChild(overlay);
158
+ }
159
+ }
@@ -35,6 +35,7 @@ export class BaseCustomControl extends BaseCustomWebComponentConstructorAppend {
35
35
  }
36
36
  async connectedCallback() {
37
37
  this._parseAttributesToProperties();
38
+ this._setupDynamicProperties();
38
39
  this._bindingsRefresh();
39
40
  this.#specialValueHandler = {
40
41
  valueProvider: (specialValueName) => {
@@ -106,6 +107,54 @@ export class BaseCustomControl extends BaseCustomWebComponentConstructorAppend {
106
107
  cleanupAnimations(this.shadowRoot);
107
108
  cleanupEffects(this.shadowRoot);
108
109
  }
110
+ // defines accessors for the per-instance dynamic properties declared in the
111
+ // 'dynamic-property-defs' attribute: [ { "name": "kamran", "type": "string", "default": "x" }, ... ]
112
+ // so they behave like normal control properties (bindings refresh + changed events)
113
+ _setupDynamicProperties() {
114
+ let defs;
115
+ try {
116
+ const v = this.getAttribute('dynamic-property-defs');
117
+ if (!v)
118
+ return;
119
+ defs = JSON.parse(v);
120
+ }
121
+ catch (e) {
122
+ console.warn('invalid dynamic-property-defs attribute', e);
123
+ return;
124
+ }
125
+ if (!Array.isArray(defs))
126
+ return;
127
+ for (const def of defs) {
128
+ const name = def?.name;
129
+ if (!name)
130
+ continue;
131
+ if (!Object.getOwnPropertyDescriptor(this, name)) {
132
+ Object.defineProperty(this, name, {
133
+ get() {
134
+ return this['_' + name];
135
+ },
136
+ set(newValue) {
137
+ if (this['_' + name] !== newValue && (!Number.isNaN(this['_' + name]) || !Number.isNaN(newValue))) {
138
+ this['_' + name] = newValue;
139
+ this._bindingsRefresh(name);
140
+ this.dispatchEvent(new CustomEvent(PropertiesHelper.camelToDashCase(name) + '-changed', { detail: { newValue } }));
141
+ }
142
+ },
143
+ enumerable: true,
144
+ configurable: true,
145
+ });
146
+ }
147
+ const attr = this.getAttribute(PropertiesHelper.camelToDashCase(name));
148
+ let value = attr ?? def.default;
149
+ if (value != null) {
150
+ if (def.type === 'number')
151
+ value = parseFloat(value);
152
+ else if (def.type === 'boolean')
153
+ value = value === '' || value === true || value === 'true';
154
+ this['_' + name] = value;
155
+ }
156
+ }
157
+ }
109
158
  _assignEvent(event, callback) {
110
159
  const arrayEl = [event, callback];
111
160
  this.#eventListeners.push(arrayEl);
@@ -0,0 +1,75 @@
1
+ import { PropertyType } from "@gokturk413/web-component-designer";
2
+ import { iobrokerHandler } from "../common/IobrokerHandler.js";
3
+ import { IobrokerWebuiSignalPropertyEditor } from "../config/IobrokerWebuiSignalPropertyEditor.js";
4
+ import { IobrokerWebuiDynamicPropsEditor } from "../config/IobrokerWebuiDynamicPropsEditor.js";
5
+
6
+ // attribute on the element instance holding the per-instance dynamic property definitions:
7
+ // [ { "name": "kamran", "type": "string", "default": "abc", "values": [...] }, ... ]
8
+ export const dynamicDefsAttributeName = 'dynamic-property-defs';
9
+
10
+ export const dynamicPropertyTypes = ['string', 'number', 'boolean', 'color', 'date', 'signal', 'screen', 'enum'];
11
+
12
+ export function readDynamicDefs(element) {
13
+ try {
14
+ const v = element.getAttribute(dynamicDefsAttributeName);
15
+ if (v) {
16
+ const parsed = JSON.parse(v);
17
+ if (Array.isArray(parsed))
18
+ return parsed.filter(x => x && x.name);
19
+ }
20
+ }
21
+ catch (e) {
22
+ console.warn('invalid ' + dynamicDefsAttributeName + ' attribute', e);
23
+ }
24
+ return [];
25
+ }
26
+
27
+ export function convertDynamicDefault(def) {
28
+ if (def.default == null || def.default === '')
29
+ return undefined;
30
+ if (def.type === 'number') {
31
+ const n = parseFloat(def.default);
32
+ return Number.isNaN(n) ? undefined : n;
33
+ }
34
+ if (def.type === 'boolean')
35
+ return def.default === true || def.default === 'true';
36
+ return def.default;
37
+ }
38
+
39
+ export async function createWebuiTypedProperty(name, prp, service) {
40
+ if (prp.type === 'color')
41
+ return { name, type: "color", service, propertyType: PropertyType.propertyAndAttribute };
42
+ if (prp.type === 'number')
43
+ return { name, type: "number", service, propertyType: PropertyType.propertyAndAttribute };
44
+ if (prp.type === 'boolean')
45
+ return { name, type: "boolean", service, propertyType: PropertyType.propertyAndAttribute };
46
+ if (prp.type === 'date')
47
+ return { name, type: "date", service, propertyType: PropertyType.propertyAndAttribute };
48
+ if (prp.type === 'enum')
49
+ return { name, type: "list", values: prp.values, service, propertyType: PropertyType.propertyAndAttribute };
50
+ if (prp.type === 'signal')
51
+ return { name, type: "signal", service, propertyType: PropertyType.propertyAndAttribute, createEditor: p => new IobrokerWebuiSignalPropertyEditor(p, window.appShell) };
52
+ if (prp.type === 'screen') {
53
+ const screens = await iobrokerHandler.getAllNames('screen');
54
+ return { name, type: "list", values: screens, service, propertyType: PropertyType.propertyAndAttribute };
55
+ }
56
+ return { name, type: "string", service, propertyType: PropertyType.propertyAndAttribute };
57
+ }
58
+
59
+ // builds the property grid entries for the dynamic properties of a design item:
60
+ // one group 'dynamic' containing the defined properties + the defs editor button
61
+ export async function buildDynamicPropertiesGroup(designItem, service) {
62
+ const defs = readDynamicDefs(designItem.element);
63
+ const properties = [];
64
+ for (const d of defs)
65
+ properties.push(await createWebuiTypedProperty(d.name, d, service));
66
+ properties.push({
67
+ name: 'dynamicPropertyDefs',
68
+ type: 'string',
69
+ attributeName: dynamicDefsAttributeName,
70
+ service,
71
+ propertyType: PropertyType.attribute,
72
+ createEditor: p => new IobrokerWebuiDynamicPropsEditor(p)
73
+ });
74
+ return { name: 'dynamic', description: 'instance dynamic properties', properties };
75
+ }
@@ -0,0 +1,53 @@
1
+ import { Lit2PropertiesService, PropertiesHelper, PropertyType, RefreshMode } from "@gokturk413/web-component-designer";
2
+ import { buildDynamicPropertiesGroup, createWebuiTypedProperty } from "./DynamicPropertiesHelper.js";
3
+
4
+ // extended properties service for Lit (2/3) widgets:
5
+ // - supports Array & custom converter properties (edited as string/JSON) which Lit2PropertiesService skips
6
+ // - supports webui specific types via 'static webuiProperties' metadata on the widget class
7
+ // (e.g. static webuiProperties = { mySignal: { type: 'signal' }, myColor: { type: 'color' } })
8
+ // - supports per-instance dynamic properties via the 'dynamic-property-defs' attribute
9
+ export class IobrokerWebuiLitPropertiesService extends Lit2PropertiesService {
10
+ name = "webuiLit";
11
+
12
+ getRefreshMode(designItem) {
13
+ return RefreshMode.fullOnValueChange;
14
+ }
15
+
16
+ async getProperties(designItem) {
17
+ if (!this.isHandledElement(designItem))
18
+ return null;
19
+ const ctor = designItem.element.constructor;
20
+ const meta = ctor.webuiProperties ?? {};
21
+ const properties = [];
22
+ const handled = new Set();
23
+ for (const [name, litProperty] of ctor.elementProperties.entries()) {
24
+ handled.add(name);
25
+ const m = meta[name];
26
+ if (m) {
27
+ properties.push(await createWebuiTypedProperty(name, m, this));
28
+ continue;
29
+ }
30
+ let type = litProperty?.type ?? litProperty;
31
+ if (type === String)
32
+ properties.push({ name, type: "string", service: this, propertyType: PropertyType.propertyAndAttribute });
33
+ else if (type === Number)
34
+ properties.push({ name, type: "number", service: this, propertyType: PropertyType.propertyAndAttribute });
35
+ else if (type === Boolean)
36
+ properties.push({ name, type: "boolean", service: this, propertyType: PropertyType.propertyAndAttribute });
37
+ else if (type === Date)
38
+ properties.push({ name, type: "date", service: this, propertyType: PropertyType.propertyAndAttribute });
39
+ else if (PropertiesHelper.isTypescriptEnum(type))
40
+ properties.push({ name, type: "enum", enumValues: PropertiesHelper.getTypescriptEnumEntries(type), service: this, propertyType: PropertyType.propertyAndAttribute });
41
+ else
42
+ // Array, Object and custom converters are edited as string (JSON)
43
+ properties.push({ name, type: "string", service: this, propertyType: PropertyType.propertyAndAttribute });
44
+ }
45
+ // webuiProperties entries which are no lit reactive properties
46
+ for (const name in meta) {
47
+ if (!handled.has(name))
48
+ properties.push(await createWebuiTypedProperty(name, meta[name], this));
49
+ }
50
+ properties.push(await buildDynamicPropertiesGroup(designItem, this));
51
+ return properties;
52
+ }
53
+ }
@@ -1,13 +1,18 @@
1
- import { BaseCustomWebComponentPropertiesService, PropertyType } from "@gokturk413/web-component-designer";
1
+ import { BaseCustomWebComponentPropertiesService, PropertyType, RefreshMode } from "@gokturk413/web-component-designer";
2
2
  import { BaseCustomControl, webuiCustomControlSymbol } from "../runtime/CustomControls.js";
3
3
  import { iobrokerHandler } from "../common/IobrokerHandler.js";
4
4
  import { ScreenViewer } from "../runtime/ScreenViewer.js";
5
5
  import { IobrokerWebuiSignalPropertyEditor } from "../config/IobrokerWebuiSignalPropertyEditor.js";
6
+ import { buildDynamicPropertiesGroup } from "./DynamicPropertiesHelper.js";
6
7
  export class IobrokerWebuiPropertiesService extends BaseCustomWebComponentPropertiesService {
7
8
  isHandledElement(designItem) {
8
9
  return designItem.element instanceof BaseCustomControl || designItem.element instanceof ScreenViewer;
9
10
  ;
10
11
  }
12
+ getRefreshMode(designItem) {
13
+ // dynamic properties can change with every value change (dynamic-property-defs attribute)
14
+ return RefreshMode.fullOnValueChange;
15
+ }
11
16
  async getProperties(designItem) {
12
17
  if (!this.isHandledElement(designItem))
13
18
  return null;
@@ -67,6 +72,7 @@ export class IobrokerWebuiPropertiesService extends BaseCustomWebComponentProper
67
72
  for (const [groupName, groupProps] of groupMap) {
68
73
  result.push({ name: groupName, description: '', properties: groupProps });
69
74
  }
75
+ result.push(await buildDynamicPropertiesGroup(designItem, this));
70
76
  return result;
71
77
  }
72
78
  else {