iobroker.mywebui 1.37.60 → 1.37.62
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/package.json +1 -1
- package/www/dist/frontend/config/IobrokerWebuiAppShell.js +526 -22
- package/www/dist/frontend/runtime/AnimationService.js +354 -0
- package/www/dist/frontend/runtime/ScreenViewer.js +7 -0
- package/www/dist/vendor/gsap/MorphSVGPlugin.min.js +11 -0
- package/www/dist/vendor/gsap/MotionPathPlugin.min.js +11 -0
- package/www/dist/vendor/gsap/gsap.min.js +11 -0
package/package.json
CHANGED
|
@@ -106,6 +106,12 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
|
|
|
106
106
|
<div id="visibilityDock" title="Visibility" style="overflow: auto; width: 100%;" dock-spawn-dock-to="attributeDock">
|
|
107
107
|
</div>
|
|
108
108
|
|
|
109
|
+
<div id="effectsDock" title="Effects" style="overflow: auto; width: 100%;" dock-spawn-dock-to="attributeDock">
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div id="animationsDock" title="Animations" style="overflow: auto; width: 100%;" dock-spawn-dock-to="attributeDock">
|
|
113
|
+
</div>
|
|
114
|
+
|
|
109
115
|
<div id="translationDock" title="Translations" style="overflow: hidden; width: 100%; height: 100%;" dock-spawn-dock-to="attributeDock">
|
|
110
116
|
<iobroker-webui-translation-editor id="translationEditor"></iobroker-webui-translation-editor>
|
|
111
117
|
</div>
|
|
@@ -282,30 +288,26 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
|
|
|
282
288
|
this._solutionExplorer.initialize(serviceContainer);
|
|
283
289
|
this.propertyGrid.serviceContainer = serviceContainer;
|
|
284
290
|
|
|
285
|
-
// Setup
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
setTimeout(
|
|
296
|
-
|
|
297
|
-
console.error('❌ [Visibility] Failed to find visibilityDock after 15 retries');
|
|
298
|
-
}
|
|
299
|
-
} catch (err) {
|
|
300
|
-
if (retries < 15) {
|
|
301
|
-
retries++;
|
|
302
|
-
setTimeout(setupPanel, 300);
|
|
303
|
-
} else {
|
|
304
|
-
console.error('❌ [Visibility] Setup error:', err);
|
|
291
|
+
// Setup side panels after DOM is fully ready - with retries
|
|
292
|
+
const makeSetup = (dockId, setupFn, label) => {
|
|
293
|
+
let retries = 0;
|
|
294
|
+
const run = () => {
|
|
295
|
+
try {
|
|
296
|
+
const dock = this._getDomElement(dockId);
|
|
297
|
+
if (dock) { setupFn(); }
|
|
298
|
+
else if (retries < 15) { retries++; setTimeout(run, 300); }
|
|
299
|
+
else { console.error(`❌ [${label}] dock not found after 15 retries`); }
|
|
300
|
+
} catch (err) {
|
|
301
|
+
if (retries < 15) { retries++; setTimeout(run, 300); }
|
|
302
|
+
else { console.error(`❌ [${label}] setup error:`, err); }
|
|
305
303
|
}
|
|
306
|
-
}
|
|
304
|
+
};
|
|
305
|
+
return run;
|
|
307
306
|
};
|
|
308
|
-
|
|
307
|
+
|
|
308
|
+
setTimeout(makeSetup('visibilityDock', () => this._setupVisibilityPanel(), 'Visibility'), 1000);
|
|
309
|
+
setTimeout(makeSetup('animationsDock', () => this._setupAnimationsPanel(), 'Animations'), 1200);
|
|
310
|
+
setTimeout(makeSetup('effectsDock', () => this._setupEffectsPanel(), 'Effects'), 1400);
|
|
309
311
|
}
|
|
310
312
|
|
|
311
313
|
_setupVisibilityPanel() {
|
|
@@ -648,6 +650,508 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
|
|
|
648
650
|
actionDiv.appendChild(actionSelect);
|
|
649
651
|
content.appendChild(actionDiv);
|
|
650
652
|
}
|
|
653
|
+
|
|
654
|
+
// ─── Animations Panel ────────────────────────────────────────────────────
|
|
655
|
+
|
|
656
|
+
_setupAnimationsPanel() {
|
|
657
|
+
const dock = this._getDomElement('animationsDock');
|
|
658
|
+
if (!dock) return;
|
|
659
|
+
if (dock.querySelector('#anim-panel-container')) return;
|
|
660
|
+
|
|
661
|
+
const panel = document.createElement('div');
|
|
662
|
+
panel.id = 'anim-panel-container';
|
|
663
|
+
panel.style.cssText = 'padding:12px;';
|
|
664
|
+
panel.innerHTML = `
|
|
665
|
+
<h3 style="margin:0 0 12px 0;font-size:14px;font-weight:bold;color:#333;">🎬 Animation</h3>
|
|
666
|
+
<div id="anim-panel-content" style="font-size:12px;"><p style="color:#999;font-style:italic;">Select an element...</p></div>
|
|
667
|
+
`;
|
|
668
|
+
dock.appendChild(panel);
|
|
669
|
+
|
|
670
|
+
let lastEl = null;
|
|
671
|
+
const poll = () => {
|
|
672
|
+
try {
|
|
673
|
+
const items = this.propertyGrid?.propertyGrid?._selectedItems;
|
|
674
|
+
const el = items?.length === 1 ? items[0].element : null;
|
|
675
|
+
if (el !== lastEl) { lastEl = el; this._updateAnimationsPanel(); }
|
|
676
|
+
} catch (e) {}
|
|
677
|
+
};
|
|
678
|
+
setInterval(poll, 500);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
_updateAnimationsPanel() {
|
|
682
|
+
const dock = this._getDomElement('animationsDock');
|
|
683
|
+
if (!dock) return;
|
|
684
|
+
const content = dock.querySelector('#anim-panel-content');
|
|
685
|
+
if (!content) return;
|
|
686
|
+
|
|
687
|
+
const items = this.propertyGrid?.propertyGrid?._selectedItems;
|
|
688
|
+
if (!items || items.length !== 1) {
|
|
689
|
+
content.innerHTML = '<p style="color:#999;font-style:italic;">Select an element to configure animations...</p>';
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const designItem = items[0];
|
|
694
|
+
const element = designItem.element;
|
|
695
|
+
|
|
696
|
+
let cfg = {};
|
|
697
|
+
try { cfg = JSON.parse(element.getAttribute('data-animation') || '{}'); } catch (e) {}
|
|
698
|
+
const controls = cfg.controls || {};
|
|
699
|
+
|
|
700
|
+
const save = () => {
|
|
701
|
+
const newCfg = collectAnimCfg();
|
|
702
|
+
if (Object.keys(newCfg).length === 0 || (!newCfg.effect && !newCfg.controls)) {
|
|
703
|
+
designItem.removeAttribute('data-animation');
|
|
704
|
+
} else {
|
|
705
|
+
designItem.setAttribute('data-animation', JSON.stringify(newCfg));
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
const field = (label, inputEl) => {
|
|
710
|
+
const row = document.createElement('div');
|
|
711
|
+
row.style.cssText = 'display:flex;align-items:center;gap:6px;margin-bottom:6px;';
|
|
712
|
+
const lbl = document.createElement('span');
|
|
713
|
+
lbl.textContent = label;
|
|
714
|
+
lbl.style.cssText = 'min-width:90px;font-size:11px;color:#555;';
|
|
715
|
+
row.appendChild(lbl);
|
|
716
|
+
row.appendChild(inputEl);
|
|
717
|
+
return row;
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
const inp = (val, type = 'text', width = '100%') => {
|
|
721
|
+
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;
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
const sel = (options, val) => {
|
|
730
|
+
const s = document.createElement('select');
|
|
731
|
+
s.style.cssText = 'flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;';
|
|
732
|
+
options.forEach(([v, t]) => {
|
|
733
|
+
const o = document.createElement('option');
|
|
734
|
+
o.value = v; o.textContent = t; o.selected = v === val;
|
|
735
|
+
s.appendChild(o);
|
|
736
|
+
});
|
|
737
|
+
s.onchange = save;
|
|
738
|
+
return s;
|
|
739
|
+
};
|
|
740
|
+
|
|
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;';
|
|
744
|
+
const c = document.createElement('input');
|
|
745
|
+
c.type = 'checkbox'; c.checked = val === true || val === 'true';
|
|
746
|
+
c.onchange = save;
|
|
747
|
+
wrap.appendChild(c);
|
|
748
|
+
wrap.appendChild(document.createTextNode(label));
|
|
749
|
+
return wrap;
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
const oidRow = (label, ctrlKey, ctrlCfg) => {
|
|
753
|
+
const wrap = document.createElement('div');
|
|
754
|
+
wrap.style.cssText = 'border:1px solid #eee;border-radius:3px;padding:6px;margin-bottom:6px;background:#fafafa;';
|
|
755
|
+
|
|
756
|
+
const head = document.createElement('div');
|
|
757
|
+
head.style.cssText = 'font-size:11px;font-weight:600;color:#444;margin-bottom:5px;';
|
|
758
|
+
head.textContent = label;
|
|
759
|
+
wrap.appendChild(head);
|
|
760
|
+
|
|
761
|
+
const oidInp = document.createElement('input');
|
|
762
|
+
oidInp.type = 'text';
|
|
763
|
+
oidInp.value = ctrlCfg.oid || '';
|
|
764
|
+
oidInp.placeholder = 'OID…';
|
|
765
|
+
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
|
+
oidInp.onchange = save;
|
|
767
|
+
|
|
768
|
+
const pickBtn = document.createElement('button');
|
|
769
|
+
pickBtn.textContent = '…';
|
|
770
|
+
pickBtn.style.cssText = 'padding:2px 6px;font-size:11px;cursor:pointer;border:1px solid #aaa;border-radius:3px;margin-left:3px;';
|
|
771
|
+
pickBtn.onclick = async () => {
|
|
772
|
+
const picked = await openSelectIdDialog(document.body, { id: oidInp.value });
|
|
773
|
+
if (picked) { oidInp.value = picked; save(); }
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
const oidRow2 = document.createElement('div');
|
|
777
|
+
oidRow2.style.cssText = 'display:flex;margin-bottom:3px;';
|
|
778
|
+
oidRow2.appendChild(oidInp);
|
|
779
|
+
oidRow2.appendChild(pickBtn);
|
|
780
|
+
wrap.appendChild(oidRow2);
|
|
781
|
+
|
|
782
|
+
const condSel = sel([
|
|
783
|
+
['equal','='], ['not_equal','≠'], ['less_than','<'], ['less_equal','≤'],
|
|
784
|
+
['greater_than','>'], ['greater_equal','≥'], ['exists','exists']
|
|
785
|
+
], ctrlCfg.condition || 'equal');
|
|
786
|
+
condSel.style.cssText += 'width:70px;flex:none;margin-right:4px;';
|
|
787
|
+
const valInp = document.createElement('input');
|
|
788
|
+
valInp.type = 'text'; valInp.value = ctrlCfg.value ?? 'true';
|
|
789
|
+
valInp.placeholder = 'value';
|
|
790
|
+
valInp.style.cssText = 'flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;';
|
|
791
|
+
condSel.onchange = save; valInp.onchange = save;
|
|
792
|
+
|
|
793
|
+
const condRow = document.createElement('div');
|
|
794
|
+
condRow.style.cssText = 'display:flex;gap:3px;';
|
|
795
|
+
condRow.appendChild(condSel); condRow.appendChild(valInp);
|
|
796
|
+
wrap.appendChild(condRow);
|
|
797
|
+
|
|
798
|
+
wrap._getCtrl = () => ({
|
|
799
|
+
oid: oidInp.value || undefined,
|
|
800
|
+
condition: condSel.value,
|
|
801
|
+
value: valInp.value
|
|
802
|
+
});
|
|
803
|
+
return wrap;
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
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']
|
|
812
|
+
], cfg.effect || '');
|
|
813
|
+
|
|
814
|
+
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']
|
|
820
|
+
], cfg.svgAttr || 'fill');
|
|
821
|
+
|
|
822
|
+
const easeSel = sel([
|
|
823
|
+
['none','none'],['power1.in','power1.in'],['power1.out','power1.out'],
|
|
824
|
+
['power1.inOut','power1.inOut'],['power2.in','power2.in'],['power2.out','power2.out'],
|
|
825
|
+
['power2.inOut','power2.inOut'],['power3.inOut','power3.inOut'],
|
|
826
|
+
['bounce.out','bounce.out'],['elastic.out(1,0.3)','elastic.out'],
|
|
827
|
+
['back.inOut(1.7)','back.inOut'],['circ.inOut','circ.inOut'],
|
|
828
|
+
['expo.inOut','expo.inOut'],['sine.inOut','sine.inOut']
|
|
829
|
+
], cfg.ease || 'power1.inOut');
|
|
830
|
+
|
|
831
|
+
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
|
|
888
|
+
content.innerHTML = '';
|
|
889
|
+
|
|
890
|
+
// Clear button
|
|
891
|
+
const clearBtn = document.createElement('button');
|
|
892
|
+
clearBtn.textContent = 'Clear Animation';
|
|
893
|
+
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
|
+
clearBtn.onclick = () => { designItem.removeAttribute('data-animation'); this._updateAnimationsPanel(); };
|
|
895
|
+
content.appendChild(clearBtn);
|
|
896
|
+
|
|
897
|
+
const sec = (title) => {
|
|
898
|
+
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;';
|
|
900
|
+
d.textContent = title;
|
|
901
|
+
return d;
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
content.appendChild(sec('Effect'));
|
|
905
|
+
content.appendChild(field('Type', effectSel));
|
|
906
|
+
|
|
907
|
+
const svgAttrRow = field('SVG Attr', svgAttrSel);
|
|
908
|
+
svgAttrRow.style.display = cfg.effect === 'svg' ? '' : 'none';
|
|
909
|
+
content.appendChild(svgAttrRow);
|
|
910
|
+
effectSel.addEventListener('change', () => {
|
|
911
|
+
svgAttrRow.style.display = effectSel.value === 'svg' ? '' : 'none';
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
content.appendChild(field('Value From', valueFromInp));
|
|
915
|
+
content.appendChild(field('Value To', valueToInp));
|
|
916
|
+
|
|
917
|
+
const colorRows = document.createElement('div');
|
|
918
|
+
colorRows.style.display = (cfg.effect === 'fill' || cfg.effect === 'svg') ? '' : 'none';
|
|
919
|
+
colorRows.appendChild(field('Color From', fillFromInp));
|
|
920
|
+
colorRows.appendChild(field('Color To', fillToInp));
|
|
921
|
+
content.appendChild(colorRows);
|
|
922
|
+
effectSel.addEventListener('change', () => {
|
|
923
|
+
colorRows.style.display = (effectSel.value === 'fill' || effectSel.value === 'svg') ? '' : 'none';
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
const motionRows = document.createElement('div');
|
|
927
|
+
motionRows.style.display = cfg.effect === 'motionPath' ? '' : 'none';
|
|
928
|
+
motionRows.appendChild(field('Path ID', pathIdInp));
|
|
929
|
+
motionRows.appendChild(alignToPathChk);
|
|
930
|
+
motionRows.appendChild(orientToPathChk);
|
|
931
|
+
content.appendChild(motionRows);
|
|
932
|
+
effectSel.addEventListener('change', () => {
|
|
933
|
+
motionRows.style.display = effectSel.value === 'motionPath' ? '' : 'none';
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
content.appendChild(sec('Timing'));
|
|
937
|
+
content.appendChild(field('Duration (s)', durationInp));
|
|
938
|
+
content.appendChild(field('Ease', easeSel));
|
|
939
|
+
content.appendChild(field('Repeat', repeatInp));
|
|
940
|
+
content.appendChild(yoyoChk);
|
|
941
|
+
|
|
942
|
+
const originRows = document.createElement('div');
|
|
943
|
+
originRows.style.display = (cfg.effect === 'rotation' || cfg.effect === 'scale') ? '' : 'none';
|
|
944
|
+
originRows.appendChild(sec('Transform Origin'));
|
|
945
|
+
originRows.appendChild(field('Origin X (%)', originXInp));
|
|
946
|
+
originRows.appendChild(field('Origin Y (%)', originYInp));
|
|
947
|
+
content.appendChild(originRows);
|
|
948
|
+
effectSel.addEventListener('change', () => {
|
|
949
|
+
originRows.style.display = (effectSel.value === 'rotation' || effectSel.value === 'scale') ? '' : 'none';
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
content.appendChild(sec('Controls (OID bindings)'));
|
|
953
|
+
content.appendChild(playCtrl);
|
|
954
|
+
content.appendChild(pauseCtrl);
|
|
955
|
+
content.appendChild(resumeCtrl);
|
|
956
|
+
content.appendChild(stopCtrl);
|
|
957
|
+
content.appendChild(reverseCtrl);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// ─── Effects Panel ───────────────────────────────────────────────────────
|
|
961
|
+
|
|
962
|
+
_setupEffectsPanel() {
|
|
963
|
+
const dock = this._getDomElement('effectsDock');
|
|
964
|
+
if (!dock) return;
|
|
965
|
+
if (dock.querySelector('#effects-panel-container')) return;
|
|
966
|
+
|
|
967
|
+
const panel = document.createElement('div');
|
|
968
|
+
panel.id = 'effects-panel-container';
|
|
969
|
+
panel.style.cssText = 'padding:12px;';
|
|
970
|
+
panel.innerHTML = `
|
|
971
|
+
<h3 style="margin:0 0 12px 0;font-size:14px;font-weight:bold;color:#333;">✨ Effect</h3>
|
|
972
|
+
<div id="effects-panel-content" style="font-size:12px;"><p style="color:#999;font-style:italic;">Select an element...</p></div>
|
|
973
|
+
`;
|
|
974
|
+
dock.appendChild(panel);
|
|
975
|
+
|
|
976
|
+
let lastEl = null;
|
|
977
|
+
const poll = () => {
|
|
978
|
+
try {
|
|
979
|
+
const items = this.propertyGrid?.propertyGrid?._selectedItems;
|
|
980
|
+
const el = items?.length === 1 ? items[0].element : null;
|
|
981
|
+
if (el !== lastEl) { lastEl = el; this._updateEffectsPanel(); }
|
|
982
|
+
} catch (e) {}
|
|
983
|
+
};
|
|
984
|
+
setInterval(poll, 500);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
_updateEffectsPanel() {
|
|
988
|
+
const dock = this._getDomElement('effectsDock');
|
|
989
|
+
if (!dock) return;
|
|
990
|
+
const content = dock.querySelector('#effects-panel-content');
|
|
991
|
+
if (!content) return;
|
|
992
|
+
|
|
993
|
+
const items = this.propertyGrid?.propertyGrid?._selectedItems;
|
|
994
|
+
if (!items || items.length !== 1) {
|
|
995
|
+
content.innerHTML = '<p style="color:#999;font-style:italic;">Select an element to configure effects...</p>';
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const designItem = items[0];
|
|
1000
|
+
const element = designItem.element;
|
|
1001
|
+
|
|
1002
|
+
let cfg = {};
|
|
1003
|
+
try { cfg = JSON.parse(element.getAttribute('data-effects') || '{}'); } catch (e) {}
|
|
1004
|
+
|
|
1005
|
+
const save = () => {
|
|
1006
|
+
const c = collectEffectsCfg();
|
|
1007
|
+
if (!c.type) designItem.removeAttribute('data-effects');
|
|
1008
|
+
else designItem.setAttribute('data-effects', JSON.stringify(c));
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
const field = (label, inputEl) => {
|
|
1012
|
+
const row = document.createElement('div');
|
|
1013
|
+
row.style.cssText = 'display:flex;align-items:center;gap:6px;margin-bottom:6px;';
|
|
1014
|
+
const lbl = document.createElement('span');
|
|
1015
|
+
lbl.textContent = label;
|
|
1016
|
+
lbl.style.cssText = 'min-width:80px;font-size:11px;color:#555;';
|
|
1017
|
+
row.appendChild(lbl); row.appendChild(inputEl);
|
|
1018
|
+
return row;
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
const inp = (val, type = 'text') => {
|
|
1022
|
+
const i = document.createElement('input');
|
|
1023
|
+
i.type = type; i.value = val ?? '';
|
|
1024
|
+
i.style.cssText = 'flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;';
|
|
1025
|
+
i.onchange = save; return i;
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
const sel = (options, val) => {
|
|
1029
|
+
const s = document.createElement('select');
|
|
1030
|
+
s.style.cssText = 'flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;';
|
|
1031
|
+
options.forEach(([v, t]) => {
|
|
1032
|
+
const o = document.createElement('option');
|
|
1033
|
+
o.value = v; o.textContent = t; o.selected = v === val;
|
|
1034
|
+
s.appendChild(o);
|
|
1035
|
+
});
|
|
1036
|
+
s.onchange = save; return s;
|
|
1037
|
+
};
|
|
1038
|
+
|
|
1039
|
+
const typeSel = sel([
|
|
1040
|
+
['','— none —'],['fadeIn','Fade In'],['fadeOut','Fade Out'],
|
|
1041
|
+
['slideInLeft','Slide In Left'],['slideInRight','Slide In Right'],
|
|
1042
|
+
['slideInTop','Slide In Top'],['slideInBottom','Slide In Bottom'],
|
|
1043
|
+
['bounce','Bounce'],['pulse','Pulse'],['shake','Shake'],
|
|
1044
|
+
['glow','Glow'],['blur','Blur'],['spin','Spin'],['flip','Flip 3D']
|
|
1045
|
+
], cfg.type || '');
|
|
1046
|
+
|
|
1047
|
+
const triggerSel = sel([
|
|
1048
|
+
['load','On Load'],['hover','On Hover'],['click','On Click'],['oid','OID State']
|
|
1049
|
+
], cfg.trigger || 'load');
|
|
1050
|
+
|
|
1051
|
+
const durationInp = inp(cfg.duration ?? 0.5, 'number');
|
|
1052
|
+
const delayInp = inp(cfg.delay ?? 0, 'number');
|
|
1053
|
+
const repeatInp = inp(cfg.repeat ?? 0, 'number');
|
|
1054
|
+
|
|
1055
|
+
const easeSel = sel([
|
|
1056
|
+
['power2.out','power2.out'],['power2.in','power2.in'],['power2.inOut','power2.inOut'],
|
|
1057
|
+
['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']
|
|
1060
|
+
], cfg.ease || 'power2.out');
|
|
1061
|
+
|
|
1062
|
+
const oidInp = inp(cfg.oid || '');
|
|
1063
|
+
const condSel = sel([
|
|
1064
|
+
['equal','='],['not_equal','≠'],['less_than','<'],['greater_than','>'],['exists','exists']
|
|
1065
|
+
], cfg.condition || 'equal');
|
|
1066
|
+
const condValInp = inp(cfg.conditionValue ?? 'true');
|
|
1067
|
+
|
|
1068
|
+
const glowColorInp = inp(cfg.glowColor || 'yellow', 'color');
|
|
1069
|
+
const glowSizeInp = inp(cfg.glowSize || 10, 'number');
|
|
1070
|
+
const blurInp = inp(cfg.blurAmount || 5, 'number');
|
|
1071
|
+
|
|
1072
|
+
const oidPickBtn = document.createElement('button');
|
|
1073
|
+
oidPickBtn.textContent = '…';
|
|
1074
|
+
oidPickBtn.style.cssText = 'padding:2px 6px;font-size:11px;cursor:pointer;border:1px solid #aaa;border-radius:3px;';
|
|
1075
|
+
oidPickBtn.onclick = async () => {
|
|
1076
|
+
const picked = await openSelectIdDialog(document.body, { id: oidInp.value });
|
|
1077
|
+
if (picked) { oidInp.value = picked; save(); }
|
|
1078
|
+
};
|
|
1079
|
+
|
|
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
|
+
|
|
1101
|
+
content.innerHTML = '';
|
|
1102
|
+
|
|
1103
|
+
const clearBtn = document.createElement('button');
|
|
1104
|
+
clearBtn.textContent = 'Clear Effect';
|
|
1105
|
+
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
|
+
clearBtn.onclick = () => { designItem.removeAttribute('data-effects'); this._updateEffectsPanel(); };
|
|
1107
|
+
content.appendChild(clearBtn);
|
|
1108
|
+
|
|
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));
|
|
1115
|
+
|
|
1116
|
+
const oidSection = document.createElement('div');
|
|
1117
|
+
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
|
+
const oidLabel = document.createElement('div');
|
|
1124
|
+
oidLabel.style.cssText = 'font-size:11px;font-weight:600;color:#555;margin-bottom:3px;';
|
|
1125
|
+
oidLabel.textContent = 'OID';
|
|
1126
|
+
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);
|
|
1132
|
+
content.appendChild(oidSection);
|
|
1133
|
+
triggerSel.addEventListener('change', () => {
|
|
1134
|
+
oidSection.style.display = triggerSel.value === 'oid' ? '' : 'none';
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
const glowSection = document.createElement('div');
|
|
1138
|
+
glowSection.style.display = cfg.type === 'glow' ? '' : 'none';
|
|
1139
|
+
glowSection.appendChild(field('Glow Color', glowColorInp));
|
|
1140
|
+
glowSection.appendChild(field('Glow Size (px)', glowSizeInp));
|
|
1141
|
+
content.appendChild(glowSection);
|
|
1142
|
+
typeSel.addEventListener('change', () => {
|
|
1143
|
+
glowSection.style.display = typeSel.value === 'glow' ? '' : 'none';
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
const blurSection = document.createElement('div');
|
|
1147
|
+
blurSection.style.display = cfg.type === 'blur' ? '' : 'none';
|
|
1148
|
+
blurSection.appendChild(field('Blur (px)', blurInp));
|
|
1149
|
+
content.appendChild(blurSection);
|
|
1150
|
+
typeSel.addEventListener('change', () => {
|
|
1151
|
+
blurSection.style.display = typeSel.value === 'blur' ? '' : 'none';
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
|
|
651
1155
|
/* Move to a Dock Spawn Helper */
|
|
652
1156
|
activateDockById(name) {
|
|
653
1157
|
this.activateDock(this._getDomElement(name));
|