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 +8 -1
- package/package.json +1 -1
- package/www/dist/frontend/config/ConfigureWebcomponentDesigner.js +2 -0
- package/www/dist/frontend/config/IobrokerWebui3DScreenPropertiesPanel.js +310 -115
- package/www/dist/frontend/config/IobrokerWebuiAppShell.js +2 -2
- package/www/dist/frontend/config/IobrokerWebuiDynamicPropsEditor.js +159 -0
- package/www/dist/frontend/runtime/CustomControls.js +49 -0
- package/www/dist/frontend/services/DynamicPropertiesHelper.js +75 -0
- package/www/dist/frontend/services/IobrokerWebuiLitPropertiesService.js +53 -0
- package/www/dist/frontend/services/IobrokerWebuiPropertiesService.js +7 -1
package/io-package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "mywebui",
|
|
4
|
-
"version": "1.42.
|
|
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
|
@@ -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
|
|
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
|
-
<!--
|
|
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
|
-
|
|
31
|
-
<div id="
|
|
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
|
-
<!--
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
<div
|
|
47
|
-
|
|
48
|
-
<
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
<
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
<
|
|
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="
|
|
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:
|
|
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:#
|
|
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 {
|
|
87
|
-
.brow { display:flex;align-items:center;padding:
|
|
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
|
-
|
|
90
|
-
.
|
|
91
|
-
.
|
|
92
|
-
.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
/*
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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:#
|
|
105
|
-
#bdTitle { font-weight:bold;color:#9cdcfe;
|
|
106
|
-
.
|
|
107
|
-
.
|
|
108
|
-
.
|
|
109
|
-
.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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(
|
|
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
|
|
261
|
-
const
|
|
333
|
+
const sq = row.querySelector('.bsq');
|
|
334
|
+
const val = row.querySelector('.bval');
|
|
262
335
|
const bind = uuid ? this._bindings[uuid]?.[key] : null;
|
|
263
|
-
|
|
264
|
-
if (
|
|
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');
|
|
412
|
+
ov?.classList.remove('open');
|
|
275
413
|
});
|
|
276
414
|
|
|
277
|
-
this._getDomElement('
|
|
278
|
-
const
|
|
279
|
-
if (
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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.
|
|
289
|
-
const signal
|
|
290
|
-
const formula
|
|
291
|
-
const
|
|
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();
|
|
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
|
-
|
|
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
|
-
|
|
309
|
-
if (!
|
|
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
|
-
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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="
|
|
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(), '
|
|
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 {
|