iobroker.mywebui 1.37.73 → 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
package/package.json
CHANGED
|
@@ -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
|
|
343
|
+
// condition_bind: dynamically update the condition operator
|
|
195
344
|
if (ctrl.condition_bind?.signal) {
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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
|
|
216
|
-
if (!
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
566
|
+
// condition_bind: resolve condition operator dynamically
|
|
441
567
|
if (cfg.condition_bind?.signal) {
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
448
|
-
|
|
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
|
|
452
|
-
const
|
|
453
|
-
if (
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
469
|
-
try { iobrokerHandler.connection.unsubscribeState(_oidId, _oidHandler); } catch (e) {}
|
|
470
|
-
}
|
|
588
|
+
_cleanupSubs(effectSubs);
|
|
471
589
|
};
|
|
472
590
|
}
|
|
@@ -2,6 +2,7 @@ import { BaseCustomWebComponentConstructorAppend, css, cssFromString } from "@go
|
|
|
2
2
|
import { iobrokerHandler } from "../common/IobrokerHandler.js";
|
|
3
3
|
import { PropertiesHelper } from "@gokturk413/web-component-designer/dist/elements/services/propertiesService/services/PropertiesHelper.js";
|
|
4
4
|
import { visibilityService } from "./VisibilityService.js";
|
|
5
|
+
import { scanAndApplyAnimations, scanAndApplyEffects, cleanupAnimations, cleanupEffects } from "./AnimationService.js";
|
|
5
6
|
export const webuiCustomControlPrefix = 'webui-';
|
|
6
7
|
export const webuiCustomControlSymbol = Symbol('webuiCustomControlSymbol');
|
|
7
8
|
export class BaseCustomControl extends BaseCustomWebComponentConstructorAppend {
|
|
@@ -80,6 +81,9 @@ export class BaseCustomControl extends BaseCustomWebComponentConstructorAppend {
|
|
|
80
81
|
|
|
81
82
|
// Scan and apply visibility to all elements inside the custom control
|
|
82
83
|
await visibilityService.scanAndApply(this.shadowRoot);
|
|
84
|
+
// Scan and apply animations/effects to all elements inside the custom control
|
|
85
|
+
scanAndApplyAnimations(this.shadowRoot).catch(() => {});
|
|
86
|
+
scanAndApplyEffects(this.shadowRoot).catch(() => {});
|
|
83
87
|
}
|
|
84
88
|
disconnectedCallback() {
|
|
85
89
|
for (let e of this.#eventListeners) {
|
|
@@ -98,6 +102,9 @@ export class BaseCustomControl extends BaseCustomWebComponentConstructorAppend {
|
|
|
98
102
|
// Remove visibility control from all elements inside the custom control
|
|
99
103
|
const controlledElements = this.shadowRoot.querySelectorAll('[data-visibility-controlled="true"]');
|
|
100
104
|
controlledElements.forEach(el => visibilityService.removeVisibility(el));
|
|
105
|
+
// Cleanup animations and effects
|
|
106
|
+
cleanupAnimations(this.shadowRoot);
|
|
107
|
+
cleanupEffects(this.shadowRoot);
|
|
101
108
|
}
|
|
102
109
|
_assignEvent(event, callback) {
|
|
103
110
|
const arrayEl = [event, callback];
|