iobroker.mywebui 1.37.67 → 1.37.69

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.67",
4
+ "version": "1.37.69",
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.67",
3
+ "version": "1.37.69",
4
4
  "description": "ioBroker mywebui - Custom edited mywebui by gokturk413",
5
5
  "type": "module",
6
6
  "main": "dist/backend/main.js",
@@ -760,85 +760,32 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
760
760
  const designItem = items[0];
761
761
  const element = designItem.element;
762
762
 
763
- let cfg = {};
764
- try { cfg = JSON.parse(element.getAttribute('data-animation') || '{}'); } catch (e) {}
765
- const controls = cfg.controls || {};
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
-
804
- const save = () => {
805
- const newCfg = collectAnimCfg();
806
- if (!newCfg.effect && !newCfg.controls && !Object.keys(newCfg).some(k => k.endsWith('_bind'))) {
807
- designItem.removeAttribute('data-animation');
808
- } else {
809
- designItem.setAttribute('data-animation', JSON.stringify(newCfg));
810
- }
763
+ // Support both single object (legacy) and array
764
+ let cfgList = [];
765
+ try {
766
+ const raw = JSON.parse(element.getAttribute('data-animation') || '[]');
767
+ cfgList = Array.isArray(raw) ? raw : [raw];
768
+ } catch (e) {}
769
+
770
+ const saveAll = () => {
771
+ const result = cfgList.filter(c => c._collect && (c._collect().effect || Object.keys(c._collect()).some(k => k.endsWith('_bind'))));
772
+ const data = result.map(c => c._collect());
773
+ if (data.length === 0) designItem.removeAttribute('data-animation');
774
+ else if (data.length === 1) designItem.setAttribute('data-animation', JSON.stringify(data[0]));
775
+ else designItem.setAttribute('data-animation', JSON.stringify(data));
811
776
  };
777
+ const saveAndRefresh = () => { saveAll(); this._updateAnimationsPanel(); };
812
778
 
813
- const saveAndRefresh = () => { save(); this._updateAnimationsPanel(); };
779
+ // ── Shared helpers ────────────────────────────────────────────────────
814
780
 
815
- // field(label, propKey, inputEl) propKey=null → no binding square
816
- const field = (label, propKey, inputEl) => {
817
- const row = document.createElement('div');
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
- }
826
- const lbl = document.createElement('span');
827
- lbl.textContent = label;
828
- lbl.style.cssText = 'min-width:84px;font-size:11px;color:#555;';
829
- row.appendChild(lbl);
830
- row.appendChild(inputEl);
831
- return row;
832
- };
833
-
834
- const inp = (val, type = 'text') => {
781
+ const mkInp = (val, type, onchange) => {
835
782
  const i = document.createElement('input');
836
783
  i.type = type; i.value = val ?? '';
837
784
  i.style.cssText = 'flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;';
838
- i.onchange = save; return i;
785
+ i.onchange = onchange; return i;
839
786
  };
840
787
 
841
- const sel = (options, val) => {
788
+ const mkSel = (options, val, onchange) => {
842
789
  const s = document.createElement('select');
843
790
  s.style.cssText = 'flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;';
844
791
  options.forEach(([v, t]) => {
@@ -846,54 +793,60 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
846
793
  o.value = v; o.textContent = t; o.selected = v === val;
847
794
  s.appendChild(o);
848
795
  });
849
- s.onchange = save; return s;
796
+ s.onchange = onchange; return s;
797
+ };
798
+
799
+ const mkField = (label, propKey, inputEl, cfg) => {
800
+ const row = document.createElement('div');
801
+ row.style.cssText = 'display:flex;align-items:center;gap:4px;margin-bottom:6px;';
802
+ if (propKey) {
803
+ row.appendChild(this._makeBindSquare(propKey, cfg, designItem, 'data-animation', saveAndRefresh));
804
+ } else {
805
+ const sp = document.createElement('div'); sp.style.cssText = 'width:11px;min-width:11px;flex-shrink:0;';
806
+ row.appendChild(sp);
807
+ }
808
+ const lbl = document.createElement('span');
809
+ lbl.textContent = label; lbl.style.cssText = 'min-width:84px;font-size:11px;color:#555;';
810
+ row.appendChild(lbl); row.appendChild(inputEl);
811
+ return row;
850
812
  };
851
813
 
852
- const chkField = (propKey, label, val) => {
814
+ const mkChkField = (propKey, label, val, cfg, onchange) => {
853
815
  const row = document.createElement('div');
854
816
  row.style.cssText = 'display:flex;align-items:center;gap:4px;margin-bottom:6px;';
855
817
  row.appendChild(this._makeBindSquare(propKey, cfg, designItem, 'data-animation', saveAndRefresh));
856
818
  const lbl = document.createElement('label');
857
819
  lbl.style.cssText = 'display:flex;align-items:center;gap:4px;font-size:11px;cursor:pointer;';
858
820
  const c = document.createElement('input');
859
- c.type = 'checkbox'; c.checked = val === true || val === 'true';
860
- c.onchange = save;
861
- lbl.appendChild(c);
862
- lbl.appendChild(document.createTextNode(label));
821
+ c.type = 'checkbox'; c.checked = val === true || val === 'true'; c.onchange = onchange;
822
+ lbl.appendChild(c); lbl.appendChild(document.createTextNode(label));
863
823
  row.appendChild(lbl);
824
+ row._checked = () => c.checked;
864
825
  return row;
865
826
  };
866
827
 
867
- const oidRow = (label, ctrlCfg) => {
828
+ const mkOidRow = (label, ctrlCfg, onchange) => {
868
829
  const wrap = document.createElement('div');
869
830
  wrap.style.cssText = 'border:1px solid #eee;border-radius:3px;padding:6px;margin-bottom:6px;background:#fafafa;';
870
831
  const head = document.createElement('div');
871
832
  head.style.cssText = 'display:flex;align-items:center;gap:4px;font-size:11px;font-weight:600;color:#444;margin-bottom:5px;';
872
833
  head.appendChild(this._makeBindSquare('oid', ctrlCfg, designItem, 'data-animation', saveAndRefresh));
873
- const headText = document.createElement('span');
874
- headText.textContent = label;
875
- head.appendChild(headText);
876
- wrap.appendChild(head);
877
- const condSel = sel([
834
+ const headText = document.createElement('span'); headText.textContent = label;
835
+ head.appendChild(headText); wrap.appendChild(head);
836
+ const condSel = mkSel([
878
837
  ['equal','='],['not_equal','≠'],['less_than','<'],['less_equal','≤'],
879
838
  ['greater_than','>'],['greater_equal','≥'],['exists','exists']
880
- ], ctrlCfg.condition || 'equal');
839
+ ], ctrlCfg.condition || 'equal', onchange);
881
840
  condSel.style.cssText += 'flex:1;';
882
- condSel.onchange = save;
883
841
  const condRow = document.createElement('div');
884
842
  condRow.style.cssText = 'display:flex;align-items:center;gap:4px;margin-bottom:4px;';
885
843
  condRow.appendChild(this._makeBindSquare('condition', ctrlCfg, designItem, 'data-animation', saveAndRefresh));
886
- condRow.appendChild(condSel);
887
- wrap.appendChild(condRow);
888
- const valInp = document.createElement('input');
889
- valInp.type = 'text'; valInp.value = ctrlCfg.value ?? 'true';
890
- valInp.style.cssText = 'flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;';
891
- valInp.onchange = save;
844
+ condRow.appendChild(condSel); wrap.appendChild(condRow);
845
+ const valInp = mkInp(ctrlCfg.value ?? 'true', 'text', onchange);
892
846
  const valRow = document.createElement('div');
893
847
  valRow.style.cssText = 'display:flex;align-items:center;gap:4px;';
894
848
  valRow.appendChild(this._makeBindSquare('value', ctrlCfg, designItem, 'data-animation', saveAndRefresh));
895
- valRow.appendChild(valInp);
896
- wrap.appendChild(valRow);
849
+ valRow.appendChild(valInp); wrap.appendChild(valRow);
897
850
  wrap._getCtrl = () => {
898
851
  const v = { condition: condSel.value, value: valInp.value };
899
852
  if (ctrlCfg.oid_bind) v.oid_bind = ctrlCfg.oid_bind;
@@ -904,111 +857,187 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
904
857
  return wrap;
905
858
  };
906
859
 
907
- const effectSel = sel([
908
- ['','— none —'],['opacity','Opacity'],['rotation','Rotation'],['scale','Scale'],
909
- ['translate','Translate XY'],['translateX','Translate X'],['translateY','Translate Y'],
910
- ['skew','Skew'],['fill','Fill Color'],['transform','Transform (CSS)'],
911
- ['svg','SVG Attribute'],['morphSVG','MorphSVG'],['motionPath','Motion Path']
912
- ], cfg.effect || '');
860
+ // ── Build one animation block ─────────────────────────────────────────
861
+
862
+ const buildAnimBlock = (cfg, index) => {
863
+ const save = saveAll;
864
+ const controls = cfg.controls || {};
865
+
866
+ const block = document.createElement('div');
867
+ block.style.cssText = 'border:2px solid #c8d6e0;border-radius:5px;padding:8px;margin-bottom:10px;background:#f7fafc;';
868
+
869
+ // Header: "Animation #N" + delete button
870
+ const hdr = document.createElement('div');
871
+ hdr.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;';
872
+ const hdrTitle = document.createElement('span');
873
+ hdrTitle.textContent = `Animation #${index + 1}`;
874
+ hdrTitle.style.cssText = 'font-size:12px;font-weight:700;color:#3a6a8a;';
875
+ const delBtn = document.createElement('button');
876
+ delBtn.textContent = '✕ Remove';
877
+ delBtn.style.cssText = 'font-size:10px;padding:2px 6px;cursor:pointer;border:1px solid #c66;border-radius:3px;background:#fff;color:#c33;';
878
+ delBtn.onclick = () => { cfgList.splice(index, 1); saveAndRefresh(); };
879
+ hdr.appendChild(hdrTitle); hdr.appendChild(delBtn);
880
+ block.appendChild(hdr);
881
+
882
+ const sec = (title) => {
883
+ const d = document.createElement('div');
884
+ d.style.cssText = 'font-size:11px;font-weight:700;color:#555;margin:8px 0 4px;padding-top:6px;border-top:1px solid #dde;clear:both;';
885
+ d.textContent = title; return d;
886
+ };
913
887
 
914
- const svgAttrSel = sel([
915
- ['fill','fill'],['color','color'],['fill-opacity','fill-opacity'],
916
- ['stroke-opacity','stroke-opacity'],['stroke-width','stroke-width'],
917
- ['stroke-dasharray','stroke-dasharray'],['stroke-dashoffset','stroke-dashoffset'],
918
- ['x','x'],['y','y'],['cx','cx'],['cy','cy'],['r','r'],['rx','rx'],['ry','ry']
919
- ], cfg.svgAttr || 'fill');
888
+ const effectSel = mkSel([
889
+ ['','— none —'],['opacity','Opacity'],['rotation','Rotation'],['scale','Scale'],
890
+ ['translateX','Move X (relative)'],['translateY','Move Y (relative)'],['translate','Move XY (relative)'],
891
+ ['left','Move Left (absolute)'],['top','Move Top (absolute)'],
892
+ ['skew','Skew'],['fill','Fill Color'],['transform','Transform (CSS)'],
893
+ ['svg','SVG Attribute'],['morphSVG','MorphSVG'],['motionPath','Motion Path']
894
+ ], cfg.effect || '', save);
895
+
896
+ const svgAttrSel = mkSel([
897
+ ['fill','fill'],['color','color'],['fill-opacity','fill-opacity'],
898
+ ['stroke-opacity','stroke-opacity'],['stroke-width','stroke-width'],
899
+ ['stroke-dasharray','stroke-dasharray'],['stroke-dashoffset','stroke-dashoffset'],
900
+ ['x','x'],['y','y'],['cx','cx'],['cy','cy'],['r','r'],['rx','rx'],['ry','ry']
901
+ ], cfg.svgAttr || 'fill', save);
902
+
903
+ const easeSel = mkSel([
904
+ ['none','none'],['power1.in','power1.in'],['power1.out','power1.out'],
905
+ ['power1.inOut','power1.inOut'],['power2.in','power2.in'],['power2.out','power2.out'],
906
+ ['power2.inOut','power2.inOut'],['power3.inOut','power3.inOut'],
907
+ ['bounce.out','bounce.out'],['elastic.out(1,0.3)','elastic.out'],
908
+ ['back.inOut(1.7)','back.inOut'],['circ.inOut','circ.inOut'],
909
+ ['expo.inOut','expo.inOut'],['sine.inOut','sine.inOut']
910
+ ], cfg.ease || 'power1.inOut', save);
911
+
912
+ const valueFromInp = mkInp(cfg.valueFrom, 'text', save);
913
+ const valueToInp = mkInp(cfg.valueTo, 'text', save);
914
+ const durationInp = mkInp(cfg.duration ?? 1, 'number', save);
915
+ const repeatInp = mkInp(cfg.repeat ?? 0, 'number', save);
916
+ const originXInp = mkInp(cfg.transformOriginX ?? '50', 'number', save);
917
+ const originYInp = mkInp(cfg.transformOriginY ?? '50', 'number', save);
918
+ const fillFromInp = mkInp(cfg.fillColorFrom || '#000000', 'color', save);
919
+ const fillToInp = mkInp(cfg.fillColorTo || '#ff0000', 'color', save);
920
+ const pathIdInp = mkInp(cfg.pathId, 'text', save);
921
+
922
+ const yoyoChk = mkChkField('yoyo', 'Yoyo', cfg.yoyo, cfg, save);
923
+ const alignToPathChk = mkChkField('alignToPath', 'Align to path', cfg.alignToPath, cfg, save);
924
+ const orientToPathChk= mkChkField('orientToPath', 'Auto-rotate', cfg.orientToPath, cfg, save);
925
+
926
+ const playCtrl = mkOidRow('▶ Play', controls.play || {}, save);
927
+ const pauseCtrl = mkOidRow('⏸ Pause', controls.pause || {}, save);
928
+ const resumeCtrl = mkOidRow('▶ Resume', controls.resume || {}, save);
929
+ const stopCtrl = mkOidRow('⏹ Stop', controls.stop || {}, save);
930
+ const reverseCtrl = mkOidRow('◀ Reverse', controls.reverse || {}, save);
931
+
932
+ // _collect: reads all UI values back into a config object
933
+ cfg._collect = () => {
934
+ const e = effectSel.value;
935
+ const c = {};
936
+ const addCtrl = (key, row) => { const v = row._getCtrl(); if (v.oid || v.oid_bind) c[key] = v; };
937
+ addCtrl('play', playCtrl); addCtrl('pause', pauseCtrl);
938
+ addCtrl('resume', resumeCtrl); addCtrl('stop', stopCtrl);
939
+ addCtrl('reverse', reverseCtrl);
940
+ const out = {};
941
+ if (e) out.effect = e;
942
+ if (e === 'svg' && svgAttrSel.value) out.svgAttr = svgAttrSel.value;
943
+ if (valueFromInp.value) out.valueFrom = valueFromInp.value;
944
+ if (valueToInp.value) out.valueTo = valueToInp.value;
945
+ out.duration = parseFloat(durationInp.value) || 1;
946
+ out.ease = easeSel.value || 'power1.inOut';
947
+ out.repeat = parseInt(repeatInp.value) || 0;
948
+ if (yoyoChk._checked()) out.yoyo = true;
949
+ if (['rotation','scale'].includes(e)) {
950
+ out.transformOriginX = originXInp.value;
951
+ out.transformOriginY = originYInp.value;
952
+ }
953
+ if (e === 'fill' || e === 'svg') {
954
+ if (fillFromInp.value) out.fillColorFrom = fillFromInp.value;
955
+ if (fillToInp.value) out.fillColorTo = fillToInp.value;
956
+ }
957
+ if (e === 'motionPath') {
958
+ if (pathIdInp.value) out.pathId = pathIdInp.value;
959
+ if (alignToPathChk._checked()) out.alignToPath = true;
960
+ if (orientToPathChk._checked()) out.orientToPath = true;
961
+ }
962
+ if (Object.keys(c).length) out.controls = c;
963
+ for (const [k, v] of Object.entries(cfg)) {
964
+ if (k.endsWith('_bind') && v) out[k] = v;
965
+ }
966
+ return out;
967
+ };
920
968
 
921
- const easeSel = sel([
922
- ['none','none'],['power1.in','power1.in'],['power1.out','power1.out'],
923
- ['power1.inOut','power1.inOut'],['power2.in','power2.in'],['power2.out','power2.out'],
924
- ['power2.inOut','power2.inOut'],['power3.inOut','power3.inOut'],
925
- ['bounce.out','bounce.out'],['elastic.out(1,0.3)','elastic.out'],
926
- ['back.inOut(1.7)','back.inOut'],['circ.inOut','circ.inOut'],
927
- ['expo.inOut','expo.inOut'],['sine.inOut','sine.inOut']
928
- ], cfg.ease || 'power1.inOut');
929
-
930
- const valueFromInp = inp(cfg.valueFrom, 'text');
931
- const valueToInp = inp(cfg.valueTo, 'text');
932
- const durationInp = inp(cfg.duration ?? 1, 'number');
933
- const repeatInp = inp(cfg.repeat ?? 0, 'number');
934
- const originXInp = inp(cfg.transformOriginX ?? '50', 'number');
935
- const originYInp = inp(cfg.transformOriginY ?? '50', 'number');
936
- const fillFromInp = inp(cfg.fillColorFrom || '#000000', 'color');
937
- const fillToInp = inp(cfg.fillColorTo || '#ff0000', 'color');
938
- const pathIdInp = inp(cfg.pathId, 'text');
939
-
940
- const yoyoChk = chkField('yoyo', 'Yoyo', cfg.yoyo);
941
- const alignToPathChk = chkField('alignToPath', 'Align to path', cfg.alignToPath);
942
- const orientToPathChk= chkField('orientToPath', 'Auto-rotate', cfg.orientToPath);
943
-
944
- const playCtrl = oidRow('▶ Play', controls.play || {});
945
- const pauseCtrl = oidRow('⏸ Pause', controls.pause || {});
946
- const resumeCtrl = oidRow(' Resume', controls.resume || {});
947
- const stopCtrl = oidRow('⏹ Stop', controls.stop || {});
948
- const reverseCtrl = oidRow('◀ Reverse', controls.reverse || {});
969
+ // Build block DOM
970
+ block.appendChild(sec('Effect'));
971
+ block.appendChild(mkField('Type', 'effect', effectSel, cfg));
972
+
973
+ const svgAttrRow = mkField('SVG Attr', 'svgAttr', svgAttrSel, cfg);
974
+ svgAttrRow.style.display = cfg.effect === 'svg' ? '' : 'none';
975
+ block.appendChild(svgAttrRow);
976
+ effectSel.addEventListener('change', () => { svgAttrRow.style.display = effectSel.value === 'svg' ? '' : 'none'; });
977
+
978
+ block.appendChild(mkField('Value From', 'valueFrom', valueFromInp, cfg));
979
+ block.appendChild(mkField('Value To', 'valueTo', valueToInp, cfg));
980
+
981
+ const colorRows = document.createElement('div');
982
+ colorRows.style.display = (cfg.effect === 'fill' || cfg.effect === 'svg') ? '' : 'none';
983
+ colorRows.appendChild(mkField('Color From', 'fillColorFrom', fillFromInp, cfg));
984
+ colorRows.appendChild(mkField('Color To', 'fillColorTo', fillToInp, cfg));
985
+ block.appendChild(colorRows);
986
+ effectSel.addEventListener('change', () => { colorRows.style.display = (effectSel.value === 'fill' || effectSel.value === 'svg') ? '' : 'none'; });
987
+
988
+ const motionRows = document.createElement('div');
989
+ motionRows.style.display = cfg.effect === 'motionPath' ? '' : 'none';
990
+ motionRows.appendChild(mkField('Path ID', 'pathId', pathIdInp, cfg));
991
+ motionRows.appendChild(alignToPathChk);
992
+ motionRows.appendChild(orientToPathChk);
993
+ block.appendChild(motionRows);
994
+ effectSel.addEventListener('change', () => { motionRows.style.display = effectSel.value === 'motionPath' ? '' : 'none'; });
995
+
996
+ block.appendChild(sec('Timing'));
997
+ block.appendChild(mkField('Duration (s)', 'duration', durationInp, cfg));
998
+ block.appendChild(mkField('Ease', 'ease', easeSel, cfg));
999
+ block.appendChild(mkField('Repeat', 'repeat', repeatInp, cfg));
1000
+ block.appendChild(yoyoChk);
1001
+
1002
+ const originRows = document.createElement('div');
1003
+ originRows.style.display = (cfg.effect === 'rotation' || cfg.effect === 'scale') ? '' : 'none';
1004
+ originRows.appendChild(sec('Transform Origin'));
1005
+ originRows.appendChild(mkField('Origin X (%)', 'transformOriginX', originXInp, cfg));
1006
+ originRows.appendChild(mkField('Origin Y (%)', 'transformOriginY', originYInp, cfg));
1007
+ block.appendChild(originRows);
1008
+ effectSel.addEventListener('change', () => { originRows.style.display = (effectSel.value === 'rotation' || effectSel.value === 'scale') ? '' : 'none'; });
1009
+
1010
+ block.appendChild(sec('Controls (OID triggers)'));
1011
+ block.appendChild(playCtrl); block.appendChild(pauseCtrl);
1012
+ block.appendChild(resumeCtrl); block.appendChild(stopCtrl);
1013
+ block.appendChild(reverseCtrl);
1014
+
1015
+ return block;
1016
+ };
949
1017
 
950
- // ── Build UI ──────────────────────────────────────────────────────────
1018
+ // ── Render ────────────────────────────────────────────────────────────
951
1019
  content.innerHTML = '';
952
1020
 
953
- const clearBtn = document.createElement('button');
954
- clearBtn.textContent = 'Clear';
955
- 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;';
956
- clearBtn.onclick = () => { designItem.removeAttribute('data-animation'); this._updateAnimationsPanel(); };
957
- content.appendChild(clearBtn);
1021
+ const clearAllBtn = document.createElement('button');
1022
+ clearAllBtn.textContent = 'Clear All';
1023
+ clearAllBtn.style.cssText = 'float:right;font-size:10px;padding:2px 6px;cursor:pointer;border:1px solid #ccc;border-radius:3px;background:#fff;margin-bottom:8px;';
1024
+ clearAllBtn.onclick = () => { designItem.removeAttribute('data-animation'); this._updateAnimationsPanel(); };
1025
+ content.appendChild(clearAllBtn);
1026
+
1027
+ if (cfgList.length === 0) {
1028
+ const hint = document.createElement('p');
1029
+ hint.style.cssText = 'color:#999;font-style:italic;font-size:11px;';
1030
+ hint.textContent = 'No animations. Click "+ Add Animation" to start.';
1031
+ content.appendChild(hint);
1032
+ }
958
1033
 
959
- const sec = (title) => {
960
- const d = document.createElement('div');
961
- 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;';
962
- d.textContent = title;
963
- return d;
964
- };
1034
+ cfgList.forEach((cfg, i) => content.appendChild(buildAnimBlock(cfg, i)));
965
1035
 
966
- content.appendChild(sec('Effect'));
967
- content.appendChild(field('Type', 'effect', effectSel));
968
-
969
- const svgAttrRow = field('SVG Attr', 'svgAttr', svgAttrSel);
970
- svgAttrRow.style.display = cfg.effect === 'svg' ? '' : 'none';
971
- content.appendChild(svgAttrRow);
972
- effectSel.addEventListener('change', () => { svgAttrRow.style.display = effectSel.value === 'svg' ? '' : 'none'; });
973
-
974
- content.appendChild(field('Value From', 'valueFrom', valueFromInp));
975
- content.appendChild(field('Value To', 'valueTo', valueToInp));
976
-
977
- const colorRows = document.createElement('div');
978
- colorRows.style.display = (cfg.effect === 'fill' || cfg.effect === 'svg') ? '' : 'none';
979
- colorRows.appendChild(field('Color From', 'fillColorFrom', fillFromInp));
980
- colorRows.appendChild(field('Color To', 'fillColorTo', fillToInp));
981
- content.appendChild(colorRows);
982
- effectSel.addEventListener('change', () => { colorRows.style.display = (effectSel.value === 'fill' || effectSel.value === 'svg') ? '' : 'none'; });
983
-
984
- const motionRows = document.createElement('div');
985
- motionRows.style.display = cfg.effect === 'motionPath' ? '' : 'none';
986
- motionRows.appendChild(field('Path ID', 'pathId', pathIdInp));
987
- motionRows.appendChild(alignToPathChk);
988
- motionRows.appendChild(orientToPathChk);
989
- content.appendChild(motionRows);
990
- effectSel.addEventListener('change', () => { motionRows.style.display = effectSel.value === 'motionPath' ? '' : 'none'; });
991
-
992
- content.appendChild(sec('Timing'));
993
- content.appendChild(field('Duration (s)', 'duration', durationInp));
994
- content.appendChild(field('Ease', 'ease', easeSel));
995
- content.appendChild(field('Repeat', 'repeat', repeatInp));
996
- content.appendChild(yoyoChk);
997
-
998
- const originRows = document.createElement('div');
999
- originRows.style.display = (cfg.effect === 'rotation' || cfg.effect === 'scale') ? '' : 'none';
1000
- originRows.appendChild(sec('Transform Origin'));
1001
- originRows.appendChild(field('Origin X (%)', 'transformOriginX', originXInp));
1002
- originRows.appendChild(field('Origin Y (%)', 'transformOriginY', originYInp));
1003
- content.appendChild(originRows);
1004
- effectSel.addEventListener('change', () => { originRows.style.display = (effectSel.value === 'rotation' || effectSel.value === 'scale') ? '' : 'none'; });
1005
-
1006
- content.appendChild(sec('Controls (OID triggers)'));
1007
- content.appendChild(playCtrl);
1008
- content.appendChild(pauseCtrl);
1009
- content.appendChild(resumeCtrl);
1010
- content.appendChild(stopCtrl);
1011
- content.appendChild(reverseCtrl);
1036
+ const addBtn = document.createElement('button');
1037
+ addBtn.textContent = '+ Add Animation';
1038
+ addBtn.style.cssText = 'width:100%;margin-top:6px;padding:5px;font-size:12px;cursor:pointer;border:1px solid #5a9;border-radius:4px;background:#e8f8f0;color:#2a7a5a;font-weight:600;';
1039
+ addBtn.onclick = () => { cfgList.push({}); this._updateAnimationsPanel(); };
1040
+ content.appendChild(addBtn);
1012
1041
  }
1013
1042
 
1014
1043
  // ─── Effects Panel ───────────────────────────────────────────────────────
@@ -95,17 +95,39 @@ function buildTweenConfig(cfg, value) {
95
95
  case 'translateX':
96
96
  config.x = cfg.valueTo != null ? parseFloat(cfg.valueTo)
97
97
  : (typeof normalVal === 'boolean' ? (normalVal ? 100 : 0) : numVal);
98
+ if (cfg.valueFrom != null) config.startAt = { x: parseFloat(cfg.valueFrom) };
98
99
  break;
99
100
  case 'translateY':
100
101
  config.y = cfg.valueTo != null ? parseFloat(cfg.valueTo)
101
102
  : (typeof normalVal === 'boolean' ? (normalVal ? 100 : 0) : numVal);
103
+ if (cfg.valueFrom != null) config.startAt = { y: parseFloat(cfg.valueFrom) };
102
104
  break;
103
105
  case 'translate': {
104
- const tv = cfg.valueTo != null ? parseFloat(cfg.valueTo)
105
- : (typeof normalVal === 'boolean' ? (normalVal ? 100 : 0) : numVal);
106
- config.x = tv; config.y = tv;
106
+ // valueTo / valueFrom: "x,y" or single number (used for x, y=0)
107
+ const parseXY = (v, defVal) => {
108
+ if (v == null) return { x: defVal, y: 0 };
109
+ const s = String(v);
110
+ const parts = s.split(',');
111
+ return { x: parseFloat(parts[0]) || 0, y: parts.length > 1 ? parseFloat(parts[1]) || 0 : 0 };
112
+ };
113
+ const to = parseXY(cfg.valueTo, typeof normalVal === 'boolean' ? (normalVal ? 100 : 0) : numVal);
114
+ config.x = to.x; config.y = to.y;
115
+ if (cfg.valueFrom != null) {
116
+ const from = parseXY(cfg.valueFrom, 0);
117
+ config.startAt = { x: from.x, y: from.y };
118
+ }
107
119
  break;
108
120
  }
121
+ case 'left':
122
+ config.left = cfg.valueTo != null ? cfg.valueTo
123
+ : (typeof normalVal === 'boolean' ? (normalVal ? '100px' : '0px') : (numVal + 'px'));
124
+ if (cfg.valueFrom != null) config.startAt = { left: isNaN(cfg.valueFrom) ? cfg.valueFrom : cfg.valueFrom + 'px' };
125
+ break;
126
+ case 'top':
127
+ config.top = cfg.valueTo != null ? cfg.valueTo
128
+ : (typeof normalVal === 'boolean' ? (normalVal ? '100px' : '0px') : (numVal + 'px'));
129
+ if (cfg.valueFrom != null) config.startAt = { top: isNaN(cfg.valueFrom) ? cfg.valueFrom : cfg.valueFrom + 'px' };
130
+ break;
109
131
  case 'skew': {
110
132
  const sv = cfg.valueTo != null ? parseFloat(cfg.valueTo)
111
133
  : (typeof normalVal === 'boolean' ? (normalVal ? 45 : 0) : numVal);
@@ -298,12 +320,19 @@ export async function scanAndApplyAnimations(root) {
298
320
  const elements = (root || document).querySelectorAll('[data-animation]');
299
321
  for (const el of elements) {
300
322
  try {
301
- const cfg = JSON.parse(el.getAttribute('data-animation'));
302
- const existing = _activeAnimations.get(el);
303
- if (existing) existing.destroy();
304
- const inst = new AnimationInstance(el, cfg);
305
- await inst.init();
306
- _activeAnimations.set(el, inst);
323
+ const raw = JSON.parse(el.getAttribute('data-animation'));
324
+ // Support both single object and array of animations
325
+ const cfgList = Array.isArray(raw) ? raw : [raw];
326
+ const existing = _activeAnimations.get(el) || [];
327
+ for (const inst of existing) inst.destroy();
328
+ const instances = [];
329
+ for (const cfg of cfgList) {
330
+ if (!cfg || typeof cfg !== 'object') continue;
331
+ const inst = new AnimationInstance(el, cfg);
332
+ await inst.init();
333
+ instances.push(inst);
334
+ }
335
+ _activeAnimations.set(el, instances);
307
336
  } catch (e) {
308
337
  console.warn('[AnimationService] data-animation parse error on element:', el, e);
309
338
  }
@@ -313,8 +342,9 @@ export async function scanAndApplyAnimations(root) {
313
342
  export function cleanupAnimations(root) {
314
343
  const elements = (root || document).querySelectorAll('[data-animation]');
315
344
  for (const el of elements) {
316
- const inst = _activeAnimations.get(el);
317
- if (inst) { inst.destroy(); _activeAnimations.delete(el); }
345
+ const instances = _activeAnimations.get(el) || [];
346
+ for (const inst of instances) inst.destroy();
347
+ _activeAnimations.delete(el);
318
348
  }
319
349
  }
320
350