iobroker.mywebui 1.37.63 → 1.37.65

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.60",
4
+ "version": "1.37.65",
5
5
  "titleLang": {
6
6
  "en": "mywebui",
7
7
  "de": "mywebui",
@@ -337,7 +337,7 @@
337
337
  "color": "#c8ffe1",
338
338
  "order": 1
339
339
  },
340
- "installedFrom": "iobroker.mywebui@1.37.16"
340
+ "installedFrom": "iobroker.mywebui"
341
341
  },
342
342
  "native": {
343
343
  "licenseKey": "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.mywebui",
3
- "version": "1.37.63",
3
+ "version": "1.37.65",
4
4
  "description": "ioBroker mywebui - Custom edited mywebui by gokturk413",
5
5
  "type": "module",
6
6
  "main": "dist/backend/main.js",
@@ -651,6 +651,73 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
651
651
  content.appendChild(actionDiv);
652
652
  }
653
653
 
654
+ // ─── Shared binding square helper ────────────────────────────────────────
655
+
656
+ _makeBindSquare(propKey, cfg, designItem, attrName, saveAndRefresh) {
657
+ const bindCfg = cfg[propKey + '_bind'] || null;
658
+ const signal = bindCfg?.signal || null;
659
+
660
+ const btn = document.createElement('div');
661
+ btn.title = signal ? `Bound: ${signal}\n(right-click: edit/clear)` : `Click to bind to ioBroker state`;
662
+ btn.style.cssText = 'width:11px;height:11px;min-width:11px;border:1px solid #596c7a;cursor:pointer;flex-shrink:0;box-sizing:border-box;'
663
+ + (signal ? 'background:orange;' : 'background:transparent;');
664
+
665
+ const openEditor = () => {
666
+ const existingBinding = signal ? { bindableObjectNames: [signal], expression: bindCfg.expression || '', target: 'property' } : null;
667
+ const dynEdt = new IobrokerWebuiBindingsEditor(
668
+ { name: propKey, propertyType: 'propertyAndAttribute' },
669
+ existingBinding, 'property', serviceContainer,
670
+ designItem.instanceServiceContainer, this
671
+ );
672
+ const cw = new IobrokerWebuiConfirmationWrapper();
673
+ cw.title = `Bind "${propKey}"`;
674
+ cw.appendChild(dynEdt);
675
+ const dlg = this.openDialog(cw, { x: 200, y: 200, width: 700, height: 460 });
676
+ cw.cancelClicked.on(() => dlg.close());
677
+ cw.okClicked.on(() => {
678
+ dlg.close();
679
+ const newSignal = dynEdt.objectNames;
680
+ if (newSignal) {
681
+ const bnd = { signal: newSignal };
682
+ if (dynEdt.expression) bnd.expression = dynEdt.expression;
683
+ cfg[propKey + '_bind'] = bnd;
684
+ } else {
685
+ delete cfg[propKey + '_bind'];
686
+ }
687
+ saveAndRefresh();
688
+ });
689
+ };
690
+
691
+ btn.onclick = openEditor;
692
+
693
+ btn.oncontextmenu = (e) => {
694
+ e.preventDefault();
695
+ const existing = document.getElementById('__animbind-ctx');
696
+ if (existing) existing.remove();
697
+ const menu = document.createElement('div');
698
+ menu.id = '__animbind-ctx';
699
+ menu.style.cssText = `position:fixed;left:${e.clientX}px;top:${e.clientY}px;background:#2d2d2d;color:#ddd;font-size:12px;border:1px solid #555;border-radius:3px;z-index:99999;min-width:120px;box-shadow:2px 2px 6px rgba(0,0,0,0.5);`;
700
+ const addItem = (text, cb, disabled = false) => {
701
+ const item = document.createElement('div');
702
+ item.textContent = text;
703
+ item.style.cssText = `padding:6px 12px;cursor:${disabled ? 'default' : 'pointer'};color:${disabled ? '#666' : '#ddd'};`;
704
+ if (!disabled) {
705
+ item.onmouseenter = () => item.style.background = '#3e6db4';
706
+ item.onmouseleave = () => item.style.background = '';
707
+ item.onclick = () => { menu.remove(); cb(); };
708
+ }
709
+ menu.appendChild(item);
710
+ };
711
+ addItem('edit binding', openEditor);
712
+ addItem('clear binding', () => { delete cfg[propKey + '_bind']; saveAndRefresh(); }, !signal);
713
+ document.body.appendChild(menu);
714
+ const close = (ev) => { if (!menu.contains(ev.target)) { menu.remove(); document.removeEventListener('mousedown', close); } };
715
+ setTimeout(() => document.addEventListener('mousedown', close), 0);
716
+ };
717
+
718
+ return btn;
719
+ }
720
+
654
721
  // ─── Animations Panel ────────────────────────────────────────────────────
655
722
 
