iobroker.mywebui 1.42.39 → 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.39",
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());
@@ -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 {