iobroker.mywebui 1.37.74 → 1.37.75

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.37.74",
4
+ "version": "1.37.75",
5
5
  "titleLang": {
6
6
  "en": "mywebui",
7
7
  "de": "mywebui",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.mywebui",
3
- "version": "1.37.74",
3
+ "version": "1.37.75",
4
4
  "description": "ioBroker mywebui - Custom edited mywebui by gokturk413",
5
5
  "type": "module",
6
6
  "main": "dist/backend/main.js",
@@ -2,7 +2,6 @@ import { iobrokerHandler } from '../common/IobrokerHandler.js';
2
2
 
3
3
  let _gsapLoadPromise = null;
4
4
 
5
- // Resolve paths relative to this module's location (works regardless of adapter URL prefix)
6
5
  // AnimationService.js is at dist/frontend/runtime/ → ../../vendor/gsap/ = dist/vendor/gsap/
7
6
  const _gsapBase = new URL('../../vendor/gsap/', import.meta.url).href;
8
7
 
@@ -105,7 +104,6 @@ function buildTweenConfig(cfg, value) {
105
104
  if (cfg.valueFrom != null) config.startAt = { y: parseFloat(cfg.valueFrom) };
106
105
  break;
107
106
  case 'translate': {
108
- // valueTo / valueFrom: "x,y" or single number (used for x, y=0)
109
107
  const parseXY = (v, defVal) => {
110
108
  if (v == null) return { x: defVal, y: 0 };
111
109
  const s = String(v);
@@ -174,6 +172,157 @@ function buildTweenConfig(cfg, value) {
174
172
  return config;
175
173
  }
176
174
 
175
+ // ─── Binding resolver ──────────────────────────────────────────────────────────
176
+ // Handles all webui binding prefix types so animations/effects work in custom
177
+ // controls just like they do in screens.
178
+
179
+ function _cleanupSubs(subs) {
180
+ for (const sub of subs) {
181
+ try {
182
+ if (sub.type === 'event') {
183
+ sub.target.removeEventListener(sub.event, sub.handler);
184
+ } else if (sub.type === 'state') {
185
+ iobrokerHandler.connection.unsubscribeState(sub.oid, sub.handler);
186
+ } else if (sub.type === 'delegate') {
187
+ _cleanupSubs(sub.innerSubs);
188
+ }
189
+ } catch (e) {}
190
+ }
191
+ subs.length = 0;
192
+ }
193
+
194
+ /**
195
+ * Resolve a template OID like "webui.0.test.{webui.0.test.select}" by subscribing
196
+ * to each {stateId} placeholder and re-resolving when any placeholder changes.
197
+ */
198
+ async function _resolveTemplateBinding(template, element, onValue, subs) {
199
+ const placeholders = [];
200
+ const regex = /\{([^}]+)\}/g;
201
+ let m;
202
+ while ((m = regex.exec(template)) !== null) placeholders.push(m[1]);
203
+ if (placeholders.length === 0) return;
204
+
205
+ const values = {};
206
+ for (const p of placeholders) values[p] = null;
207
+
208
+ const innerSubs = [];
209
+
210
+ const rebuildAndSub = async () => {
211
+ if (Object.values(values).some(v => v == null)) return;
212
+ let resolved = template;
213
+ for (const [p, v] of Object.entries(values)) resolved = resolved.replace('{' + p + '}', v);
214
+ _cleanupSubs(innerSubs);
215
+ await resolveAnimBinding(resolved, element, onValue, innerSubs);
216
+ };
217
+
218
+ for (const placeholder of placeholders) {
219
+ const h = async (id, state) => {
220
+ if (state?.val != null) { values[placeholder] = String(state.val); await rebuildAndSub(); }
221
+ };
222
+ try { iobrokerHandler.connection.subscribeState(placeholder, h); subs.push({ type: 'state', oid: placeholder, handler: h }); } catch (e) {}
223
+ try { const s = await iobrokerHandler.connection.getState(placeholder); if (s?.val != null) values[placeholder] = String(s.val); } catch (e) {}
224
+ }
225
+
226
+ await rebuildAndSub();
227
+ subs.push({ type: 'delegate', innerSubs });
228
+ }
229
+
230
+ /**
231
+ * Subscribe to a signal/binding reference, calling onValue(value) for current and future values.
232
+ * Supports: {template} ??prop ?prop state:id object:id local_* .relative plain OID
233
+ *
234
+ * @param {string} signal - binding signal string from *_bind.signal
235
+ * @param {Element} element - DOM element carrying the animation (for host lookup)
236
+ * @param {Function} onValue - called with the resolved scalar value
237
+ * @param {Array} subs - array for cleanup descriptors (push here, pass to _cleanupSubs later)
238
+ */
239
+ async function resolveAnimBinding(signal, element, onValue, subs) {
240
+ if (!signal) return;
241
+
242
+ // Template OID: "prefix.{stateId}.suffix" — resolve placeholders dynamically
243
+ if (signal.includes('{') && signal.includes('}')) {
244
+ await _resolveTemplateBinding(signal, element, onValue, subs);
245
+ return;
246
+ }
247
+
248
+ // Find nearest custom-control host element (for ? and ?? bindings)
249
+ const getRoot = () => {
250
+ const rn = element.getRootNode();
251
+ return rn && rn !== document ? (rn.host ?? null) : null;
252
+ };
253
+
254
+ if (signal.startsWith('??')) {
255
+ // Direct property read: root[propName], re-fires on propName-changed event
256
+ const propName = signal.slice(2);
257
+ const root = getRoot();
258
+ if (!root) return;
259
+ const read = () => {
260
+ const v = root[propName] ?? root.getAttribute?.(propName);
261
+ if (v != null) onValue(v);
262
+ };
263
+ read();
264
+ const h = () => read();
265
+ root.addEventListener(propName + '-changed', h);
266
+ subs.push({ type: 'event', target: root, event: propName + '-changed', handler: h });
267
+
268
+ } else if (signal.startsWith('?')) {
269
+ // Indirect: root[propName] holds the actual OID; re-resolve when property changes
270
+ const propName = signal.slice(1);
271
+ const root = getRoot();
272
+ if (!root) return;
273
+ const innerSubs = [];
274
+ const resolveAndSub = async () => {
275
+ _cleanupSubs(innerSubs);
276
+ const actualOid = root[propName] ?? root.getAttribute?.(propName);
277
+ if (actualOid) await resolveAnimBinding(actualOid, element, onValue, innerSubs);
278
+ };
279
+ await resolveAndSub();
280
+ const h = () => resolveAndSub();
281
+ root.addEventListener(propName + '-changed', h);
282
+ subs.push({ type: 'event', target: root, event: propName + '-changed', handler: h });
283
+ subs.push({ type: 'delegate', innerSubs });
284
+
285
+ } else if (signal.startsWith('state:')) {
286
+ const oid = signal.slice(6);
287
+ const h = (id, state) => { if (state?.val != null) onValue(state.val); };
288
+ try { iobrokerHandler.connection.subscribeState(oid, h); subs.push({ type: 'state', oid, handler: h }); } catch (e) {}
289
+ try { const s = await iobrokerHandler.connection.getState(oid); if (s?.val != null) onValue(s.val); } catch (e) {}
290
+
291
+ } else if (signal.startsWith('object:')) {
292
+ // object: bindings rarely used for animation scalar params — skip
293
+
294
+ } else {
295
+ // Relative path (starts with '.'): prepend host's relativeSignalsPath
296
+ let oid = signal;
297
+ if (oid.startsWith('.')) {
298
+ const root = getRoot();
299
+ const relPath = root?._getRelativeSignalsPath?.() ?? '';
300
+ oid = relPath + oid.slice(1);
301
+ }
302
+ // local_* handled by iobrokerHandler.subscribeState; plain OID falls through to connection
303
+ const h = (id, state) => { if (state?.val != null) onValue(state.val); };
304
+ try {
305
+ if (oid.startsWith('local_')) {
306
+ iobrokerHandler.subscribeState(oid, h);
307
+ } else {
308
+ iobrokerHandler.connection.subscribeState(oid, h);
309
+ }
310
+ subs.push({ type: 'state', oid, handler: h });
311
+ } catch (e) {}
312
+ try {
313
+ let s;
314
+ if (oid.startsWith('local_')) {
315
+ s = await iobrokerHandler.getState(oid);
316
+ } else {
317
+ s = await iobrokerHandler.connection.getState(oid);
318
+ }
319
+ if (s?.val != null) onValue(s.val);
320
+ } catch (e) {}
321
+ }
322
+ }
323
+
324
+ // ─── AnimationInstance ─────────────────────────────────────────────────────────
325
+
177
326
  class AnimationInstance {
178
327
  constructor(element, cfg) {
179
328
  this.element = element;
@@ -191,39 +340,25 @@ class AnimationInstance {
191
340
  const ctrl = controls[key];
192
341
  if (!ctrl) return;
193
342
 
194
- // condition_bind: dynamically update which condition operator to use
343
+ // condition_bind: dynamically update the condition operator
195
344
  if (ctrl.condition_bind?.signal) {
196
- try {
197
- const s = await iobrokerHandler.connection.getState(ctrl.condition_bind.signal);
198
- if (s?.val != null) ctrl.condition = String(s.val);
199
- } catch (e) {}
200
- const h = (id, state) => { if (state?.val != null) ctrl.condition = String(state.val); };
201
- try { iobrokerHandler.connection.subscribeState(ctrl.condition_bind.signal, h); this._subs.push({ oid: ctrl.condition_bind.signal, handler: h }); } catch (e) {}
345
+ await resolveAnimBinding(ctrl.condition_bind.signal, this.element,
346
+ (val) => { ctrl.condition = String(val); }, this._subs);
202
347
  }
203
348
 
204
349
  // value_bind: dynamically update the trigger value
205
350
  if (ctrl.value_bind?.signal) {
206
- try {
207
- const s = await iobrokerHandler.connection.getState(ctrl.value_bind.signal);
208
- if (s?.val != null) ctrl.value = String(s.val);
209
- } catch (e) {}
210
- const h = (id, state) => { if (state?.val != null) ctrl.value = String(state.val); };
211
- try { iobrokerHandler.connection.subscribeState(ctrl.value_bind.signal, h); this._subs.push({ oid: ctrl.value_bind.signal, handler: h }); } catch (e) {}
351
+ await resolveAnimBinding(ctrl.value_bind.signal, this.element,
352
+ (val) => { ctrl.value = String(val); }, this._subs);
212
353
  }
213
354
 
214
355
  // oid_bind.signal IS the OID to watch (binding square directly holds the target OID)
215
- const oid = ctrl.oid || ctrl.oid_bind?.signal;
216
- if (!oid) return;
217
- const handler = (id, state) => {
218
- if (checkCond(state?.val, ctrl.condition || 'equal', ctrl.value ?? 'true')) action();
219
- };
220
- try {
221
- iobrokerHandler.connection.subscribeState(oid, handler);
222
- this._subs.push({ oid, handler });
223
- } catch (e) {}
224
- iobrokerHandler.connection.getState(oid).then(state => {
225
- if (state && checkCond(state.val, ctrl.condition || 'equal', ctrl.value ?? 'true')) action();
226
- }).catch(() => {});
356
+ const oidSignal = ctrl.oid_bind?.signal || ctrl.oid;
357
+ if (!oidSignal) return;
358
+
359
+ await resolveAnimBinding(oidSignal, this.element, (val) => {
360
+ if (checkCond(val, ctrl.condition || 'equal', ctrl.value ?? 'true')) action();
361
+ }, this._subs);
227
362
  };
228
363
 
229
364
  await bindControl('play', () => this._play());
@@ -243,14 +378,13 @@ class AnimationInstance {
243
378
  const bindCfg = this.cfg[prop + '_bind'];
244
379
  if (!bindCfg?.signal) continue;
245
380
  const propCapture = prop;
246
- const handler = (id, state) => {
247
- if (state?.val == null) return;
248
- this.cfg[propCapture] = state.val;
381
+ await resolveAnimBinding(bindCfg.signal, this.element, (val) => {
382
+ this.cfg[propCapture] = val;
249
383
  if (!this.tween) return;
250
384
  // Duration: adjust timeScale without restarting (smooth for repeat:-1)
251
385
  if (propCapture === 'duration' && this.tween.vars) {
252
386
  const origDur = parseFloat(this.tween.vars.duration) || 1;
253
- const newDur = parseFloat(state.val) || 1;
387
+ const newDur = parseFloat(val) || 1;
254
388
  this.tween.timeScale(origDur / newDur);
255
389
  return;
256
390
  }
@@ -259,15 +393,7 @@ class AnimationInstance {
259
393
  _restartTimer = setTimeout(() => {
260
394
  if (this.tween && !this.tween.paused()) this._play();
261
395
  }, 60);
262
- };
263
- try {
264
- iobrokerHandler.connection.subscribeState(bindCfg.signal, handler);
265
- this._subs.push({ oid: bindCfg.signal, handler });
266
- } catch (e) {}
267
- try {
268
- const state = await iobrokerHandler.connection.getState(bindCfg.signal);
269
- if (state?.val != null) this.cfg[prop] = state.val;
270
- } catch (e) {}
396
+ }, this._subs);
271
397
  }
272
398
  }
273
399
 
@@ -316,13 +442,12 @@ class AnimationInstance {
316
442
 
317
443
  destroy() {
318
444
  if (this.tween) { this.tween.kill(); this.tween = null; }
319
- for (const sub of this._subs) {
320
- try { iobrokerHandler.connection.unsubscribeState(sub.oid, sub.handler); } catch (e) {}
321
- }
322
- this._subs = [];
445
+ _cleanupSubs(this._subs);
323
446
  }
324
447
  }
325
448
 
449
+ // ─── Public API ────────────────────────────────────────────────────────────────
450
+
326
451
  const _activeAnimations = new WeakMap();
327
452
  const _activeEffects = new WeakMap();
328
453
 
@@ -426,7 +551,8 @@ async function _applyEffect(el, cfg) {
426
551
  }
427
552
  };
428
553
 
429
- let _hoverFn = null, _clickFn = null, _oidHandler = null, _oidId = null;
554
+ let _hoverFn = null, _clickFn = null;
555
+ const effectSubs = [];
430
556
 
431
557
  if (cfg.trigger === 'load') {
432
558
  applyTween();
@@ -437,36 +563,28 @@ async function _applyEffect(el, cfg) {
437
563
  _clickFn = applyTween;
438
564
  el.addEventListener('click', _clickFn);
439
565
  } else if (cfg.trigger === 'oid') {
440
- // condition_bind and conditionValue_bind
566
+ // condition_bind: resolve condition operator dynamically
441
567
  if (cfg.condition_bind?.signal) {
442
- try { const s = await iobrokerHandler.connection.getState(cfg.condition_bind.signal); if (s?.val != null) cfg.condition = String(s.val); } catch (e) {}
443
- const h = (id, state) => { if (state?.val != null) cfg.condition = String(state.val); };
444
- try { iobrokerHandler.connection.subscribeState(cfg.condition_bind.signal, h); } catch (e) {}
568
+ await resolveAnimBinding(cfg.condition_bind.signal, el,
569
+ (val) => { cfg.condition = String(val); }, effectSubs);
445
570
  }
571
+ // conditionValue_bind: resolve comparison value dynamically
446
572
  if (cfg.conditionValue_bind?.signal) {
447
- try { const s = await iobrokerHandler.connection.getState(cfg.conditionValue_bind.signal); if (s?.val != null) cfg.conditionValue = String(s.val); } catch (e) {}
448
- const h = (id, state) => { if (state?.val != null) cfg.conditionValue = String(state.val); };
449
- try { iobrokerHandler.connection.subscribeState(cfg.conditionValue_bind.signal, h); } catch (e) {}
573
+ await resolveAnimBinding(cfg.conditionValue_bind.signal, el,
574
+ (val) => { cfg.conditionValue = String(val); }, effectSubs);
450
575
  }
451
- // oid_bind.signal IS the OID to watch (same as for animation controls)
452
- const oid = cfg.oid || cfg.oid_bind?.signal;
453
- if (oid) {
454
- _oidId = oid;
455
- _oidHandler = (id, state) => {
456
- if (checkCond(state?.val, cfg.condition || 'equal', cfg.conditionValue ?? 'true')) applyTween();
457
- };
458
- try { iobrokerHandler.connection.subscribeState(oid, _oidHandler); } catch (e) {}
459
- iobrokerHandler.connection.getState(oid).then(state => {
460
- if (state && checkCond(state.val, cfg.condition || 'equal', cfg.conditionValue ?? 'true')) applyTween();
461
- }).catch(() => {});
576
+ // oid_bind.signal IS the OID to watch
577
+ const oidSignal = cfg.oid_bind?.signal || cfg.oid;
578
+ if (oidSignal) {
579
+ await resolveAnimBinding(oidSignal, el, (val) => {
580
+ if (checkCond(val, cfg.condition || 'equal', cfg.conditionValue ?? 'true')) applyTween();
581
+ }, effectSubs);
462
582
  }
463
583
  }
464
584
 
465
585
  return () => {
466
586
  if (_hoverFn) el.removeEventListener('mouseenter', _hoverFn);
467
587
  if (_clickFn) el.removeEventListener('click', _clickFn);
468
- if (_oidHandler && _oidId) {
469
- try { iobrokerHandler.connection.unsubscribeState(_oidId, _oidHandler); } catch (e) {}
470
- }
588
+ _cleanupSubs(effectSubs);
471
589
  };
472
590
  }