656
723
  _setupAnimationsPanel() {
@@ -697,33 +764,78 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
697
764
  try { cfg = JSON.parse(element.getAttribute('data-animation') || '{}'); } catch (e) {}
698
765
  const controls = cfg.controls || {};
699
766
 
767
+ const collectAnimCfg = () => {
768
+ const e = effectSel.value;
769
+ const c = {};
770
+ const addCtrl = (key, row) => { const v = row._getCtrl(); if (v.oid || v.oid_bind) c[key] = v; };
771
+ addCtrl('play', playCtrl); addCtrl('pause', pauseCtrl);
772
+ addCtrl('resume', resumeCtrl); addCtrl('stop', stopCtrl);
773
+ addCtrl('reverse', reverseCtrl);
774
+ const out = {};
775
+ if (e) out.effect = e;
776
+ if (e === 'svg' && svgAttrSel.value) out.svgAttr = svgAttrSel.value;
777
+ if (valueFromInp.value) out.valueFrom = valueFromInp.value;
778
+ if (valueToInp.value) out.valueTo = valueToInp.value;
779
+ out.duration = parseFloat(durationInp.value) || 1;
780
+ out.ease = easeSel.value || 'power1.inOut';
781
+ out.repeat = parseInt(repeatInp.value) || 0;
782
+ if (yoyoChk.querySelector('input').checked) out.yoyo = true;
783
+ if (['rotation','scale'].includes(e)) {
784
+ out.transformOriginX = originXInp.value;
785
+ out.transformOriginY = originYInp.value;
786
+ }
787
+ if (e === 'fill' || e === 'svg') {
788
+ if (fillFromInp.value) out.fillColorFrom = fillFromInp.value;
789
+ if (fillToInp.value) out.fillColorTo = fillToInp.value;
790
+ }
791
+ if (e === 'motionPath') {
792
+ if (pathIdInp.value) out.pathId = pathIdInp.value;
793
+ if (alignToPathChk.querySelector('input').checked) out.alignToPath = true;
794
+ if (orientToPathChk.querySelector('input').checked) out.orientToPath = true;
795
+ }
796
+ if (Object.keys(c).length) out.controls = c;
797
+ // Preserve all _bind properties from cfg
798
+ for (const [k, v] of Object.entries(cfg)) {
799
+ if (k.endsWith('_bind') && v) out[k] = v;
800
+ }
801
+ return out;
802
+ };
803
+
700
804
  const save = () => {
701
805
  const newCfg = collectAnimCfg();
702
- if (Object.keys(newCfg).length === 0 || (!newCfg.effect && !newCfg.controls)) {
806
+ if (!newCfg.effect && !newCfg.controls && !Object.keys(newCfg).some(k => k.endsWith('_bind'))) {
703
807
  designItem.removeAttribute('data-animation');
704
808
  } else {
705
809
  designItem.setAttribute('data-animation', JSON.stringify(newCfg));
706
810
  }
707
811
  };
708
812
 
709
- const field = (label, inputEl) => {
813
+ const saveAndRefresh = () => { save(); this._updateAnimationsPanel(); };
814
+
815
+ // field(label, propKey, inputEl) — propKey=null → no binding square
816
+ const field = (label, propKey, inputEl) => {
710
817
  const row = document.createElement('div');
711
- row.style.cssText = 'display:flex;align-items:center;gap:6px;margin-bottom:6px;';
818
+ row.style.cssText = 'display:flex;align-items:center;gap:4px;margin-bottom:6px;';
819
+ if (propKey) {
820
+ row.appendChild(this._makeBindSquare(propKey, cfg, designItem, 'data-animation', saveAndRefresh));
821
+ } else {
822
+ const sp = document.createElement('div');
823
+ sp.style.cssText = 'width:11px;min-width:11px;flex-shrink:0;';
824
+ row.appendChild(sp);
825
+ }
712
826
  const lbl = document.createElement('span');
713
827
  lbl.textContent = label;
714
- lbl.style.cssText = 'min-width:90px;font-size:11px;color:#555;';
828
+ lbl.style.cssText = 'min-width:84px;font-size:11px;color:#555;';
715
829
  row.appendChild(lbl);
716
830
  row.appendChild(inputEl);
717
831
  return row;
718
832
  };
719
833
 
720
- const inp = (val, type = 'text', width = '100%') => {
834
+ const inp = (val, type = 'text') => {
721
835
  const i = document.createElement('input');
722
- i.type = type;
723
- i.value = val ?? '';
724
- i.style.cssText = `flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;width:${width};`;
725
- i.onchange = save;
726
- return i;
836
+ i.type = type; i.value = val ?? '';
837
+ i.style.cssText = 'flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;';
838
+ i.onchange = save; return i;
727
839
  };
728
840
 
729
841
  const sel = (options, val) => {
@@ -734,37 +846,39 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
734
846
  o.value = v; o.textContent = t; o.selected = v === val;
735
847
  s.appendChild(o);
736
848
  });
737
- s.onchange = save;
738
- return s;
849
+ s.onchange = save; return s;
739
850
  };
740
851
 
741
- const chk = (val, label) => {
742
- const wrap = document.createElement('label');
743
- wrap.style.cssText = 'display:flex;align-items:center;gap:4px;font-size:11px;cursor:pointer;';
852
+ const chkField = (propKey, label, val) => {
853
+ const row = document.createElement('div');
854
+ row.style.cssText = 'display:flex;align-items:center;gap:4px;margin-bottom:6px;';
855
+ row.appendChild(this._makeBindSquare(propKey, cfg, designItem, 'data-animation', saveAndRefresh));
856
+ const lbl = document.createElement('label');
857
+ lbl.style.cssText = 'display:flex;align-items:center;gap:4px;font-size:11px;cursor:pointer;';
744
858
  const c = document.createElement('input');
745
859
  c.type = 'checkbox'; c.checked = val === true || val === 'true';
746
860
  c.onchange = save;
747
- wrap.appendChild(c);
748
- wrap.appendChild(document.createTextNode(label));
749
- return wrap;
861
+ lbl.appendChild(c);
862
+ lbl.appendChild(document.createTextNode(label));
863
+ row.appendChild(lbl);
864
+ return row;
750
865
  };
751
866
 
752
- const oidRow = (label, ctrlKey, ctrlCfg) => {
867
+ const oidRow = (label, ctrlCfg) => {
753
868
  const wrap = document.createElement('div');
754
869
  wrap.style.cssText = 'border:1px solid #eee;border-radius:3px;padding:6px;margin-bottom:6px;background:#fafafa;';
755
-
756
870
  const head = document.createElement('div');
757
- head.style.cssText = 'font-size:11px;font-weight:600;color:#444;margin-bottom:5px;';
758
- head.textContent = label;
871
+ head.style.cssText = 'display:flex;align-items:center;gap:4px;font-size:11px;font-weight:600;color:#444;margin-bottom:5px;';
872
+ head.appendChild(this._makeBindSquare('oid', ctrlCfg, designItem, 'data-animation', saveAndRefresh));
873
+ const headText = document.createElement('span');
874
+ headText.textContent = label;
875
+ head.appendChild(headText);
759
876
  wrap.appendChild(head);
760
-
761
877
  const oidInp = document.createElement('input');
762
- oidInp.type = 'text';
763
- oidInp.value = ctrlCfg.oid || '';
878
+ oidInp.type = 'text'; oidInp.value = ctrlCfg.oid || '';
764
879
  oidInp.placeholder = 'OID…';
765
880
  oidInp.style.cssText = 'width:100%;box-sizing:border-box;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;margin-bottom:3px;';
766
881
  oidInp.onchange = save;
767
-
768
882
  const pickBtn = document.createElement('button');
769
883
  pickBtn.textContent = '…';
770
884
  pickBtn.style.cssText = 'padding:2px 6px;font-size:11px;cursor:pointer;border:1px solid #aaa;border-radius:3px;margin-left:3px;';
@@ -772,51 +886,43 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
772
886
  const picked = await openSelectIdDialog(document.body, { id: oidInp.value });
773
887
  if (picked) { oidInp.value = picked; save(); }
774
888
  };
775
-
776
889
  const oidRow2 = document.createElement('div');
777
890
  oidRow2.style.cssText = 'display:flex;margin-bottom:3px;';
778
- oidRow2.appendChild(oidInp);
779
- oidRow2.appendChild(pickBtn);
891
+ oidRow2.appendChild(oidInp); oidRow2.appendChild(pickBtn);
780
892
  wrap.appendChild(oidRow2);
781
-
782
893
  const condSel = sel([
783
- ['equal','='], ['not_equal','≠'], ['less_than','<'], ['less_equal','≤'],
784
- ['greater_than','>'], ['greater_equal','≥'], ['exists','exists']
894
+ ['equal','='],['not_equal','≠'],['less_than','<'],['less_equal','≤'],
895
+ ['greater_than','>'],['greater_equal','≥'],['exists','exists']
785
896
  ], ctrlCfg.condition || 'equal');
786
897
  condSel.style.cssText += 'width:70px;flex:none;margin-right:4px;';
787
898
  const valInp = document.createElement('input');
788
899
  valInp.type = 'text'; valInp.value = ctrlCfg.value ?? 'true';
789
- valInp.placeholder = 'value';
790
900
  valInp.style.cssText = 'flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;';
791
901
  condSel.onchange = save; valInp.onchange = save;
792
-
793
902
  const condRow = document.createElement('div');
794
903
  condRow.style.cssText = 'display:flex;gap:3px;';
795
904
  condRow.appendChild(condSel); condRow.appendChild(valInp);
796
905
  wrap.appendChild(condRow);
797
-
798
- wrap._getCtrl = () => ({
799
- oid: oidInp.value || undefined,
800
- condition: condSel.value,
801
- value: valInp.value
802
- });
906
+ wrap._getCtrl = () => {
907
+ const v = { oid: oidInp.value || undefined, condition: condSel.value, value: valInp.value };
908
+ if (ctrlCfg.oid_bind) v.oid_bind = ctrlCfg.oid_bind;
909
+ return v;
910
+ };
803
911
  return wrap;
804
912
  };
805
913
 
806
914
  const effectSel = sel([
807
- ['', '— none —'], ['opacity','Opacity'], ['rotation','Rotation'],
808
- ['scale','Scale'], ['translate','Translate XY'],
809
- ['translateX','Translate X'], ['translateY','Translate Y'],
810
- ['skew','Skew'], ['fill','Fill Color'], ['transform','Transform (CSS)'],
811
- ['svg','SVG Attribute'], ['morphSVG','MorphSVG'], ['motionPath','Motion Path']
915
+ ['','— none —'],['opacity','Opacity'],['rotation','Rotation'],['scale','Scale'],
916
+ ['translate','Translate XY'],['translateX','Translate X'],['translateY','Translate Y'],
917
+ ['skew','Skew'],['fill','Fill Color'],['transform','Transform (CSS)'],
918
+ ['svg','SVG Attribute'],['morphSVG','MorphSVG'],['motionPath','Motion Path']
812
919
  ], cfg.effect || '');
813
920
 
814
921
  const svgAttrSel = sel([
815
- ['fill','fill'], ['color','color'], ['fill-opacity','fill-opacity'],
816
- ['stroke-opacity','stroke-opacity'], ['stroke-width','stroke-width'],
817
- ['stroke-dasharray','stroke-dasharray'], ['stroke-dashoffset','stroke-dashoffset'],
818
- ['x','x'], ['y','y'], ['cx','cx'], ['cy','cy'],
819
- ['r','r'], ['rx','rx'], ['ry','ry']
922
+ ['fill','fill'],['color','color'],['fill-opacity','fill-opacity'],
923
+ ['stroke-opacity','stroke-opacity'],['stroke-width','stroke-width'],
924
+ ['stroke-dasharray','stroke-dasharray'],['stroke-dashoffset','stroke-dashoffset'],
925
+ ['x','x'],['y','y'],['cx','cx'],['cy','cy'],['r','r'],['rx','rx'],['ry','ry']
820
926
  ], cfg.svgAttr || 'fill');
821
927
 
822
928
  const easeSel = sel([
@@ -829,127 +935,82 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
829
935
  ], cfg.ease || 'power1.inOut');
830
936
 
831
937
  const valueFromInp = inp(cfg.valueFrom, 'text');
832
- const valueToInp = inp(cfg.valueTo, 'text');
833
- const durationInp = inp(cfg.duration ?? 1, 'number');
834
- const repeatInp = inp(cfg.repeat ?? 0, 'number');
835
- const yoyoChk = chk(cfg.yoyo, 'yoyo');
836
- const originXInp = inp(cfg.transformOriginX ?? '50', 'number');
837
- const originYInp = inp(cfg.transformOriginY ?? '50', 'number');
838
- const fillFromInp = inp(cfg.fillColorFrom || '#000000', 'color');
839
- const fillToInp = inp(cfg.fillColorTo || '#ff0000', 'color');
840
- const pathIdInp = inp(cfg.pathId, 'text');
841
- const alignToPathChk = chk(cfg.alignToPath, 'align to path');
842
- const orientToPathChk = chk(cfg.orientToPath, 'auto-rotate');
843
-
844
- const playCtrl = oidRow('▶ Play', 'play', controls.play || {});
845
- const pauseCtrl = oidRow(' Pause', 'pause', controls.pause || {});
846
- const resumeCtrl = oidRow(' Resume', 'resume', controls.resume || {});
847
- const stopCtrl = oidRow(' Stop', 'stop', controls.stop || {});
848
- const reverseCtrl = oidRow(' Reverse', 'reverse', controls.reverse || {});
849
-
850
- const collectAnimCfg = () => {
851
- const e = effectSel.value;
852
- const c = {};
853
- const addCtrl = (key, row) => {
854
- const v = row._getCtrl();
855
- if (v.oid) c[key] = v;
856
- };
857
- addCtrl('play', playCtrl); addCtrl('pause', pauseCtrl);
858
- addCtrl('resume', resumeCtrl); addCtrl('stop', stopCtrl);
859
- addCtrl('reverse', reverseCtrl);
860
-
861
- const out = {};
862
- if (e) out.effect = e;
863
- if (e === 'svg' && svgAttrSel.value) out.svgAttr = svgAttrSel.value;
864
- if (valueFromInp.value) out.valueFrom = valueFromInp.value;
865
- if (valueToInp.value) out.valueTo = valueToInp.value;
866
- out.duration = parseFloat(durationInp.value) || 1;
867
- out.ease = easeSel.value || 'power1.inOut';
868
- out.repeat = parseInt(repeatInp.value) || 0;
869
- if (yoyoChk.querySelector('input').checked) out.yoyo = true;
870
- if (['rotation','scale'].includes(e)) {
871
- out.transformOriginX = originXInp.value;
872
- out.transformOriginY = originYInp.value;
873
- }
874
- if (e === 'fill' || e === 'svg') {
875
- if (fillFromInp.value) out.fillColorFrom = fillFromInp.value;
876
- if (fillToInp.value) out.fillColorTo = fillToInp.value;
877
- }
878
- if (e === 'motionPath') {
879
- if (pathIdInp.value) out.pathId = pathIdInp.value;
880
- if (alignToPathChk.querySelector('input').checked) out.alignToPath = true;
881
- if (orientToPathChk.querySelector('input').checked) out.orientToPath = true;
882
- }
883
- if (Object.keys(c).length) out.controls = c;
884
- return out;
885
- };
886
-
887
- // Build UI
938
+ const valueToInp = inp(cfg.valueTo, 'text');
939
+ const durationInp = inp(cfg.duration ?? 1, 'number');
940
+ const repeatInp = inp(cfg.repeat ?? 0, 'number');
941
+ const originXInp = inp(cfg.transformOriginX ?? '50', 'number');
942
+ const originYInp = inp(cfg.transformOriginY ?? '50', 'number');
943
+ const fillFromInp = inp(cfg.fillColorFrom || '#000000', 'color');
944
+ const fillToInp = inp(cfg.fillColorTo || '#ff0000', 'color');
945
+ const pathIdInp = inp(cfg.pathId, 'text');
946
+
947
+ const yoyoChk = chkField('yoyo', 'Yoyo', cfg.yoyo);
948
+ const alignToPathChk = chkField('alignToPath', 'Align to path', cfg.alignToPath);
949
+ const orientToPathChk= chkField('orientToPath', 'Auto-rotate', cfg.orientToPath);
950
+
951
+ const playCtrl = oidRow(' Play', controls.play || {});
952
+ const pauseCtrl = oidRow(' Pause', controls.pause || {});
953
+ const resumeCtrl = oidRow(' Resume', controls.resume || {});
954
+ const stopCtrl = oidRow(' Stop', controls.stop || {});
955
+ const reverseCtrl = oidRow('◀ Reverse', controls.reverse || {});
956
+
957
+ // ── Build UI ──────────────────────────────────────────────────────────
888
958
  content.innerHTML = '';
889
959
 
890
- // Clear button
891
960
  const clearBtn = document.createElement('button');
892
- clearBtn.textContent = 'Clear Animation';
961
+ clearBtn.textContent = 'Clear';
893
962
  clearBtn.style.cssText = 'float:right;font-size:10px;padding:2px 6px;cursor:pointer;border:1px solid #ccc;border-radius:3px;background:#fff;margin-bottom:8px;';
894
963
  clearBtn.onclick = () => { designItem.removeAttribute('data-animation'); this._updateAnimationsPanel(); };
895
964
  content.appendChild(clearBtn);
896
965
 
897
966
  const sec = (title) => {
898
967
  const d = document.createElement('div');
899
- d.style.cssText = 'font-size:11px;font-weight:700;color:#555;margin:10px 0 5px;padding-top:8px;border-top:1px solid #eee;';
968
+ d.style.cssText = 'font-size:11px;font-weight:700;color:#555;margin:10px 0 5px;padding-top:8px;border-top:1px solid #eee;clear:both;';
900
969
  d.textContent = title;
901
970
  return d;
902
971
  };
903
972
 
904
973
  content.appendChild(sec('Effect'));
905
- content.appendChild(field('Type', effectSel));
974
+ content.appendChild(field('Type', 'effect', effectSel));
906
975
 
907
- const svgAttrRow = field('SVG Attr', svgAttrSel);
976
+ const svgAttrRow = field('SVG Attr', 'svgAttr', svgAttrSel);
908
977
  svgAttrRow.style.display = cfg.effect === 'svg' ? '' : 'none';
909
978
  content.appendChild(svgAttrRow);
910
- effectSel.addEventListener('change', () => {
911
- svgAttrRow.style.display = effectSel.value === 'svg' ? '' : 'none';
912
- });
979
+ effectSel.addEventListener('change', () => { svgAttrRow.style.display = effectSel.value === 'svg' ? '' : 'none'; });
913
980
 
914
- content.appendChild(field('Value From', valueFromInp));
915
- content.appendChild(field('Value To', valueToInp));
981
+ content.appendChild(field('Value From', 'valueFrom', valueFromInp));
982
+ content.appendChild(field('Value To', 'valueTo', valueToInp));
916
983
 
917
984
  const colorRows = document.createElement('div');
918
985
  colorRows.style.display = (cfg.effect === 'fill' || cfg.effect === 'svg') ? '' : 'none';
919
- colorRows.appendChild(field('Color From', fillFromInp));
920
- colorRows.appendChild(field('Color To', fillToInp));
986
+ colorRows.appendChild(field('Color From', 'fillColorFrom', fillFromInp));
987
+ colorRows.appendChild(field('Color To', 'fillColorTo', fillToInp));
921
988
  content.appendChild(colorRows);
922
- effectSel.addEventListener('change', () => {
923
- colorRows.style.display = (effectSel.value === 'fill' || effectSel.value === 'svg') ? '' : 'none';
924
- });
989
+ effectSel.addEventListener('change', () => { colorRows.style.display = (effectSel.value === 'fill' || effectSel.value === 'svg') ? '' : 'none'; });
925
990
 
926
991
  const motionRows = document.createElement('div');
927
992
  motionRows.style.display = cfg.effect === 'motionPath' ? '' : 'none';
928
- motionRows.appendChild(field('Path ID', pathIdInp));
993
+ motionRows.appendChild(field('Path ID', 'pathId', pathIdInp));
929
994
  motionRows.appendChild(alignToPathChk);
930
995
  motionRows.appendChild(orientToPathChk);
931
996
  content.appendChild(motionRows);
932
- effectSel.addEventListener('change', () => {
933
- motionRows.style.display = effectSel.value === 'motionPath' ? '' : 'none';
934
- });
997
+ effectSel.addEventListener('change', () => { motionRows.style.display = effectSel.value === 'motionPath' ? '' : 'none'; });
935
998
 
936
999
  content.appendChild(sec('Timing'));
937
- content.appendChild(field('Duration (s)', durationInp));
938
- content.appendChild(field('Ease', easeSel));
939
- content.appendChild(field('Repeat', repeatInp));
1000
+ content.appendChild(field('Duration (s)', 'duration', durationInp));
1001
+ content.appendChild(field('Ease', 'ease', easeSel));
1002
+ content.appendChild(field('Repeat', 'repeat', repeatInp));
940
1003
  content.appendChild(yoyoChk);
941
1004
 
942
1005
  const originRows = document.createElement('div');
943
1006
  originRows.style.display = (cfg.effect === 'rotation' || cfg.effect === 'scale') ? '' : 'none';
944
1007
  originRows.appendChild(sec('Transform Origin'));
945
- originRows.appendChild(field('Origin X (%)', originXInp));
946
- originRows.appendChild(field('Origin Y (%)', originYInp));
1008
+ originRows.appendChild(field('Origin X (%)', 'transformOriginX', originXInp));
1009
+ originRows.appendChild(field('Origin Y (%)', 'transformOriginY', originYInp));
947
1010
  content.appendChild(originRows);
948
- effectSel.addEventListener('change', () => {
949
- originRows.style.display = (effectSel.value === 'rotation' || effectSel.value === 'scale') ? '' : 'none';
950
- });
1011
+ effectSel.addEventListener('change', () => { originRows.style.display = (effectSel.value === 'rotation' || effectSel.value === 'scale') ? '' : 'none'; });
951
1012
 
952
- content.appendChild(sec('Controls (OID bindings)'));
1013
+ content.appendChild(sec('Controls (OID triggers)'));
953
1014
  content.appendChild(playCtrl);
954
1015
  content.appendChild(pauseCtrl);
955
1016
  content.appendChild(resumeCtrl);
@@ -1002,18 +1063,52 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
1002
1063
  let cfg = {};
1003
1064
  try { cfg = JSON.parse(element.getAttribute('data-effects') || '{}'); } catch (e) {}
1004
1065
 
1066
+ const collectEffectsCfg = () => {
1067
+ const out = {};
1068
+ if (typeSel.value) out.type = typeSel.value;
1069
+ out.trigger = triggerSel.value || 'load';
1070
+ out.duration = parseFloat(durationInp.value) || 0.5;
1071
+ if (parseFloat(delayInp.value)) out.delay = parseFloat(delayInp.value);
1072
+ if (parseInt(repeatInp.value)) out.repeat = parseInt(repeatInp.value);
1073
+ out.ease = easeSel.value || 'power2.out';
1074
+ if (triggerSel.value === 'oid' && (oidInp.value || cfg.oid_bind)) {
1075
+ if (oidInp.value) out.oid = oidInp.value;
1076
+ out.condition = condSel.value;
1077
+ out.conditionValue = condValInp.value;
1078
+ }
1079
+ if (typeSel.value === 'glow') {
1080
+ out.glowColor = glowColorInp.value;
1081
+ out.glowSize = parseInt(glowSizeInp.value) || 10;
1082
+ }
1083
+ if (typeSel.value === 'blur') out.blurAmount = parseInt(blurInp.value) || 5;
1084
+ // Preserve all _bind properties
1085
+ for (const [k, v] of Object.entries(cfg)) {
1086
+ if (k.endsWith('_bind') && v) out[k] = v;
1087
+ }
1088
+ return out;
1089
+ };
1090
+
1005
1091
  const save = () => {
1006
1092
  const c = collectEffectsCfg();
1007
1093
  if (!c.type) designItem.removeAttribute('data-effects');
1008
1094
  else designItem.setAttribute('data-effects', JSON.stringify(c));
1009
1095
  };
1010
1096
 
1011
- const field = (label, inputEl) => {
1097
+ const saveAndRefresh = () => { save(); this._updateEffectsPanel(); };
1098
+
1099
+ const field = (label, propKey, inputEl) => {
1012
1100
  const row = document.createElement('div');
1013
- row.style.cssText = 'display:flex;align-items:center;gap:6px;margin-bottom:6px;';
1101
+ row.style.cssText = 'display:flex;align-items:center;gap:4px;margin-bottom:6px;';
1102
+ if (propKey) {
1103
+ row.appendChild(this._makeBindSquare(propKey, cfg, designItem, 'data-effects', saveAndRefresh));
1104
+ } else {
1105
+ const sp = document.createElement('div');
1106
+ sp.style.cssText = 'width:11px;min-width:11px;flex-shrink:0;';
1107
+ row.appendChild(sp);
1108
+ }
1014
1109
  const lbl = document.createElement('span');
1015
1110
  lbl.textContent = label;
1016
- lbl.style.cssText = 'min-width:80px;font-size:11px;color:#555;';
1111
+ lbl.style.cssText = 'min-width:78px;font-size:11px;color:#555;';
1017
1112
  row.appendChild(lbl); row.appendChild(inputEl);
1018
1113
  return row;
1019
1114
  };
@@ -1049,107 +1144,78 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
1049
1144
  ], cfg.trigger || 'load');
1050
1145
 
1051
1146
  const durationInp = inp(cfg.duration ?? 0.5, 'number');
1052
- const delayInp = inp(cfg.delay ?? 0, 'number');
1053
- const repeatInp = inp(cfg.repeat ?? 0, 'number');
1147
+ const delayInp = inp(cfg.delay ?? 0, 'number');
1148
+ const repeatInp = inp(cfg.repeat ?? 0, 'number');
1054
1149
 
1055
1150
  const easeSel = sel([
1056
1151
  ['power2.out','power2.out'],['power2.in','power2.in'],['power2.inOut','power2.inOut'],
1057
1152
  ['power1.inOut','power1.inOut'],['bounce.out','bounce.out'],
1058
- ['elastic.out(1,0.3)','elastic.out'],['back.inOut(1.7)','back.inOut'],
1059
- ['none','none']
1153
+ ['elastic.out(1,0.3)','elastic.out'],['back.inOut(1.7)','back.inOut'],['none','none']
1060
1154
  ], cfg.ease || 'power2.out');
1061
1155
 
1062
- const oidInp = inp(cfg.oid || '');
1063
- const condSel = sel([
1064
- ['equal','='],['not_equal','≠'],['less_than','<'],['greater_than','>'],['exists','exists']
1065
- ], cfg.condition || 'equal');
1156
+ const oidInp = inp(cfg.oid || '');
1157
+ const condSel = sel([['equal','='],['not_equal','≠'],['less_than','<'],['greater_than','>'],['exists','exists']], cfg.condition || 'equal');
1066
1158
  const condValInp = inp(cfg.conditionValue ?? 'true');
1067
-
1068
1159
  const glowColorInp = inp(cfg.glowColor || 'yellow', 'color');
1069
- const glowSizeInp = inp(cfg.glowSize || 10, 'number');
1070
- const blurInp = inp(cfg.blurAmount || 5, 'number');
1160
+ const glowSizeInp = inp(cfg.glowSize || 10, 'number');
1161
+ const blurInp = inp(cfg.blurAmount || 5, 'number');
1071
1162
 
1072
1163
  const oidPickBtn = document.createElement('button');
1073
1164
  oidPickBtn.textContent = '…';
1074
- oidPickBtn.style.cssText = 'padding:2px 6px;font-size:11px;cursor:pointer;border:1px solid #aaa;border-radius:3px;';
1165
+ oidPickBtn.style.cssText = 'padding:2px 6px;font-size:11px;cursor:pointer;border:1px solid #aaa;border-radius:3px;flex-shrink:0;';
1075
1166
  oidPickBtn.onclick = async () => {
1076
1167
  const picked = await openSelectIdDialog(document.body, { id: oidInp.value });
1077
1168
  if (picked) { oidInp.value = picked; save(); }
1078
1169
  };
1079
1170
 
1080
- const collectEffectsCfg = () => {
1081
- const out = {};
1082
- if (typeSel.value) out.type = typeSel.value;
1083
- out.trigger = triggerSel.value || 'load';
1084
- out.duration = parseFloat(durationInp.value) || 0.5;
1085
- if (parseFloat(delayInp.value)) out.delay = parseFloat(delayInp.value);
1086
- if (parseInt(repeatInp.value)) out.repeat = parseInt(repeatInp.value);
1087
- out.ease = easeSel.value || 'power2.out';
1088
- if (triggerSel.value === 'oid' && oidInp.value) {
1089
- out.oid = oidInp.value;
1090
- out.condition = condSel.value;
1091
- out.conditionValue = condValInp.value;
1092
- }
1093
- if (typeSel.value === 'glow') {
1094
- out.glowColor = glowColorInp.value;
1095
- out.glowSize = parseInt(glowSizeInp.value) || 10;
1096
- }
1097
- if (typeSel.value === 'blur') out.blurAmount = parseInt(blurInp.value) || 5;
1098
- return out;
1099
- };
1100
-
1171
+ // ── Build UI ──────────────────────────────────────────────────────────
1101
1172
  content.innerHTML = '';
1102
1173
 
1103
1174
  const clearBtn = document.createElement('button');
1104
- clearBtn.textContent = 'Clear Effect';
1175
+ clearBtn.textContent = 'Clear';
1105
1176
  clearBtn.style.cssText = 'float:right;font-size:10px;padding:2px 6px;cursor:pointer;border:1px solid #ccc;border-radius:3px;background:#fff;margin-bottom:8px;';
1106
1177
  clearBtn.onclick = () => { designItem.removeAttribute('data-effects'); this._updateEffectsPanel(); };
1107
1178
  content.appendChild(clearBtn);
1108
1179
 
1109
- content.appendChild(field('Type', typeSel));
1110
- content.appendChild(field('Trigger', triggerSel));
1111
- content.appendChild(field('Duration (s)', durationInp));
1112
- content.appendChild(field('Delay (s)', delayInp));
1113
- content.appendChild(field('Repeat', repeatInp));
1114
- content.appendChild(field('Ease', easeSel));
1180
+ content.appendChild(field('Type', 'type', typeSel));
1181
+ content.appendChild(field('Trigger', null, triggerSel));
1182
+ content.appendChild(field('Duration (s)', 'duration', durationInp));
1183
+ content.appendChild(field('Delay (s)', 'delay', delayInp));
1184
+ content.appendChild(field('Repeat', 'repeat', repeatInp));
1185
+ content.appendChild(field('Ease', 'ease', easeSel));
1115
1186
 
1116
1187
  const oidSection = document.createElement('div');
1117
1188
  oidSection.style.display = cfg.trigger === 'oid' ? '' : 'none';
1118
- const oidRow = document.createElement('div');
1119
- oidRow.style.cssText = 'display:flex;gap:3px;margin-bottom:6px;';
1120
- oidRow.appendChild(oidInp); oidRow.appendChild(oidPickBtn);
1121
- oidSection.appendChild(field('OID', document.createElement('span')));
1122
- oidSection.lastChild.remove();
1123
1189
  const oidLabel = document.createElement('div');
1124
- oidLabel.style.cssText = 'font-size:11px;font-weight:600;color:#555;margin-bottom:3px;';
1125
- oidLabel.textContent = 'OID';
1190
+ oidLabel.style.cssText = 'display:flex;align-items:center;gap:4px;font-size:11px;font-weight:600;color:#555;margin-bottom:3px;padding-left:4px;';
1191
+ oidLabel.appendChild(this._makeBindSquare('oid', cfg, designItem, 'data-effects', saveAndRefresh));
1192
+ const oidLabelText = document.createElement('span');
1193
+ oidLabelText.textContent = 'OID';
1194
+ oidLabel.appendChild(oidLabelText);
1195
+ const oidRowDiv = document.createElement('div');
1196
+ oidRowDiv.style.cssText = 'display:flex;gap:3px;margin-bottom:4px;padding-left:15px;';
1197
+ oidRowDiv.appendChild(oidInp); oidRowDiv.appendChild(oidPickBtn);
1198
+ const condRowDiv = document.createElement('div');
1199
+ condRowDiv.style.cssText = 'display:flex;gap:3px;margin-bottom:6px;padding-left:15px;';
1200
+ condRowDiv.appendChild(condSel); condRowDiv.appendChild(condValInp);
1126
1201
  oidSection.appendChild(oidLabel);
1127
- oidSection.appendChild(oidRow);
1128
- const condRow = document.createElement('div');
1129
- condRow.style.cssText = 'display:flex;gap:3px;margin-bottom:6px;';
1130
- condRow.appendChild(condSel); condRow.appendChild(condValInp);
1131
- oidSection.appendChild(condRow);
1202
+ oidSection.appendChild(oidRowDiv);
1203
+ oidSection.appendChild(condRowDiv);
1132
1204
  content.appendChild(oidSection);
1133
- triggerSel.addEventListener('change', () => {
1134
- oidSection.style.display = triggerSel.value === 'oid' ? '' : 'none';
1135
- });
1205
+ triggerSel.addEventListener('change', () => { oidSection.style.display = triggerSel.value === 'oid' ? '' : 'none'; });
1136
1206
 
1137
1207
  const glowSection = document.createElement('div');
1138
1208
  glowSection.style.display = cfg.type === 'glow' ? '' : 'none';
1139
- glowSection.appendChild(field('Glow Color', glowColorInp));
1140
- glowSection.appendChild(field('Glow Size (px)', glowSizeInp));
1209
+ glowSection.appendChild(field('Glow Color', 'glowColor', glowColorInp));
1210
+ glowSection.appendChild(field('Glow Size', 'glowSize', glowSizeInp));
1141
1211
  content.appendChild(glowSection);
1142
- typeSel.addEventListener('change', () => {
1143
- glowSection.style.display = typeSel.value === 'glow' ? '' : 'none';
1144
- });
1212
+ typeSel.addEventListener('change', () => { glowSection.style.display = typeSel.value === 'glow' ? '' : 'none'; });
1145
1213
 
1146
1214
  const blurSection = document.createElement('div');
1147
1215
  blurSection.style.display = cfg.type === 'blur' ? '' : 'none';
1148
- blurSection.appendChild(field('Blur (px)', blurInp));
1216
+ blurSection.appendChild(field('Blur (px)', 'blurAmount', blurInp));
1149
1217
  content.appendChild(blurSection);
1150
- typeSel.addEventListener('change', () => {
1151
- blurSection.style.display = typeSel.value === 'blur' ? '' : 'none';
1152
- });
1218
+ typeSel.addEventListener('change', () => { blurSection.style.display = typeSel.value === 'blur' ? '' : 'none'; });
1153
1219
  }
1154
1220
 
1155
1221
  /* Move to a Dock Spawn Helper */
@@ -156,9 +156,26 @@ class AnimationInstance {
156
156
  if (!window.gsap) return;
157
157
  const controls = this.cfg.controls || {};
158
158
 
159
- const bindControl = (key, action) => {
159
+ const bindControl = async (key, action) => {
160
160
  const ctrl = controls[key];
161
- if (!ctrl?.oid) return;
161
+ if (!ctrl) return;
162
+
163
+ // If OID itself is bound via binding square, resolve it first
164
+ if (ctrl.oid_bind?.signal) {
165
+ try {
166
+ const s = await iobrokerHandler.connection.getState(ctrl.oid_bind.signal);
167
+ if (s?.val != null) ctrl.oid = String(s.val);
168
+ } catch (e) {}
169
+ const oidBindHandler = (id, state) => {
170
+ if (state?.val != null) ctrl.oid = String(state.val);
171
+ };
172
+ try {
173
+ iobrokerHandler.connection.subscribeState(ctrl.oid_bind.signal, oidBindHandler);
174
+ this._subs.push({ oid: ctrl.oid_bind.signal, handler: oidBindHandler });
175
+ } catch (e) {}
176
+ }
177
+
178
+ if (!ctrl.oid) return;
162
179
  const handler = (id, state) => {
163
180
  if (checkCond(state?.val, ctrl.condition || 'equal', ctrl.value ?? 'true')) action();
164
181
  };
@@ -171,11 +188,38 @@ class AnimationInstance {
171
188
  }).catch(() => {});
172
189
  };
173
190
 
174
- bindControl('play', () => this._play());
175
- bindControl('pause', () => this.tween?.pause());
176
- bindControl('resume', () => this.tween?.play());
177
- bindControl('stop', () => this.tween?.pause(0));
178
- bindControl('reverse', () => this.tween?.reverse());
191
+ await bindControl('play', () => this._play());
192
+ await bindControl('pause', () => this.tween?.pause());
193
+ await bindControl('resume', () => this.tween?.play());
194
+ await bindControl('stop', () => this.tween?.pause(0));
195
+ await bindControl('reverse', () => this.tween?.reverse());
196
+
197
+ // Dynamic property bindings — each prop can have a propKey_bind: {signal, expression}
198
+ const dynamicProps = [
199
+ 'effect', 'valueFrom', 'valueTo', 'duration', 'ease', 'repeat', 'yoyo',
200
+ 'fillColorFrom', 'fillColorTo', 'transformOriginX', 'transformOriginY',
201
+ 'svgAttr', 'pathId', 'alignToPath', 'orientToPath'
202
+ ];
203
+ for (const prop of dynamicProps) {
204
+ const bindCfg = this.cfg[prop + '_bind'];
205
+ if (!bindCfg?.signal) continue;
206
+ const propCapture = prop;
207
+ const handler = (id, state) => {
208
+ if (state?.val != null) {
209
+ this.cfg[propCapture] = state.val;
210
+ // Rebuild tween if currently playing
211
+ if (this.tween && !this.tween.paused()) this._play();
212
+ }
213
+ };
214
+ try {
215
+ iobrokerHandler.connection.subscribeState(bindCfg.signal, handler);
216
+ this._subs.push({ oid: bindCfg.signal, handler });
217
+ } catch (e) {}
218
+ try {
219
+ const state = await iobrokerHandler.connection.getState(bindCfg.signal);
220
+ if (state?.val != null) this.cfg[prop] = state.val;
221
+ } catch (e) {}
222
+ }
179
223
  }
180
224
 
181
225
  _play() {
@@ -333,15 +377,28 @@ async function _applyEffect(el, cfg) {
333
377
  } else if (cfg.trigger === 'click') {
334
378
  _clickFn = applyTween;
335
379
  el.addEventListener('click', _clickFn);
336
- } else if (cfg.trigger === 'oid' && cfg.oid) {
337
- _oidId = cfg.oid;
338
- _oidHandler = (id, state) => {
339
- if (checkCond(state?.val, cfg.condition || 'equal', cfg.conditionValue ?? 'true')) applyTween();
340
- };
341
- try { iobrokerHandler.connection.subscribeState(cfg.oid, _oidHandler); } catch (e) {}
342
- iobrokerHandler.connection.getState(cfg.oid).then(state => {
343
- if (state && checkCond(state.val, cfg.condition || 'equal', cfg.conditionValue ?? 'true')) applyTween();
344
- }).catch(() => {});
380
+ } else if (cfg.trigger === 'oid') {
381
+ // Resolve OID — may come from a binding square binding
382
+ if (cfg.oid_bind?.signal) {
383
+ try {
384
+ const s = await iobrokerHandler.connection.getState(cfg.oid_bind.signal);
385
+ if (s?.val != null) cfg.oid = String(s.val);
386
+ } catch (e) {}
387
+ const oidBindHandler = (id, state) => { if (state?.val != null) cfg.oid = String(state.val); };
388
+ try { iobrokerHandler.connection.subscribeState(cfg.oid_bind.signal, oidBindHandler); } catch (e) {}
389
+ _oidId = cfg.oid_bind.signal;
390
+ _oidHandler = oidBindHandler;
391
+ }
392
+ if (cfg.oid) {
393
+ _oidId = cfg.oid;
394
+ _oidHandler = (id, state) => {
395
+ if (checkCond(state?.val, cfg.condition || 'equal', cfg.conditionValue ?? 'true')) applyTween();
396
+ };
397
+ try { iobrokerHandler.connection.subscribeState(cfg.oid, _oidHandler); } catch (e) {}
398
+ iobrokerHandler.connection.getState(cfg.oid).then(state => {
399
+ if (state && checkCond(state.val, cfg.condition || 'equal', cfg.conditionValue ?? 'true')) applyTween();
400
+ }).catch(() => {});
401
+ }
345
402
  }
346
403
 
347
404
  return () => {