draply-dev 1.5.2 → 1.5.4
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/bin/cli.js +5 -2
- package/package.json +1 -1
- package/src/overlay.js +1080 -1008
package/src/overlay.js
CHANGED
|
@@ -824,1104 +824,1176 @@
|
|
|
824
824
|
const subLay = document.getElementById('__sub_lay__');
|
|
825
825
|
if (subLay) subLay.classList.remove('v');
|
|
826
826
|
}
|
|
827
|
-
|
|
827
|
+
|
|
828
828
|
|
|
829
829
|
// ── HOVER ────────────────────────────────────────────────────────────────
|
|
830
830
|
document.addEventListener('mouseover', e => {
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
} else {
|
|
872
|
-
// Add to selection
|
|
873
|
-
state.selectedEls.push(t);
|
|
874
|
-
t.classList.add('__ps_multi__');
|
|
875
|
-
// If no primary yet, make this primary
|
|
876
|
-
if (!state.selectedEl) {
|
|
877
|
-
state.selectedEl = t;
|
|
878
|
-
t.classList.remove('__ps_multi__');
|
|
879
|
-
t.classList.add('__ps__');
|
|
831
|
+
if (!state.tool || ps(e.target)) return;
|
|
832
|
+
document.querySelectorAll('.__ph__').forEach(el => el.classList.remove('__ph__'));
|
|
833
|
+
e.target.classList.add('__ph__');
|
|
834
|
+
if (state.tool === 'ins') {
|
|
835
|
+
const r = e.target.getBoundingClientRect(), cs = getComputedStyle(e.target);
|
|
836
|
+
tip.textContent = `${Math.round(r.width)}×${Math.round(r.height)} ${cs.position} ${cs.display}`;
|
|
837
|
+
tip.classList.add('v');
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
document.addEventListener('mouseout', e => {
|
|
841
|
+
if (!ps(e.target)) e.target.classList.remove('__ph__');
|
|
842
|
+
if (state.tool === 'ins') tip.classList.remove('v');
|
|
843
|
+
});
|
|
844
|
+
document.addEventListener('mousemove', e => {
|
|
845
|
+
if (state.tool === 'ins') { tip.style.left = (e.clientX + 14) + 'px'; tip.style.top = (e.clientY - 28) + 'px'; }
|
|
846
|
+
if (state.dragging || state.resizing) { tip.style.left = (e.clientX + 14) + 'px'; tip.style.top = (e.clientY - 28) + 'px'; }
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
// ── CLICK SELECT ─────────────────────────────────────────────────────────
|
|
850
|
+
document.addEventListener('click', e => {
|
|
851
|
+
if (!state.tool || ps(e.target) || state.tool === 'ins') return;
|
|
852
|
+
e.preventDefault(); e.stopPropagation();
|
|
853
|
+
|
|
854
|
+
if (state.tool === 'mov' && e.ctrlKey) {
|
|
855
|
+
// Ctrl+click: toggle element in multi-select
|
|
856
|
+
const t = e.target;
|
|
857
|
+
const idx = state.selectedEls.indexOf(t);
|
|
858
|
+
if (idx >= 0) {
|
|
859
|
+
// Already in selection — remove it
|
|
860
|
+
state.selectedEls.splice(idx, 1);
|
|
861
|
+
t.classList.remove('__ps_multi__');
|
|
862
|
+
t.classList.remove('__ps__');
|
|
863
|
+
// If we removed the primary, promote the first remaining to primary
|
|
864
|
+
if (t === state.selectedEl) {
|
|
865
|
+
state.selectedEl = state.selectedEls[0] || null;
|
|
866
|
+
if (state.selectedEl) {
|
|
867
|
+
state.selectedEl.classList.remove('__ps_multi__');
|
|
868
|
+
state.selectedEl.classList.add('__ps__');
|
|
880
869
|
}
|
|
881
870
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
871
|
+
} else {
|
|
872
|
+
// Add to selection
|
|
873
|
+
state.selectedEls.push(t);
|
|
874
|
+
t.classList.add('__ps_multi__');
|
|
875
|
+
// If no primary yet, make this primary
|
|
876
|
+
if (!state.selectedEl) {
|
|
877
|
+
state.selectedEl = t;
|
|
878
|
+
t.classList.remove('__ps_multi__');
|
|
879
|
+
t.classList.add('__ps__');
|
|
880
|
+
}
|
|
886
881
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
el.classList.remove('__ps_multi__');
|
|
892
|
-
});
|
|
893
|
-
state.selectedEls = [e.target];
|
|
894
|
-
state.selectedEl = e.target;
|
|
895
|
-
e.target.classList.add('__ps__');
|
|
896
|
-
|
|
897
|
-
if (state.tool === 'mov') { dEl = null; placeHdl(e.target); }
|
|
898
|
-
if (state.tool === 'rsz') { rzEl = null; rzDir = null; placeRH(e.target); }
|
|
899
|
-
if (state.tool === 'clr') populateColors(e.target);
|
|
900
|
-
if (state.tool === 'typ') populateTypo(e.target);
|
|
901
|
-
}, true);
|
|
902
|
-
|
|
903
|
-
// ══════════════════════════════════════════
|
|
904
|
-
// MOVE
|
|
905
|
-
// ══════════════════════════════════════════
|
|
906
|
-
function placeHdl(el) {
|
|
907
|
-
const r = el.getBoundingClientRect();
|
|
908
|
-
const hdlW = 80; // approximate handle width
|
|
909
|
-
// Center above element, but clamp inside viewport
|
|
910
|
-
let left = r.left + r.width / 2 - hdlW / 2;
|
|
911
|
-
let top = r.top - 36;
|
|
912
|
-
// Clamp horizontally
|
|
913
|
-
left = Math.max(8, Math.min(left, window.innerWidth - hdlW - 8));
|
|
914
|
-
// If element is at top, show handle below instead
|
|
915
|
-
if (top < 8) top = r.bottom + 8;
|
|
916
|
-
hdl.style.left = left + 'px';
|
|
917
|
-
hdl.style.top = top + 'px';
|
|
918
|
-
hdl.classList.add('v');
|
|
882
|
+
// Show handle on primary if any selected
|
|
883
|
+
if (state.selectedEl) placeHdl(state.selectedEl);
|
|
884
|
+
else hdl.classList.remove('v');
|
|
885
|
+
return;
|
|
919
886
|
}
|
|
920
887
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
dEl = state.selectedEl;
|
|
926
|
-
state.dragSX = e.clientX; state.dragSY = e.clientY;
|
|
927
|
-
|
|
928
|
-
// Snapshot original positions for ALL selected elements
|
|
929
|
-
const els = state.selectedEls.length > 0 ? state.selectedEls : [state.selectedEl];
|
|
930
|
-
state.dragOrigPositions = els.map(el => {
|
|
931
|
-
const cs = getComputedStyle(el);
|
|
932
|
-
const origL = parseFloat(cs.left) || 0;
|
|
933
|
-
const origT = parseFloat(cs.top) || 0;
|
|
934
|
-
if (cs.position === 'static') el.style.position = 'relative';
|
|
935
|
-
el.style.transition = 'none';
|
|
936
|
-
return { el, origL, origT, prevL: origL + 'px', prevT: origT + 'px' };
|
|
937
|
-
});
|
|
938
|
-
|
|
939
|
-
state.dragging = true;
|
|
940
|
-
});
|
|
941
|
-
document.addEventListener('mousemove', e => {
|
|
942
|
-
if (!state.dragging) return;
|
|
943
|
-
const dx = e.clientX - state.dragSX, dy = e.clientY - state.dragSY;
|
|
944
|
-
state.dragOrigPositions.forEach(({ el, origL, origT }) => {
|
|
945
|
-
el.style.left = (origL + dx) + 'px';
|
|
946
|
-
el.style.top = (origT + dy) + 'px';
|
|
947
|
-
});
|
|
948
|
-
if (dEl) {
|
|
949
|
-
placeHdl(dEl);
|
|
950
|
-
const { origL, origT } = state.dragOrigPositions[0];
|
|
951
|
-
tip.textContent = `x:${Math.round(origL + dx)} y:${Math.round(origT + dy)}`;
|
|
952
|
-
tip.classList.add('v');
|
|
953
|
-
}
|
|
888
|
+
// Normal click (no Ctrl): clear all and select just this
|
|
889
|
+
document.querySelectorAll('.__ps__, .__ps_multi__').forEach(el => {
|
|
890
|
+
el.classList.remove('__ps__');
|
|
891
|
+
el.classList.remove('__ps_multi__');
|
|
954
892
|
});
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
893
|
+
state.selectedEls = [e.target];
|
|
894
|
+
state.selectedEl = e.target;
|
|
895
|
+
e.target.classList.add('__ps__');
|
|
896
|
+
|
|
897
|
+
if (state.tool === 'mov') { dEl = null; placeHdl(e.target); }
|
|
898
|
+
if (state.tool === 'rsz') { rzEl = null; rzDir = null; placeRH(e.target); }
|
|
899
|
+
if (state.tool === 'clr') populateColors(e.target);
|
|
900
|
+
if (state.tool === 'typ') populateTypo(e.target);
|
|
901
|
+
}, true);
|
|
902
|
+
|
|
903
|
+
// ══════════════════════════════════════════
|
|
904
|
+
// MOVE
|
|
905
|
+
// ══════════════════════════════════════════
|
|
906
|
+
function placeHdl(el) {
|
|
907
|
+
const r = el.getBoundingClientRect();
|
|
908
|
+
const hdlW = 80; // approximate handle width
|
|
909
|
+
// Center above element, but clamp inside viewport
|
|
910
|
+
let left = r.left + r.width / 2 - hdlW / 2;
|
|
911
|
+
let top = r.top - 36;
|
|
912
|
+
// Clamp horizontally
|
|
913
|
+
left = Math.max(8, Math.min(left, window.innerWidth - hdlW - 8));
|
|
914
|
+
// If element is at top, show handle below instead
|
|
915
|
+
if (top < 8) top = r.bottom + 8;
|
|
916
|
+
hdl.style.left = left + 'px';
|
|
917
|
+
hdl.style.top = top + 'px';
|
|
918
|
+
hdl.classList.add('v');
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
let dEl;
|
|
922
|
+
hdl.addEventListener('mousedown', e => {
|
|
923
|
+
if (!state.selectedEl) return;
|
|
924
|
+
e.preventDefault();
|
|
925
|
+
dEl = state.selectedEl;
|
|
926
|
+
state.dragSX = e.clientX; state.dragSY = e.clientY;
|
|
927
|
+
|
|
928
|
+
// Snapshot original positions for ALL selected elements
|
|
929
|
+
const els = state.selectedEls.length > 0 ? state.selectedEls : [state.selectedEl];
|
|
930
|
+
state.dragOrigPositions = els.map(el => {
|
|
931
|
+
const cs = getComputedStyle(el);
|
|
932
|
+
const origL = parseFloat(cs.left) || 0;
|
|
933
|
+
const origT = parseFloat(cs.top) || 0;
|
|
934
|
+
if (cs.position === 'static') el.style.position = 'relative';
|
|
935
|
+
el.style.transition = 'none';
|
|
936
|
+
return { el, origL, origT, prevL: origL + 'px', prevT: origT + 'px' };
|
|
969
937
|
});
|
|
970
938
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
const hs = 5; // half handle size for centering
|
|
980
|
-
|
|
981
|
-
const pos = {
|
|
982
|
-
nw: [l - hs, t - hs],
|
|
983
|
-
n: [l + hw / 2 - hs, t - hs],
|
|
984
|
-
ne: [l + hw - hs, t - hs],
|
|
985
|
-
w: [l - hs, t + hh / 2 - hs],
|
|
986
|
-
e: [l + hw - hs, t + hh / 2 - hs],
|
|
987
|
-
sw: [l - hs, t + hh - hs],
|
|
988
|
-
s: [l + hw / 2 - hs, t + hh - hs],
|
|
989
|
-
se: [l + hw - hs, t + hh - hs],
|
|
990
|
-
};
|
|
991
|
-
rhDirs.forEach(d => {
|
|
992
|
-
rhs[d].style.left = pos[d][0] + 'px';
|
|
993
|
-
rhs[d].style.top = pos[d][1] + 'px';
|
|
994
|
-
rhs[d].classList.add('v');
|
|
995
|
-
});
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
let rzEl = null, rzDir = null;
|
|
999
|
-
rhDirs.forEach(d => {
|
|
1000
|
-
rhs[d].addEventListener('mousedown', e => {
|
|
1001
|
-
if (!state.selectedEl) return;
|
|
1002
|
-
e.preventDefault(); e.stopPropagation();
|
|
1003
|
-
rzEl = state.selectedEl; rzDir = d;
|
|
1004
|
-
const r = rzEl.getBoundingClientRect();
|
|
1005
|
-
const cs = getComputedStyle(rzEl);
|
|
1006
|
-
state.resizeOrigW = r.width; state.resizeOrigH = r.height;
|
|
1007
|
-
state.resizeOrigX = e.clientX; state.resizeOrigY = e.clientY;
|
|
1008
|
-
state.resizeOrigL = parseFloat(cs.left) || 0;
|
|
1009
|
-
state.resizeOrigT = parseFloat(cs.top) || 0;
|
|
1010
|
-
if (cs.position === 'static') rzEl.style.position = 'relative';
|
|
1011
|
-
rzEl.style.transition = 'none';
|
|
1012
|
-
state.resizing = true;
|
|
1013
|
-
});
|
|
939
|
+
state.dragging = true;
|
|
940
|
+
});
|
|
941
|
+
document.addEventListener('mousemove', e => {
|
|
942
|
+
if (!state.dragging) return;
|
|
943
|
+
const dx = e.clientX - state.dragSX, dy = e.clientY - state.dragSY;
|
|
944
|
+
state.dragOrigPositions.forEach(({ el, origL, origT }) => {
|
|
945
|
+
el.style.left = (origL + dx) + 'px';
|
|
946
|
+
el.style.top = (origT + dy) + 'px';
|
|
1014
947
|
});
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
let w = state.resizeOrigW, h = state.resizeOrigH;
|
|
1020
|
-
let l = state.resizeOrigL, t = state.resizeOrigT;
|
|
1021
|
-
|
|
1022
|
-
if (rzDir.includes('e')) w = Math.max(20, w + dx);
|
|
1023
|
-
if (rzDir.includes('s')) h = Math.max(20, h + dy);
|
|
1024
|
-
if (rzDir.includes('w')) { w = Math.max(20, w - dx); l = state.resizeOrigL + (state.resizeOrigW - w); }
|
|
1025
|
-
if (rzDir.includes('n')) { h = Math.max(20, h - dy); t = state.resizeOrigT + (state.resizeOrigH - h); }
|
|
1026
|
-
|
|
1027
|
-
rzEl.style.width = Math.round(w) + 'px';
|
|
1028
|
-
rzEl.style.height = Math.round(h) + 'px';
|
|
1029
|
-
rzEl.style.left = Math.round(l) + 'px';
|
|
1030
|
-
rzEl.style.top = Math.round(t) + 'px';
|
|
1031
|
-
placeRH(rzEl);
|
|
1032
|
-
tip.textContent = `${Math.round(w)}×${Math.round(h)}`;
|
|
948
|
+
if (dEl) {
|
|
949
|
+
placeHdl(dEl);
|
|
950
|
+
const { origL, origT } = state.dragOrigPositions[0];
|
|
951
|
+
tip.textContent = `x:${Math.round(origL + dx)} y:${Math.round(origT + dy)}`;
|
|
1033
952
|
tip.classList.add('v');
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
document.addEventListener('mouseup', () => {
|
|
956
|
+
if (!state.dragging) return;
|
|
957
|
+
state.dragging = false; tip.classList.remove('v');
|
|
958
|
+
// Record each element separately
|
|
959
|
+
state.dragOrigPositions.forEach(({ el, prevL, prevT }) => {
|
|
960
|
+
const cs = getComputedStyle(el);
|
|
961
|
+
const newLeft = Math.round(parseFloat(cs.left)) + 'px';
|
|
962
|
+
const newTop = Math.round(parseFloat(cs.top)) + 'px';
|
|
963
|
+
if (newLeft !== prevL || newTop !== prevT) {
|
|
964
|
+
rec(el, { left: newLeft, top: newTop }, { left: prevL, top: prevT });
|
|
965
|
+
}
|
|
1034
966
|
});
|
|
967
|
+
state.dragOrigPositions = [];
|
|
968
|
+
dEl = null;
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
// ══════════════════════════════════════════
|
|
972
|
+
// RESIZE
|
|
973
|
+
// ══════════════════════════════════════════
|
|
974
|
+
function placeRH(el) {
|
|
975
|
+
const r = el.getBoundingClientRect();
|
|
976
|
+
// Use viewport coords (fixed position handles need viewport coords, not page)
|
|
977
|
+
const t = r.top, l = r.left;
|
|
978
|
+
const hw = r.width, hh = r.height;
|
|
979
|
+
const hs = 5; // half handle size for centering
|
|
980
|
+
|
|
981
|
+
const pos = {
|
|
982
|
+
nw: [l - hs, t - hs],
|
|
983
|
+
n: [l + hw / 2 - hs, t - hs],
|
|
984
|
+
ne: [l + hw - hs, t - hs],
|
|
985
|
+
w: [l - hs, t + hh / 2 - hs],
|
|
986
|
+
e: [l + hw - hs, t + hh / 2 - hs],
|
|
987
|
+
sw: [l - hs, t + hh - hs],
|
|
988
|
+
s: [l + hw / 2 - hs, t + hh - hs],
|
|
989
|
+
se: [l + hw - hs, t + hh - hs],
|
|
990
|
+
};
|
|
991
|
+
rhDirs.forEach(d => {
|
|
992
|
+
rhs[d].style.left = pos[d][0] + 'px';
|
|
993
|
+
rhs[d].style.top = pos[d][1] + 'px';
|
|
994
|
+
rhs[d].classList.add('v');
|
|
995
|
+
});
|
|
996
|
+
}
|
|
1035
997
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
998
|
+
let rzEl = null, rzDir = null;
|
|
999
|
+
rhDirs.forEach(d => {
|
|
1000
|
+
rhs[d].addEventListener('mousedown', e => {
|
|
1001
|
+
if (!state.selectedEl) return;
|
|
1002
|
+
e.preventDefault(); e.stopPropagation();
|
|
1003
|
+
rzEl = state.selectedEl; rzDir = d;
|
|
1004
|
+
const r = rzEl.getBoundingClientRect();
|
|
1039
1005
|
const cs = getComputedStyle(rzEl);
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
const newSize = {
|
|
1048
|
-
width: Math.round(parseFloat(cs.width)) + 'px',
|
|
1049
|
-
height: Math.round(parseFloat(cs.height)) + 'px',
|
|
1050
|
-
left: Math.round(parseFloat(cs.left)) + 'px',
|
|
1051
|
-
top: Math.round(parseFloat(cs.top)) + 'px',
|
|
1052
|
-
};
|
|
1053
|
-
if (newSize.width !== prevProps.width || newSize.height !== prevProps.height || newSize.left !== prevProps.left || newSize.top !== prevProps.top) {
|
|
1054
|
-
rec(rzEl, newSize, prevProps);
|
|
1055
|
-
}
|
|
1056
|
-
rzEl = null; rzDir = null;
|
|
1006
|
+
state.resizeOrigW = r.width; state.resizeOrigH = r.height;
|
|
1007
|
+
state.resizeOrigX = e.clientX; state.resizeOrigY = e.clientY;
|
|
1008
|
+
state.resizeOrigL = parseFloat(cs.left) || 0;
|
|
1009
|
+
state.resizeOrigT = parseFloat(cs.top) || 0;
|
|
1010
|
+
if (cs.position === 'static') rzEl.style.position = 'relative';
|
|
1011
|
+
rzEl.style.transition = 'none';
|
|
1012
|
+
state.resizing = true;
|
|
1057
1013
|
});
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
const
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
document.addEventListener('mousemove', e => {
|
|
1017
|
+
if (!state.resizing || !rzEl) return;
|
|
1018
|
+
const dx = e.clientX - state.resizeOrigX, dy = e.clientY - state.resizeOrigY;
|
|
1019
|
+
let w = state.resizeOrigW, h = state.resizeOrigH;
|
|
1020
|
+
let l = state.resizeOrigL, t = state.resizeOrigT;
|
|
1021
|
+
|
|
1022
|
+
if (rzDir.includes('e')) w = Math.max(20, w + dx);
|
|
1023
|
+
if (rzDir.includes('s')) h = Math.max(20, h + dy);
|
|
1024
|
+
if (rzDir.includes('w')) { w = Math.max(20, w - dx); l = state.resizeOrigL + (state.resizeOrigW - w); }
|
|
1025
|
+
if (rzDir.includes('n')) { h = Math.max(20, h - dy); t = state.resizeOrigT + (state.resizeOrigH - h); }
|
|
1026
|
+
|
|
1027
|
+
rzEl.style.width = Math.round(w) + 'px';
|
|
1028
|
+
rzEl.style.height = Math.round(h) + 'px';
|
|
1029
|
+
rzEl.style.left = Math.round(l) + 'px';
|
|
1030
|
+
rzEl.style.top = Math.round(t) + 'px';
|
|
1031
|
+
placeRH(rzEl);
|
|
1032
|
+
tip.textContent = `${Math.round(w)}×${Math.round(h)}`;
|
|
1033
|
+
tip.classList.add('v');
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
document.addEventListener('mouseup', () => {
|
|
1037
|
+
if (!state.resizing || !rzEl) return;
|
|
1038
|
+
state.resizing = false; tip.classList.remove('v');
|
|
1039
|
+
const cs = getComputedStyle(rzEl);
|
|
1040
|
+
// prevProps = original values before resize started
|
|
1041
|
+
const prevProps = {
|
|
1042
|
+
width: Math.round(state.resizeOrigW) + 'px',
|
|
1043
|
+
height: Math.round(state.resizeOrigH) + 'px',
|
|
1044
|
+
left: Math.round(state.resizeOrigL) + 'px',
|
|
1045
|
+
top: Math.round(state.resizeOrigT) + 'px',
|
|
1083
1046
|
};
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1047
|
+
const newSize = {
|
|
1048
|
+
width: Math.round(parseFloat(cs.width)) + 'px',
|
|
1049
|
+
height: Math.round(parseFloat(cs.height)) + 'px',
|
|
1050
|
+
left: Math.round(parseFloat(cs.left)) + 'px',
|
|
1051
|
+
top: Math.round(parseFloat(cs.top)) + 'px',
|
|
1089
1052
|
};
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
return !color || color === 'transparent' || color === 'rgba(0, 0, 0, 0)';
|
|
1053
|
+
if (newSize.width !== prevProps.width || newSize.height !== prevProps.height || newSize.left !== prevProps.left || newSize.top !== prevProps.top) {
|
|
1054
|
+
rec(rzEl, newSize, prevProps);
|
|
1093
1055
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1056
|
+
rzEl = null; rzDir = null;
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
// ══════════════════════════════════════════
|
|
1060
|
+
// COLORS
|
|
1061
|
+
// ══════════════════════════════════════════
|
|
1062
|
+
const clrElName = document.getElementById('__clr_el_name__');
|
|
1063
|
+
const swBg = document.getElementById('__sw_bg__'), cpBg = document.getElementById('__cp_bg__');
|
|
1064
|
+
const swFg = document.getElementById('__sw_fg__'), cpFg = document.getElementById('__cp_fg__');
|
|
1065
|
+
const swBd = document.getElementById('__sw_bd__'), cpBd = document.getElementById('__cp_bd__');
|
|
1066
|
+
const cpBgTrans = document.getElementById('__cp_bg_trans__');
|
|
1067
|
+
const cpBdTrans = document.getElementById('__cp_bd_trans__');
|
|
1068
|
+
|
|
1069
|
+
function setupSwatch(sw, cp) {
|
|
1070
|
+
sw.onclick = () => cp.click();
|
|
1071
|
+
cp.oninput = () => { sw.style.background = cp.value; };
|
|
1072
|
+
}
|
|
1073
|
+
setupSwatch(swBg, cpBg);
|
|
1074
|
+
setupSwatch(swFg, cpFg);
|
|
1075
|
+
setupSwatch(swBd, cpBd);
|
|
1076
|
+
|
|
1077
|
+
// Transparent checkbox toggles swatch appearance
|
|
1078
|
+
cpBgTrans.onchange = () => {
|
|
1079
|
+
swBg.style.background = cpBgTrans.checked
|
|
1080
|
+
? 'repeating-conic-gradient(#444 0% 25%, #222 0% 50%) 0 0/10px 10px'
|
|
1081
|
+
: cpBg.value;
|
|
1082
|
+
swBg.style.opacity = cpBgTrans.checked ? '0.6' : '1';
|
|
1083
|
+
};
|
|
1084
|
+
cpBdTrans.onchange = () => {
|
|
1085
|
+
swBd.style.background = cpBdTrans.checked
|
|
1086
|
+
? 'repeating-conic-gradient(#444 0% 25%, #222 0% 50%) 0 0/10px 10px'
|
|
1087
|
+
: cpBd.value;
|
|
1088
|
+
swBd.style.opacity = cpBdTrans.checked ? '0.6' : '1';
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
function isTransparent(color) {
|
|
1092
|
+
return !color || color === 'transparent' || color === 'rgba(0, 0, 0, 0)';
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function populateColors(el) {
|
|
1096
|
+
const cs = getComputedStyle(el);
|
|
1097
|
+
clrElName.textContent = el.tagName.toLowerCase() + (el.id ? '#' + el.id : el.className ? '.' + [...el.classList][0] : '');
|
|
1098
|
+
let bg = cs.backgroundColor;
|
|
1099
|
+
let fg = cs.color || '#000000';
|
|
1100
|
+
let bd = cs.borderColor || '#cccccc';
|
|
1101
|
+
const bw = cs.borderWidth;
|
|
1102
|
+
|
|
1103
|
+
// #14: Support SVG fill/stroke (#14)
|
|
1104
|
+
const isSvg = el.namespaceURI === 'http://www.w3.org/2000/svg';
|
|
1105
|
+
if (isSvg) {
|
|
1106
|
+
fg = cs.fill;
|
|
1107
|
+
bd = cs.stroke;
|
|
1140
1108
|
}
|
|
1141
1109
|
|
|
1142
|
-
//
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
if (state.selectedEl) state.selectedEl.style.boxShadow = e.target.value === 'none' ? '' : e.target.value;
|
|
1153
|
-
};
|
|
1154
|
-
|
|
1155
|
-
document.getElementById('__clr_apply__').onclick = () => {
|
|
1156
|
-
if (!state.selectedEl) { toast('⚠ Select an element first'); return; }
|
|
1157
|
-
const el = state.selectedEl;
|
|
1158
|
-
const bgVal = cpBgTrans.checked ? 'transparent' : cpBg.value;
|
|
1159
|
-
const bdVal = cpBdTrans.checked ? 'none' : cpBd.value;
|
|
1160
|
-
const opVal = document.getElementById('__st_opacity__').value;
|
|
1161
|
-
const radVal = document.getElementById('__st_radius__').value + 'px';
|
|
1162
|
-
const shVal = document.getElementById('__st_shadow__').value;
|
|
1163
|
-
|
|
1164
|
-
const prevProps = {
|
|
1165
|
-
'background-color': el.style.backgroundColor || '',
|
|
1166
|
-
'color': el.style.color || '',
|
|
1167
|
-
'border': el.style.border || '',
|
|
1168
|
-
'opacity': el.style.opacity || '',
|
|
1169
|
-
'border-radius': el.style.borderRadius || '',
|
|
1170
|
-
'box-shadow': el.style.boxShadow || ''
|
|
1171
|
-
};
|
|
1172
|
-
|
|
1173
|
-
if (el.namespaceURI === 'http://www.w3.org/2000/svg') {
|
|
1174
|
-
el.style.fill = cpFg.value;
|
|
1175
|
-
el.style.stroke = cpBdTrans.checked ? 'none' : bdVal;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
el.style.backgroundColor = bgVal;
|
|
1179
|
-
el.style.color = cpFg.value;
|
|
1180
|
-
el.style.opacity = opVal;
|
|
1181
|
-
el.style.borderRadius = radVal;
|
|
1182
|
-
el.style.boxShadow = shVal === 'none' ? '' : shVal;
|
|
1183
|
-
|
|
1184
|
-
if (cpBdTrans.checked) {
|
|
1185
|
-
el.style.border = 'none';
|
|
1186
|
-
} else {
|
|
1187
|
-
el.style.borderColor = bdVal;
|
|
1188
|
-
if (getComputedStyle(el).borderWidth === '0px') el.style.borderWidth = '1px';
|
|
1189
|
-
if (getComputedStyle(el).borderStyle === 'none') el.style.borderStyle = 'solid';
|
|
1190
|
-
}
|
|
1191
|
-
rec(el, {
|
|
1192
|
-
'background-color': bgVal, color: cpFg.value,
|
|
1193
|
-
'border': cpBdTrans.checked ? 'none' : `1px solid ${bdVal}`,
|
|
1194
|
-
'opacity': opVal, 'border-radius': radVal, 'box-shadow': shVal === 'none' ? '' : shVal
|
|
1195
|
-
}, prevProps);
|
|
1196
|
-
toast('🎨 Styles applied!');
|
|
1197
|
-
};
|
|
1198
|
-
|
|
1199
|
-
function rgb2hex(rgb) {
|
|
1200
|
-
if (rgb.startsWith('#')) return rgb;
|
|
1201
|
-
const m = rgb.match(/\d+/g);
|
|
1202
|
-
if (!m || m.length < 3) return '#000000';
|
|
1203
|
-
return '#' + m.slice(0, 3).map(x => parseInt(x).toString(16).padStart(2, '0')).join('');
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
// ══════════════════════════════════════════
|
|
1207
|
-
// TYPOGRAPHY
|
|
1208
|
-
// ══════════════════════════════════════════
|
|
1209
|
-
const typElName = document.getElementById('__typ_el_name__');
|
|
1210
|
-
const typSz = document.getElementById('__typ_sz__');
|
|
1211
|
-
const typLh = document.getElementById('__typ_lh__');
|
|
1212
|
-
const typLs = document.getElementById('__typ_ls__');
|
|
1213
|
-
const typFont = document.getElementById('__typ_font__');
|
|
1214
|
-
const typBold = document.getElementById('__typ_bold__');
|
|
1215
|
-
const typItalic = document.getElementById('__typ_italic__');
|
|
1216
|
-
const typUnder = document.getElementById('__typ_under__');
|
|
1217
|
-
const typStrike = document.getElementById('__typ_strike__');
|
|
1218
|
-
const typUpper = document.getElementById('__typ_upper__');
|
|
1219
|
-
const typLower = document.getElementById('__typ_lower__');
|
|
1220
|
-
|
|
1221
|
-
// Track toggle states
|
|
1222
|
-
const typState = { bold: false, italic: false, under: false, strike: false, upper: false, lower: false };
|
|
1223
|
-
|
|
1224
|
-
function setStyleBtn(btn, key, val) {
|
|
1225
|
-
typState[key] = val;
|
|
1226
|
-
btn.classList.toggle('on', val);
|
|
1110
|
+
// Background — detect transparent
|
|
1111
|
+
if (isTransparent(bg)) {
|
|
1112
|
+
cpBgTrans.checked = true;
|
|
1113
|
+
swBg.style.background = 'repeating-conic-gradient(#444 0% 25%, #222 0% 50%) 0 0/10px 10px';
|
|
1114
|
+
swBg.style.opacity = '0.6';
|
|
1115
|
+
} else {
|
|
1116
|
+
cpBgTrans.checked = false;
|
|
1117
|
+
cpBg.value = rgb2hex(bg);
|
|
1118
|
+
swBg.style.background = cpBg.value;
|
|
1119
|
+
swBg.style.opacity = '1';
|
|
1227
1120
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
if (!name || loadedFonts.has(name)) return;
|
|
1240
|
-
loadedFonts.add(name);
|
|
1241
|
-
const link = document.createElement('link');
|
|
1242
|
-
link.rel = 'stylesheet';
|
|
1243
|
-
link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(name)}:wght@400;700&display=swap`;
|
|
1244
|
-
document.head.appendChild(link);
|
|
1121
|
+
cpFg.value = rgb2hex(fg); swFg.style.background = cpFg.value;
|
|
1122
|
+
// Border — detect none/transparent
|
|
1123
|
+
if (isTransparent(bd) || bw === '0px') {
|
|
1124
|
+
cpBdTrans.checked = true;
|
|
1125
|
+
swBd.style.background = 'repeating-conic-gradient(#444 0% 25%, #222 0% 50%) 0 0/10px 10px';
|
|
1126
|
+
swBd.style.opacity = '0.6';
|
|
1127
|
+
} else {
|
|
1128
|
+
cpBdTrans.checked = false;
|
|
1129
|
+
cpBd.value = rgb2hex(bd);
|
|
1130
|
+
swBd.style.background = cpBd.value;
|
|
1131
|
+
swBd.style.opacity = '1';
|
|
1245
1132
|
}
|
|
1246
1133
|
|
|
1247
|
-
//
|
|
1248
|
-
|
|
1249
|
-
|
|
1134
|
+
// #11: Advanced styles detection
|
|
1135
|
+
const opVal = parseFloat(cs.opacity) || 1;
|
|
1136
|
+
document.getElementById('__st_opacity__').value = opVal;
|
|
1137
|
+
document.getElementById('__st_opacity_val__').textContent = opVal.toFixed(2);
|
|
1138
|
+
document.getElementById('__st_radius__').value = parseInt(cs.borderRadius) || 0;
|
|
1139
|
+
document.getElementById('__st_shadow__').value = cs.boxShadow === 'none' ? 'none' : '0 4px 12px rgba(0,0,0,0.15)'; // simple match
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// #11: Live preview for advanced styles
|
|
1143
|
+
document.getElementById('__st_opacity__').oninput = e => {
|
|
1144
|
+
const v = e.target.value;
|
|
1145
|
+
document.getElementById('__st_opacity_val__').textContent = parseFloat(v).toFixed(2);
|
|
1146
|
+
if (state.selectedEl) state.selectedEl.style.opacity = v;
|
|
1147
|
+
};
|
|
1148
|
+
document.getElementById('__st_radius__').oninput = e => {
|
|
1149
|
+
if (state.selectedEl) state.selectedEl.style.borderRadius = e.target.value + 'px';
|
|
1150
|
+
};
|
|
1151
|
+
document.getElementById('__st_shadow__').onchange = e => {
|
|
1152
|
+
if (state.selectedEl) state.selectedEl.style.boxShadow = e.target.value === 'none' ? '' : e.target.value;
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
document.getElementById('__clr_apply__').onclick = () => {
|
|
1156
|
+
if (!state.selectedEl) { toast('⚠ Select an element first'); return; }
|
|
1157
|
+
const el = state.selectedEl;
|
|
1158
|
+
const bgVal = cpBgTrans.checked ? 'transparent' : cpBg.value;
|
|
1159
|
+
const bdVal = cpBdTrans.checked ? 'none' : cpBd.value;
|
|
1160
|
+
const opVal = document.getElementById('__st_opacity__').value;
|
|
1161
|
+
const radVal = document.getElementById('__st_radius__').value + 'px';
|
|
1162
|
+
const shVal = document.getElementById('__st_shadow__').value;
|
|
1163
|
+
|
|
1164
|
+
const prevProps = {
|
|
1165
|
+
'background-color': el.style.backgroundColor || '',
|
|
1166
|
+
'color': el.style.color || '',
|
|
1167
|
+
'border': el.style.border || '',
|
|
1168
|
+
'opacity': el.style.opacity || '',
|
|
1169
|
+
'border-radius': el.style.borderRadius || '',
|
|
1170
|
+
'box-shadow': el.style.boxShadow || ''
|
|
1250
1171
|
};
|
|
1251
1172
|
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
el.contentEditable = 'true';
|
|
1256
|
-
el.dataset.origText = el.innerHTML || '';
|
|
1257
|
-
el.focus();
|
|
1258
|
-
|
|
1259
|
-
const finishEdit = () => {
|
|
1260
|
-
el.contentEditable = 'false';
|
|
1261
|
-
el.removeEventListener('blur', finishEdit);
|
|
1262
|
-
if (el.innerHTML !== el.dataset.origText) {
|
|
1263
|
-
rec(el, { innerHTML: el.innerHTML }, { innerHTML: el.dataset.origText });
|
|
1264
|
-
toast('Text updated!');
|
|
1265
|
-
}
|
|
1266
|
-
};
|
|
1267
|
-
el.addEventListener('blur', finishEdit);
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
const cs = getComputedStyle(el);
|
|
1271
|
-
typElName.textContent = el.tagName.toLowerCase() + (el.id ? '#' + el.id : el.className ? '.' + [...el.classList].filter(c => !c.startsWith('__'))[0] : '');
|
|
1272
|
-
typSz.value = Math.round(parseFloat(cs.fontSize)) || 16;
|
|
1273
|
-
typLh.value = parseFloat(cs.lineHeight) ? (parseFloat(cs.lineHeight) / parseFloat(cs.fontSize)).toFixed(1) : '1.5';
|
|
1274
|
-
typLs.value = parseFloat(cs.letterSpacing) || 0;
|
|
1275
|
-
typFont.value = '';
|
|
1276
|
-
|
|
1277
|
-
// Detect current styles
|
|
1278
|
-
setStyleBtn(typBold, 'bold', parseInt(cs.fontWeight) >= 700);
|
|
1279
|
-
setStyleBtn(typItalic, 'italic', cs.fontStyle === 'italic');
|
|
1280
|
-
setStyleBtn(typUnder, 'under', cs.textDecoration.includes('underline'));
|
|
1281
|
-
setStyleBtn(typStrike, 'strike', cs.textDecoration.includes('line-through'));
|
|
1282
|
-
setStyleBtn(typUpper, 'upper', cs.textTransform === 'uppercase');
|
|
1283
|
-
setStyleBtn(typLower, 'lower', cs.textTransform === 'lowercase');
|
|
1284
|
-
|
|
1285
|
-
// Live preview on input change
|
|
1286
|
-
[typSz, typLh, typLs].forEach(inp => { inp.oninput = () => previewTypo(el); });
|
|
1173
|
+
if (el.namespaceURI === 'http://www.w3.org/2000/svg') {
|
|
1174
|
+
el.style.fill = cpFg.value;
|
|
1175
|
+
el.style.stroke = cpBdTrans.checked ? 'none' : bdVal;
|
|
1287
1176
|
}
|
|
1288
1177
|
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
el.style.
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
}
|
|
1178
|
+
el.style.backgroundColor = bgVal;
|
|
1179
|
+
el.style.color = cpFg.value;
|
|
1180
|
+
el.style.opacity = opVal;
|
|
1181
|
+
el.style.borderRadius = radVal;
|
|
1182
|
+
el.style.boxShadow = shVal === 'none' ? '' : shVal;
|
|
1183
|
+
|
|
1184
|
+
if (cpBdTrans.checked) {
|
|
1185
|
+
el.style.border = 'none';
|
|
1186
|
+
} else {
|
|
1187
|
+
el.style.borderColor = bdVal;
|
|
1188
|
+
if (getComputedStyle(el).borderWidth === '0px') el.style.borderWidth = '1px';
|
|
1189
|
+
if (getComputedStyle(el).borderStyle === 'none') el.style.borderStyle = 'solid';
|
|
1302
1190
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
};
|
|
1379
|
-
reader.readAsDataURL(file);
|
|
1380
|
-
});
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
function addThumb(asset) {
|
|
1384
|
-
const img = document.createElement('img');
|
|
1385
|
-
img.className = 'ps-ast-thumb';
|
|
1386
|
-
img.src = asset.src;
|
|
1387
|
-
img.title = asset.name;
|
|
1388
|
-
img.dataset.assetId = asset.id;
|
|
1389
|
-
img.onclick = () => {
|
|
1390
|
-
// Deselect all thumbs
|
|
1391
|
-
document.querySelectorAll('.ps-ast-thumb').forEach(t => t.classList.remove('active'));
|
|
1392
|
-
img.classList.add('active');
|
|
1393
|
-
startPlacing(asset);
|
|
1191
|
+
rec(el, {
|
|
1192
|
+
'background-color': bgVal, color: cpFg.value,
|
|
1193
|
+
'border': cpBdTrans.checked ? 'none' : `1px solid ${bdVal}`,
|
|
1194
|
+
'opacity': opVal, 'border-radius': radVal, 'box-shadow': shVal === 'none' ? '' : shVal
|
|
1195
|
+
}, prevProps);
|
|
1196
|
+
toast('🎨 Styles applied!');
|
|
1197
|
+
};
|
|
1198
|
+
|
|
1199
|
+
function rgb2hex(rgb) {
|
|
1200
|
+
if (rgb.startsWith('#')) return rgb;
|
|
1201
|
+
const m = rgb.match(/\d+/g);
|
|
1202
|
+
if (!m || m.length < 3) return '#000000';
|
|
1203
|
+
return '#' + m.slice(0, 3).map(x => parseInt(x).toString(16).padStart(2, '0')).join('');
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// ══════════════════════════════════════════
|
|
1207
|
+
// TYPOGRAPHY
|
|
1208
|
+
// ══════════════════════════════════════════
|
|
1209
|
+
const typElName = document.getElementById('__typ_el_name__');
|
|
1210
|
+
const typSz = document.getElementById('__typ_sz__');
|
|
1211
|
+
const typLh = document.getElementById('__typ_lh__');
|
|
1212
|
+
const typLs = document.getElementById('__typ_ls__');
|
|
1213
|
+
const typFont = document.getElementById('__typ_font__');
|
|
1214
|
+
const typBold = document.getElementById('__typ_bold__');
|
|
1215
|
+
const typItalic = document.getElementById('__typ_italic__');
|
|
1216
|
+
const typUnder = document.getElementById('__typ_under__');
|
|
1217
|
+
const typStrike = document.getElementById('__typ_strike__');
|
|
1218
|
+
const typUpper = document.getElementById('__typ_upper__');
|
|
1219
|
+
const typLower = document.getElementById('__typ_lower__');
|
|
1220
|
+
|
|
1221
|
+
// Track toggle states
|
|
1222
|
+
const typState = { bold: false, italic: false, under: false, strike: false, upper: false, lower: false };
|
|
1223
|
+
|
|
1224
|
+
function setStyleBtn(btn, key, val) {
|
|
1225
|
+
typState[key] = val;
|
|
1226
|
+
btn.classList.toggle('on', val);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
typBold.onclick = () => setStyleBtn(typBold, 'bold', !typState.bold);
|
|
1230
|
+
typItalic.onclick = () => setStyleBtn(typItalic, 'italic', !typState.italic);
|
|
1231
|
+
typUnder.onclick = () => setStyleBtn(typUnder, 'under', !typState.under);
|
|
1232
|
+
typStrike.onclick = () => setStyleBtn(typStrike, 'strike', !typState.strike);
|
|
1233
|
+
typUpper.onclick = () => { setStyleBtn(typUpper, 'upper', !typState.upper); if (typState.upper) setStyleBtn(typLower, 'lower', false); };
|
|
1234
|
+
typLower.onclick = () => { setStyleBtn(typLower, 'lower', !typState.lower); if (typState.lower) setStyleBtn(typUpper, 'upper', false); };
|
|
1235
|
+
|
|
1236
|
+
// Loaded Google Fonts cache
|
|
1237
|
+
const loadedFonts = new Set();
|
|
1238
|
+
function loadGoogleFont(name) {
|
|
1239
|
+
if (!name || loadedFonts.has(name)) return;
|
|
1240
|
+
loadedFonts.add(name);
|
|
1241
|
+
const link = document.createElement('link');
|
|
1242
|
+
link.rel = 'stylesheet';
|
|
1243
|
+
link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(name)}:wght@400;700&display=swap`;
|
|
1244
|
+
document.head.appendChild(link);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// Live preview when font changes
|
|
1248
|
+
typFont.onchange = () => {
|
|
1249
|
+
if (typFont.value) loadGoogleFont(typFont.value);
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
function populateTypo(el) {
|
|
1253
|
+
// Make text directly editable
|
|
1254
|
+
if (!el.isContentEditable) {
|
|
1255
|
+
el.contentEditable = 'true';
|
|
1256
|
+
el.dataset.origText = el.innerHTML || '';
|
|
1257
|
+
el.focus();
|
|
1258
|
+
|
|
1259
|
+
const finishEdit = () => {
|
|
1260
|
+
el.contentEditable = 'false';
|
|
1261
|
+
el.removeEventListener('blur', finishEdit);
|
|
1262
|
+
if (el.innerHTML !== el.dataset.origText) {
|
|
1263
|
+
rec(el, { innerHTML: el.innerHTML }, { innerHTML: el.dataset.origText });
|
|
1264
|
+
toast('Text updated!');
|
|
1265
|
+
}
|
|
1394
1266
|
};
|
|
1395
|
-
|
|
1267
|
+
el.addEventListener('blur', finishEdit);
|
|
1396
1268
|
}
|
|
1397
1269
|
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1270
|
+
const cs = getComputedStyle(el);
|
|
1271
|
+
typElName.textContent = el.tagName.toLowerCase() + (el.id ? '#' + el.id : el.className ? '.' + [...el.classList].filter(c => !c.startsWith('__'))[0] : '');
|
|
1272
|
+
typSz.value = Math.round(parseFloat(cs.fontSize)) || 16;
|
|
1273
|
+
typLh.value = parseFloat(cs.lineHeight) ? (parseFloat(cs.lineHeight) / parseFloat(cs.fontSize)).toFixed(1) : '1.5';
|
|
1274
|
+
typLs.value = parseFloat(cs.letterSpacing) || 0;
|
|
1275
|
+
typFont.value = '';
|
|
1276
|
+
|
|
1277
|
+
// Detect current styles
|
|
1278
|
+
setStyleBtn(typBold, 'bold', parseInt(cs.fontWeight) >= 700);
|
|
1279
|
+
setStyleBtn(typItalic, 'italic', cs.fontStyle === 'italic');
|
|
1280
|
+
setStyleBtn(typUnder, 'under', cs.textDecoration.includes('underline'));
|
|
1281
|
+
setStyleBtn(typStrike, 'strike', cs.textDecoration.includes('line-through'));
|
|
1282
|
+
setStyleBtn(typUpper, 'upper', cs.textTransform === 'uppercase');
|
|
1283
|
+
setStyleBtn(typLower, 'lower', cs.textTransform === 'lowercase');
|
|
1284
|
+
|
|
1285
|
+
// Live preview on input change
|
|
1286
|
+
[typSz, typLh, typLs].forEach(inp => { inp.oninput = () => previewTypo(el); });
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function previewTypo(el) {
|
|
1290
|
+
el.style.fontSize = typSz.value + 'px';
|
|
1291
|
+
el.style.lineHeight = typLh.value;
|
|
1292
|
+
el.style.letterSpacing = typLs.value + 'px';
|
|
1293
|
+
el.style.setProperty('font-weight', typState.bold ? '700' : '400', 'important');
|
|
1294
|
+
el.style.setProperty('font-style', typState.italic ? 'italic' : 'normal', 'important');
|
|
1295
|
+
const deco = [typState.under && 'underline', typState.strike && 'line-through'].filter(Boolean).join(' ') || 'none';
|
|
1296
|
+
el.style.setProperty('text-decoration', deco, 'important');
|
|
1297
|
+
el.style.textTransform = typState.upper ? 'uppercase' : typState.lower ? 'lowercase' : 'none';
|
|
1298
|
+
if (typFont.value) {
|
|
1299
|
+
loadGoogleFont(typFont.value);
|
|
1300
|
+
el.style.fontFamily = `'${typFont.value}', sans-serif`;
|
|
1414
1301
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
document.getElementById('__typ_apply__').onclick = () => {
|
|
1305
|
+
if (!state.selectedEl) { toast('⚠ Select a text element first'); return; }
|
|
1306
|
+
const el = state.selectedEl;
|
|
1307
|
+
const deco = [typState.under && 'underline', typState.strike && 'line-through'].filter(Boolean).join(' ') || 'none';
|
|
1308
|
+
const props = {
|
|
1309
|
+
'font-size': typSz.value + 'px',
|
|
1310
|
+
'font-weight': typState.bold ? '700' : '400',
|
|
1311
|
+
'font-style': typState.italic ? 'italic' : 'normal',
|
|
1312
|
+
'text-decoration': deco,
|
|
1313
|
+
'text-transform': typState.upper ? 'uppercase' : typState.lower ? 'lowercase' : 'none',
|
|
1314
|
+
'line-height': typLh.value,
|
|
1315
|
+
'letter-spacing': typLs.value + 'px',
|
|
1316
|
+
};
|
|
1317
|
+
if (typFont.value) {
|
|
1318
|
+
props['font-family'] = `'${typFont.value}', sans-serif`;
|
|
1319
|
+
props['googleFont'] = typFont.value;
|
|
1420
1320
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1321
|
+
// Snapshot BEFORE previewTypo applies styles
|
|
1322
|
+
const prevProps = {};
|
|
1323
|
+
Object.keys(props).forEach(p => {
|
|
1324
|
+
const camel = p.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1325
|
+
prevProps[p] = el.style[camel] || '';
|
|
1326
|
+
});
|
|
1327
|
+
previewTypo(el);
|
|
1328
|
+
rec(el, props, prevProps);
|
|
1329
|
+
toast('📝 Typography applied!');
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1332
|
+
// ══════════════════════════════════════════
|
|
1333
|
+
// ASSETS
|
|
1334
|
+
// ══════════════════════════════════════════
|
|
1335
|
+
const astDrop = document.getElementById('__ast_drop__');
|
|
1336
|
+
const astFile = document.getElementById('__ast_file__');
|
|
1337
|
+
const astList = document.getElementById('__ast_list__');
|
|
1338
|
+
const astHint = document.getElementById('__ast_hint__');
|
|
1339
|
+
const astStore = []; // { id, name, src, url }
|
|
1340
|
+
let pendingAsset = null; // asset waiting to be placed
|
|
1341
|
+
let placingEl = null; // the ghost element following cursor
|
|
1342
|
+
let astDragEl = null; // placed asset being moved
|
|
1343
|
+
let astDragSX, astDragSY, astDragOL, astDragOT;
|
|
1344
|
+
|
|
1345
|
+
// Upload via click or drop
|
|
1346
|
+
astDrop.onclick = () => astFile.click();
|
|
1347
|
+
astFile.onchange = e => loadAssets(e.target.files);
|
|
1348
|
+
astDrop.ondragover = e => { e.preventDefault(); astDrop.style.borderColor = '#7fff6e'; };
|
|
1349
|
+
astDrop.ondragleave = () => { astDrop.style.borderColor = ''; };
|
|
1350
|
+
astDrop.ondrop = e => {
|
|
1351
|
+
e.preventDefault(); astDrop.style.borderColor = '';
|
|
1352
|
+
loadAssets(e.dataTransfer.files);
|
|
1353
|
+
};
|
|
1354
|
+
|
|
1355
|
+
function loadAssets(files) {
|
|
1356
|
+
[...files].forEach(file => {
|
|
1357
|
+
if (!file.type.startsWith('image/')) return;
|
|
1358
|
+
const reader = new FileReader();
|
|
1359
|
+
reader.onload = async ev => {
|
|
1360
|
+
try {
|
|
1361
|
+
const res = await fetch('/draply-upload', {
|
|
1362
|
+
method: 'POST',
|
|
1363
|
+
body: JSON.stringify({ name: file.name, base64: ev.target.result })
|
|
1364
|
+
});
|
|
1365
|
+
const data = await res.json();
|
|
1366
|
+
if (data.ok) {
|
|
1367
|
+
const asset = { id: Date.now() + Math.random(), name: file.name, src: data.url };
|
|
1368
|
+
astStore.push(asset);
|
|
1369
|
+
addThumb(asset);
|
|
1370
|
+
astHint.style.display = 'block';
|
|
1371
|
+
toast('🖼️ ' + file.name + ' loaded');
|
|
1440
1372
|
} else {
|
|
1441
|
-
|
|
1442
|
-
const prevBg = cs.backgroundImage;
|
|
1443
|
-
target.style.backgroundImage = `url('${pendingAsset.src}')`;
|
|
1444
|
-
target.style.backgroundSize = 'cover';
|
|
1445
|
-
rec(target, { backgroundImage: `url('${pendingAsset.src}')`, backgroundSize: 'cover' }, { backgroundImage: prevBg });
|
|
1446
|
-
toast('🖼️ Background set!');
|
|
1373
|
+
toast('⚠ Upload failed');
|
|
1447
1374
|
}
|
|
1375
|
+
} catch (e) {
|
|
1376
|
+
toast('⚠ Upload failed');
|
|
1448
1377
|
}
|
|
1378
|
+
};
|
|
1379
|
+
reader.readAsDataURL(file);
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function addThumb(asset) {
|
|
1384
|
+
const img = document.createElement('img');
|
|
1385
|
+
img.className = 'ps-ast-thumb';
|
|
1386
|
+
img.src = asset.src;
|
|
1387
|
+
img.title = asset.name;
|
|
1388
|
+
img.dataset.assetId = asset.id;
|
|
1389
|
+
img.onclick = () => {
|
|
1390
|
+
// Deselect all thumbs
|
|
1391
|
+
document.querySelectorAll('.ps-ast-thumb').forEach(t => t.classList.remove('active'));
|
|
1392
|
+
img.classList.add('active');
|
|
1393
|
+
startPlacing(asset);
|
|
1394
|
+
};
|
|
1395
|
+
astList.appendChild(img);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// Start placing: attach ghost image to cursor
|
|
1399
|
+
function startPlacing(asset) {
|
|
1400
|
+
cancelPlacing();
|
|
1401
|
+
pendingAsset = asset;
|
|
1402
|
+
|
|
1403
|
+
// Create ghost
|
|
1404
|
+
placingEl = document.createElement('div');
|
|
1405
|
+
placingEl.className = 'ps-asset-placed placing';
|
|
1406
|
+
placingEl.style.cssText = 'width:120px;height:120px;pointer-events:none;opacity:.75;';
|
|
1407
|
+
placingEl.innerHTML = `<img src="${asset.src}" draggable="false">`;
|
|
1408
|
+
document.body.appendChild(placingEl);
|
|
1409
|
+
|
|
1410
|
+
toast('Click anywhere on page to place');
|
|
1411
|
+
document.addEventListener('mousemove', onPlacingMove);
|
|
1412
|
+
document.addEventListener('click', onPlacingClick, true);
|
|
1413
|
+
document.addEventListener('keydown', onPlacingCancel);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function onPlacingMove(e) {
|
|
1417
|
+
if (!placingEl) return;
|
|
1418
|
+
placingEl.style.left = (e.clientX - 60) + 'px';
|
|
1419
|
+
placingEl.style.top = (e.clientY - 60) + 'px';
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function onPlacingClick(e) {
|
|
1423
|
+
if (ps(e.target)) return; // ignore clicks inside our panel
|
|
1424
|
+
e.preventDefault(); e.stopPropagation();
|
|
1425
|
+
if (!pendingAsset || !placingEl) return;
|
|
1426
|
+
|
|
1427
|
+
if (e.shiftKey || e.altKey) {
|
|
1428
|
+
// #6: Better insertion targeting
|
|
1429
|
+
const target = e.target;
|
|
1430
|
+
if (target.tagName.toLowerCase() === 'img') {
|
|
1431
|
+
const prevSrc = target.getAttribute('src');
|
|
1432
|
+
target.src = pendingAsset.src;
|
|
1433
|
+
rec(target, { src: pendingAsset.src }, { src: prevSrc || '' });
|
|
1434
|
+
toast('🖼️ Image replaced!');
|
|
1449
1435
|
} else {
|
|
1450
|
-
//
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1436
|
+
// Option to insert INSIDE clicked container if Alt is held
|
|
1437
|
+
if (e.altKey) {
|
|
1438
|
+
placeAsset(pendingAsset, 0, 0, 120, 120, target);
|
|
1439
|
+
toast('✦ Placed inside ' + target.tagName.toLowerCase());
|
|
1440
|
+
} else {
|
|
1441
|
+
const cs = getComputedStyle(target);
|
|
1442
|
+
const prevBg = cs.backgroundImage;
|
|
1443
|
+
target.style.backgroundImage = `url('${pendingAsset.src}')`;
|
|
1444
|
+
target.style.backgroundSize = 'cover';
|
|
1445
|
+
rec(target, { backgroundImage: `url('${pendingAsset.src}')`, backgroundSize: 'cover' }, { backgroundImage: prevBg });
|
|
1446
|
+
toast('🖼️ Background set!');
|
|
1447
|
+
}
|
|
1454
1448
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
if (e.key === 'Escape') cancelPlacing();
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
function cancelPlacing() {
|
|
1464
|
-
if (placingEl) { placingEl.remove(); placingEl = null; }
|
|
1465
|
-
pendingAsset = null;
|
|
1466
|
-
document.querySelectorAll('.ps-ast-thumb').forEach(t => t.classList.remove('active'));
|
|
1467
|
-
document.removeEventListener('mousemove', onPlacingMove);
|
|
1468
|
-
document.removeEventListener('click', onPlacingClick, true);
|
|
1469
|
-
document.removeEventListener('keydown', onPlacingCancel);
|
|
1449
|
+
} else {
|
|
1450
|
+
// PLACE mode (standard)
|
|
1451
|
+
const x = e.clientX - 60;
|
|
1452
|
+
const y = e.clientY - 60;
|
|
1453
|
+
placeAsset(pendingAsset, x, y, 120, 120);
|
|
1470
1454
|
}
|
|
1455
|
+
cancelPlacing();
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function onPlacingCancel(e) {
|
|
1459
|
+
if (e.key === 'Escape') cancelPlacing();
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function cancelPlacing() {
|
|
1463
|
+
if (placingEl) { placingEl.remove(); placingEl = null; }
|
|
1464
|
+
pendingAsset = null;
|
|
1465
|
+
document.querySelectorAll('.ps-ast-thumb').forEach(t => t.classList.remove('active'));
|
|
1466
|
+
document.removeEventListener('mousemove', onPlacingMove);
|
|
1467
|
+
document.removeEventListener('click', onPlacingClick, true);
|
|
1468
|
+
document.removeEventListener('keydown', onPlacingCancel);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// #6: Place asset permanently on page (optionally inside a parent)
|
|
1472
|
+
function placeAsset(asset, x, y, w, h, parent = document.body) {
|
|
1473
|
+
const wrap = document.createElement('div');
|
|
1474
|
+
wrap.className = 'ps-asset-placed';
|
|
1475
|
+
const uid = 'ps-asset-' + Date.now();
|
|
1476
|
+
wrap.id = uid;
|
|
1477
|
+
|
|
1478
|
+
// If we have a specific parent, use relative positioning if needed
|
|
1479
|
+
const posType = parent === document.body ? 'fixed' : 'absolute';
|
|
1480
|
+
wrap.style.cssText = `position:${posType};left:${Math.round(x)}px;top:${Math.round(y)}px;width:${w}px;height:${h}px;z-index:1;`;
|
|
1481
|
+
wrap.innerHTML = `<img src="${asset.src}" draggable="false" alt="${asset.name}">`;
|
|
1482
|
+
parent.appendChild(wrap);
|
|
1483
|
+
|
|
1484
|
+
|
|
1485
|
+
// Click to select this placed asset (for z-index control)
|
|
1486
|
+
wrap.addEventListener('click', e => {
|
|
1487
|
+
if (ps(e.target)) return;
|
|
1488
|
+
selectPlaced(wrap);
|
|
1489
|
+
});
|
|
1471
1490
|
|
|
1472
|
-
//
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
const uid = 'ps-asset-' + Date.now();
|
|
1477
|
-
wrap.id = uid;
|
|
1478
|
-
|
|
1479
|
-
// If we have a specific parent, use relative positioning if needed
|
|
1480
|
-
const posType = parent === document.body ? 'fixed' : 'absolute';
|
|
1481
|
-
wrap.style.cssText = `position:${posType};left:${Math.round(x)}px;top:${Math.round(y)}px;width:${w}px;height:${h}px;z-index:1;`;
|
|
1482
|
-
wrap.innerHTML = `<img src="${asset.src}" draggable="false" alt="${asset.name}">`;
|
|
1483
|
-
parent.appendChild(wrap);
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
// Click to select this placed asset (for z-index control)
|
|
1487
|
-
wrap.addEventListener('click', e => {
|
|
1488
|
-
if (ps(e.target)) return;
|
|
1489
|
-
selectPlaced(wrap);
|
|
1490
|
-
});
|
|
1491
|
-
|
|
1492
|
-
// Make placed asset draggable
|
|
1493
|
-
wrap.addEventListener('mousedown', e => {
|
|
1494
|
-
if (ps(e.target)) return;
|
|
1495
|
-
e.preventDefault();
|
|
1496
|
-
selectPlaced(wrap);
|
|
1497
|
-
astDragEl = wrap;
|
|
1498
|
-
astDragSX = e.clientX; astDragSY = e.clientY;
|
|
1499
|
-
astDragOL = parseFloat(wrap.style.left) || 0;
|
|
1500
|
-
astDragOT = parseFloat(wrap.style.top) || 0;
|
|
1501
|
-
wrap.style.cursor = 'grabbing';
|
|
1502
|
-
state.dragging = true;
|
|
1503
|
-
});
|
|
1504
|
-
|
|
1505
|
-
rec(wrap, {
|
|
1506
|
-
src: asset.src,
|
|
1507
|
-
left: Math.round(x) + 'px',
|
|
1508
|
-
top: Math.round(y) + 'px',
|
|
1509
|
-
width: w + 'px',
|
|
1510
|
-
height: h + 'px',
|
|
1511
|
-
'z-index': '1',
|
|
1512
|
-
}, null, true); // true = isCreate
|
|
1513
|
-
|
|
1514
|
-
toast('✦ Placed — drag to reposition');
|
|
1491
|
+
// Make placed asset draggable
|
|
1492
|
+
wrap.addEventListener('mousedown', e => {
|
|
1493
|
+
if (ps(e.target)) return;
|
|
1494
|
+
e.preventDefault();
|
|
1515
1495
|
selectPlaced(wrap);
|
|
1516
|
-
|
|
1517
|
-
|
|
1496
|
+
astDragEl = wrap;
|
|
1497
|
+
astDragSX = e.clientX; astDragSY = e.clientY;
|
|
1498
|
+
astDragOL = parseFloat(wrap.style.left) || 0;
|
|
1499
|
+
astDragOT = parseFloat(wrap.style.top) || 0;
|
|
1500
|
+
wrap.style.cursor = 'grabbing';
|
|
1501
|
+
state.dragging = true;
|
|
1502
|
+
});
|
|
1518
1503
|
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1504
|
+
rec(wrap, {
|
|
1505
|
+
src: asset.src,
|
|
1506
|
+
left: Math.round(x) + 'px',
|
|
1507
|
+
top: Math.round(y) + 'px',
|
|
1508
|
+
width: w + 'px',
|
|
1509
|
+
height: h + 'px',
|
|
1510
|
+
'z-index': '1',
|
|
1511
|
+
}, null, true); // true = isCreate
|
|
1512
|
+
|
|
1513
|
+
toast('✦ Placed — drag to reposition');
|
|
1514
|
+
selectPlaced(wrap);
|
|
1515
|
+
return wrap;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// Track selected placed asset for z-index controls
|
|
1519
|
+
let selectedPlaced = null;
|
|
1520
|
+
const zCtrl = document.getElementById('__ast_zctrl__');
|
|
1521
|
+
const zVal = document.getElementById('__z_val__');
|
|
1522
|
+
const zFront = document.getElementById('__z_front__');
|
|
1523
|
+
const zBack = document.getElementById('__z_back__');
|
|
1524
|
+
const zSet = document.getElementById('__z_set__');
|
|
1525
|
+
|
|
1526
|
+
function selectPlaced(wrap) {
|
|
1527
|
+
// Deselect previous
|
|
1528
|
+
if (selectedPlaced) selectedPlaced.style.outline = '';
|
|
1529
|
+
selectedPlaced = wrap;
|
|
1530
|
+
wrap.style.outline = '2px solid #7fff6e';
|
|
1531
|
+
zVal.value = parseInt(wrap.style.zIndex) || 1;
|
|
1532
|
+
zCtrl.style.display = 'block';
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
zFront.onclick = () => {
|
|
1536
|
+
if (!selectedPlaced) return;
|
|
1537
|
+
// Find max z-index of all placed assets
|
|
1538
|
+
const max = Math.max(0, ...[...document.querySelectorAll('.ps-asset-placed')].map(el => parseInt(el.style.zIndex) || 0));
|
|
1539
|
+
const nz = max + 1;
|
|
1540
|
+
selectedPlaced.style.zIndex = nz;
|
|
1541
|
+
zVal.value = nz;
|
|
1542
|
+
rec(selectedPlaced, { 'z-index': String(nz) });
|
|
1543
|
+
toast('▲ Moved to front (z:' + nz + ')');
|
|
1544
|
+
};
|
|
1545
|
+
|
|
1546
|
+
zBack.onclick = () => {
|
|
1547
|
+
if (!selectedPlaced) return;
|
|
1548
|
+
const min = Math.min(0, ...[...document.querySelectorAll('.ps-asset-placed')].map(el => parseInt(el.style.zIndex) || 0));
|
|
1549
|
+
const nz = min - 1;
|
|
1550
|
+
selectedPlaced.style.zIndex = nz;
|
|
1551
|
+
zVal.value = nz;
|
|
1552
|
+
rec(selectedPlaced, { 'z-index': String(nz) });
|
|
1553
|
+
toast('▼ Moved to back (z:' + nz + ')');
|
|
1554
|
+
};
|
|
1555
|
+
|
|
1556
|
+
// #12: LAYERS PANEL LOGIC
|
|
1557
|
+
function updateLayers() {
|
|
1558
|
+
const list = document.getElementById('__lay_list__');
|
|
1559
|
+
list.innerHTML = '';
|
|
1560
|
+
|
|
1561
|
+
// Find interesting elements (headers, sections, divs with classes, images)
|
|
1562
|
+
const items = document.querySelectorAll('body > *:not(#__ps__), section *, header *, .container *');
|
|
1563
|
+
const filtered = [...items].filter(el => {
|
|
1564
|
+
if (ps(el)) return false;
|
|
1565
|
+
return el.tagName !== 'SCRIPT' && el.tagName !== 'STYLE' && (el.children.length === 0 || el.id || el.className);
|
|
1566
|
+
}).slice(0, 50); // limit for performance
|
|
1567
|
+
|
|
1568
|
+
if (!filtered.length) {
|
|
1569
|
+
list.innerHTML = '<div style="color:#444;font-size:10px;text-align:center;padding:10px">No elements found</div>';
|
|
1570
|
+
return;
|
|
1534
1571
|
}
|
|
1535
1572
|
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
rec(selectedPlaced, { 'z-index': String(nz) });
|
|
1544
|
-
toast('▲ Moved to front (z:' + nz + ')');
|
|
1545
|
-
};
|
|
1546
|
-
|
|
1547
|
-
zBack.onclick = () => {
|
|
1548
|
-
if (!selectedPlaced) return;
|
|
1549
|
-
const min = Math.min(0, ...[...document.querySelectorAll('.ps-asset-placed')].map(el => parseInt(el.style.zIndex) || 0));
|
|
1550
|
-
const nz = min - 1;
|
|
1551
|
-
selectedPlaced.style.zIndex = nz;
|
|
1552
|
-
zVal.value = nz;
|
|
1553
|
-
rec(selectedPlaced, { 'z-index': String(nz) });
|
|
1554
|
-
toast('▼ Moved to back (z:' + nz + ')');
|
|
1555
|
-
};
|
|
1556
|
-
|
|
1557
|
-
// #12: LAYERS PANEL LOGIC
|
|
1558
|
-
function updateLayers() {
|
|
1559
|
-
const list = document.getElementById('__lay_list__');
|
|
1560
|
-
list.innerHTML = '';
|
|
1561
|
-
|
|
1562
|
-
// Find interesting elements (headers, sections, divs with classes, images)
|
|
1563
|
-
const items = document.querySelectorAll('body > *:not(#__ps__), section *, header *, .container *');
|
|
1564
|
-
const filtered = [...items].filter(el => {
|
|
1565
|
-
if (ps(el)) return false;
|
|
1566
|
-
return el.tagName !== 'SCRIPT' && el.tagName !== 'STYLE' && (el.children.length === 0 || el.id || el.className);
|
|
1567
|
-
}).slice(0, 50); // limit for performance
|
|
1568
|
-
|
|
1569
|
-
if (!filtered.length) {
|
|
1570
|
-
list.innerHTML = '<div style="color:#444;font-size:10px;text-align:center;padding:10px">No elements found</div>';
|
|
1571
|
-
return;
|
|
1573
|
+
filtered.forEach(el => {
|
|
1574
|
+
const row = document.createElement('div');
|
|
1575
|
+
row.style.cssText = 'padding:6px 8px;border-radius:4px;background:#1a1a2a;border:1px solid #2a2a3a;cursor:pointer;display:flex;align-items:center;gap:8px;font-size:10px;color:#8080a8';
|
|
1576
|
+
if (state.selectedEl === el) {
|
|
1577
|
+
row.style.borderColor = '#7fff6e';
|
|
1578
|
+
row.style.background = 'rgba(127,255,110,0.1)';
|
|
1579
|
+
row.style.color = '#7fff6e';
|
|
1572
1580
|
}
|
|
1573
|
-
|
|
1574
|
-
filtered.forEach(el => {
|
|
1575
|
-
const row = document.createElement('div');
|
|
1576
|
-
row.style.cssText = 'padding:6px 8px;border-radius:4px;background:#1a1a2a;border:1px solid #2a2a3a;cursor:pointer;display:flex;align-items:center;gap:8px;font-size:10px;color:#8080a8';
|
|
1577
|
-
if (state.selectedEl === el) {
|
|
1578
|
-
row.style.borderColor = '#7fff6e';
|
|
1579
|
-
row.style.background = 'rgba(127,255,110,0.1)';
|
|
1580
|
-
row.style.color = '#7fff6e';
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
const icon = el.tagName === 'IMG' ? '🖼️' : el.tagName.match(/H[1-6]/) ? 'Tt' : '📦';
|
|
1584
|
-
const name = el.tagName.toLowerCase() + (el.id ? '#' + el.id : el.className ? '.' + el.className.split(' ')[0] : '');
|
|
1585
|
-
|
|
1586
|
-
row.innerHTML = `<span>${icon}</span> <span style="flex:1;overflow:hidden;text-overflow:ellipsis">${name}</span>`;
|
|
1587
|
-
row.onclick = () => {
|
|
1588
|
-
select(el);
|
|
1589
|
-
updateLayers();
|
|
1590
|
-
};
|
|
1591
|
-
list.appendChild(row);
|
|
1592
|
-
});
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
zSet.onclick = () => {
|
|
1596
|
-
if (!selectedPlaced) return;
|
|
1597
|
-
const nz = parseInt(zVal.value) || 0;
|
|
1598
|
-
selectedPlaced.style.zIndex = nz;
|
|
1599
|
-
rec(selectedPlaced, { 'z-index': String(nz) });
|
|
1600
|
-
toast('z-index set to ' + nz);
|
|
1601
|
-
};
|
|
1602
|
-
|
|
1603
|
-
zVal.onkeydown = e => { if (e.key === 'Enter') zSet.click(); };
|
|
1604
|
-
|
|
1605
|
-
// Drag placed assets
|
|
1606
|
-
document.addEventListener('mousemove', e => {
|
|
1607
|
-
if (!astDragEl || !state.dragging) return;
|
|
1608
|
-
const dx = e.clientX - astDragSX, dy = e.clientY - astDragSY;
|
|
1609
|
-
astDragEl.style.left = (astDragOL + dx) + 'px';
|
|
1610
|
-
astDragEl.style.top = (astDragOT + dy) + 'px';
|
|
1611
|
-
tip.textContent = `x:${Math.round(astDragOL + dx)} y:${Math.round(astDragOT + dy)}`;
|
|
1612
|
-
tip.style.left = (e.clientX + 14) + 'px'; tip.style.top = (e.clientY - 28) + 'px';
|
|
1613
|
-
tip.classList.add('v');
|
|
1614
|
-
});
|
|
1615
1581
|
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
tip.classList.remove('v');
|
|
1619
|
-
const cs = getComputedStyle(astDragEl);
|
|
1620
|
-
const newLeft = Math.round(parseFloat(cs.left)) + 'px';
|
|
1621
|
-
const newTop = Math.round(parseFloat(cs.top)) + 'px';
|
|
1622
|
-
if (newLeft !== (astDragOL + 'px') || newTop !== (astDragOT + 'px')) {
|
|
1623
|
-
rec(astDragEl, { left: newLeft, top: newTop });
|
|
1624
|
-
}
|
|
1625
|
-
astDragEl.style.cursor = 'grab';
|
|
1626
|
-
astDragEl = null;
|
|
1627
|
-
});
|
|
1582
|
+
const icon = el.tagName === 'IMG' ? '🖼️' : el.tagName.match(/H[1-6]/) ? 'Tt' : '📦';
|
|
1583
|
+
const name = el.tagName.toLowerCase() + (el.id ? '#' + el.id : el.className ? '.' + el.className.split(' ')[0] : '');
|
|
1628
1584
|
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
function rec(el, props, prevPropsOverride, isCreate = false) {
|
|
1636
|
-
const selector = el.dataset.pixelshiftId ? null : gsel(el);
|
|
1637
|
-
const key = el.dataset.pixelshiftId || selector;
|
|
1638
|
-
|
|
1639
|
-
// Use provided prevProps if given (snapshot taken before style was applied),
|
|
1640
|
-
// otherwise snapshot current inline values (for tools like Colors/Typography that call rec before apply)
|
|
1641
|
-
const prevProps = prevPropsOverride || {};
|
|
1642
|
-
if (!prevPropsOverride) {
|
|
1643
|
-
Object.keys(props).forEach(p => {
|
|
1644
|
-
const camel = p.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1645
|
-
prevProps[p] = el.style[camel] || '';
|
|
1585
|
+
row.innerHTML = `<span>${icon}</span> <span style="flex:1;overflow:hidden;text-overflow:ellipsis">${name}</span>`;
|
|
1586
|
+
row.onclick = () => {
|
|
1587
|
+
// Select element (inline logic — no separate select() function)
|
|
1588
|
+
document.querySelectorAll('.__ps__, .__ps_multi__').forEach(el2 => {
|
|
1589
|
+
el2.classList.remove('__ps__', '__ps_multi__');
|
|
1646
1590
|
});
|
|
1647
|
-
|
|
1591
|
+
state.selectedEls = [el];
|
|
1592
|
+
state.selectedEl = el;
|
|
1593
|
+
el.classList.add('__ps__');
|
|
1594
|
+
if (state.tool === 'mov') placeHdl(el);
|
|
1595
|
+
if (state.tool === 'rsz') placeRH(el);
|
|
1596
|
+
if (state.tool === 'clr') populateColors(el);
|
|
1597
|
+
if (state.tool === 'typ') populateTypo(el);
|
|
1598
|
+
updateLayers();
|
|
1599
|
+
};
|
|
1600
|
+
list.appendChild(row);
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
zSet.onclick = () => {
|
|
1605
|
+
if (!selectedPlaced) return;
|
|
1606
|
+
const nz = parseInt(zVal.value) || 0;
|
|
1607
|
+
selectedPlaced.style.zIndex = nz;
|
|
1608
|
+
rec(selectedPlaced, { 'z-index': String(nz) });
|
|
1609
|
+
toast('z-index set to ' + nz);
|
|
1610
|
+
};
|
|
1611
|
+
|
|
1612
|
+
zVal.onkeydown = e => { if (e.key === 'Enter') zSet.click(); };
|
|
1613
|
+
|
|
1614
|
+
// Drag placed assets
|
|
1615
|
+
document.addEventListener('mousemove', e => {
|
|
1616
|
+
if (!astDragEl || !state.dragging) return;
|
|
1617
|
+
const dx = e.clientX - astDragSX, dy = e.clientY - astDragSY;
|
|
1618
|
+
astDragEl.style.left = (astDragOL + dx) + 'px';
|
|
1619
|
+
astDragEl.style.top = (astDragOT + dy) + 'px';
|
|
1620
|
+
tip.textContent = `x:${Math.round(astDragOL + dx)} y:${Math.round(astDragOT + dy)}`;
|
|
1621
|
+
tip.style.left = (e.clientX + 14) + 'px'; tip.style.top = (e.clientY - 28) + 'px';
|
|
1622
|
+
tip.classList.add('v');
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1625
|
+
document.addEventListener('mouseup', () => {
|
|
1626
|
+
if (!astDragEl) return;
|
|
1627
|
+
tip.classList.remove('v');
|
|
1628
|
+
const cs = getComputedStyle(astDragEl);
|
|
1629
|
+
const newLeft = Math.round(parseFloat(cs.left)) + 'px';
|
|
1630
|
+
const newTop = Math.round(parseFloat(cs.top)) + 'px';
|
|
1631
|
+
if (newLeft !== (astDragOL + 'px') || newTop !== (astDragOT + 'px')) {
|
|
1632
|
+
rec(astDragEl, { left: newLeft, top: newTop });
|
|
1633
|
+
}
|
|
1634
|
+
astDragEl.style.cursor = 'grab';
|
|
1635
|
+
astDragEl = null;
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
// ══════════════════════════════════════════
|
|
1639
|
+
// RECORD + SAVE
|
|
1640
|
+
// ══════════════════════════════════════════
|
|
1641
|
+
// Full history — every individual action
|
|
1642
|
+
const history = [];
|
|
1643
|
+
const redoHistory = [];
|
|
1644
|
+
|
|
1645
|
+
function rec(el, props, prevPropsOverride, isCreate = false) {
|
|
1646
|
+
const selector = el.dataset.pixelshiftId ? null : gsel(el);
|
|
1647
|
+
const key = el.dataset.pixelshiftId || selector;
|
|
1648
|
+
|
|
1649
|
+
// Use provided prevProps if given (snapshot taken before style was applied),
|
|
1650
|
+
// otherwise snapshot current inline values (for tools like Colors/Typography that call rec before apply)
|
|
1651
|
+
const prevProps = prevPropsOverride || {};
|
|
1652
|
+
if (!prevPropsOverride) {
|
|
1653
|
+
Object.keys(props).forEach(p => {
|
|
1654
|
+
const camel = p.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1655
|
+
prevProps[p] = el.style[camel] || '';
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1648
1658
|
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
}
|
|
1659
|
+
// Extract exact file from React Fiber if available
|
|
1660
|
+
function getReactSource(element) {
|
|
1661
|
+
for (const key in element) {
|
|
1662
|
+
if (key.startsWith('__reactFiber$')) {
|
|
1663
|
+
let fiber = element[key];
|
|
1664
|
+
while (fiber) {
|
|
1665
|
+
if (fiber._debugSource && fiber._debugSource.fileName) return fiber._debugSource.fileName;
|
|
1666
|
+
fiber = fiber.return;
|
|
1658
1667
|
}
|
|
1659
1668
|
}
|
|
1660
|
-
return null;
|
|
1661
1669
|
}
|
|
1670
|
+
return null;
|
|
1671
|
+
}
|
|
1662
1672
|
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1673
|
+
// Merge into state.changes (for save/apply)
|
|
1674
|
+
const ch = {
|
|
1675
|
+
type: isCreate ? 'create' : (el.dataset.pixelshiftId ? 'inline' : 'css'),
|
|
1676
|
+
isCreate,
|
|
1677
|
+
pixelshiftId: el.dataset.pixelshiftId || null,
|
|
1678
|
+
selector,
|
|
1679
|
+
exactFile: getReactSource(el) || window.location.pathname.replace(/^\//, '') || null, // fallback for vanilla HTML (#7)
|
|
1680
|
+
file: el.dataset.pixelshiftFile || null,
|
|
1681
|
+
props,
|
|
1682
|
+
tagName: el.tagName.toLowerCase(),
|
|
1683
|
+
outerHTML: el.outerHTML // Send the whole element for 'create' actions
|
|
1684
|
+
};
|
|
1685
|
+
const i = state.changes.findIndex(c => (c.pixelshiftId || c.selector) === key);
|
|
1686
|
+
if (i >= 0) Object.assign(state.changes[i].props, props); else state.changes.push(ch);
|
|
1677
1687
|
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
}
|
|
1688
|
+
// Deep check: only record if at least one property actually changed
|
|
1689
|
+
let changed = isCreate;
|
|
1690
|
+
if (!isCreate) {
|
|
1691
|
+
for (const p in props) {
|
|
1692
|
+
if (String(props[p]) !== String(prevProps[p])) { changed = true; break; }
|
|
1684
1693
|
}
|
|
1685
|
-
if (!changed) return;
|
|
1686
|
-
|
|
1687
|
-
// Push to history
|
|
1688
|
-
const hid = Date.now() + Math.random();
|
|
1689
|
-
history.push({ hid, el, props, prevProps, selector: key, isCreate });
|
|
1690
|
-
|
|
1691
|
-
updateUnsUI();
|
|
1692
1694
|
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1695
|
+
if (!changed) return;
|
|
1696
|
+
|
|
1697
|
+
// Push to history
|
|
1698
|
+
const hid = Date.now() + Math.random();
|
|
1699
|
+
history.push({
|
|
1700
|
+
hid, el, props, prevProps, selector: key, isCreate,
|
|
1701
|
+
parent: isCreate ? el.parentElement : null,
|
|
1702
|
+
nextSibling: isCreate ? el.nextElementSibling : null
|
|
1703
|
+
});
|
|
1704
|
+
|
|
1705
|
+
// Clear redo stack on new action
|
|
1706
|
+
redoHistory.length = 0;
|
|
1707
|
+
|
|
1708
|
+
updateUnsUI();
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
function updateUnsUI() {
|
|
1712
|
+
const n = state.changes.length;
|
|
1713
|
+
sv.disabled = n === 0; nb.textContent = history.length;
|
|
1714
|
+
uns.style.display = history.length ? 'flex' : 'none';
|
|
1715
|
+
|
|
1716
|
+
// Rebuild history list (newest first)
|
|
1717
|
+
unsList.innerHTML = '';
|
|
1718
|
+
[...history].reverse().forEach(h => {
|
|
1719
|
+
const propStr = Object.entries(h.props).map(([k, v]) => `${k}: ${v}`).join(', ');
|
|
1720
|
+
const row = document.createElement('div');
|
|
1721
|
+
row.style.cssText = 'display:flex;align-items:flex-start;gap:6px;background:#0d0d1a;border:1px solid #1e1e3a;border-radius:5px;padding:6px 8px;font-size:9px;';
|
|
1722
|
+
row.innerHTML = `
|
|
1706
1723
|
<div style="flex:1;overflow:hidden">
|
|
1707
1724
|
<div style="color:#7fff6e;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${h.selector || 'placed element'}</div>
|
|
1708
1725
|
<div style="color:#555577;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${propStr}</div>
|
|
1709
1726
|
</div>
|
|
1710
1727
|
<button data-hid="${h.hid}" style="background:none;border:1px solid #2a2a44;color:#555577;border-radius:3px;padding:2px 6px;font-size:9px;cursor:pointer;white-space:nowrap;flex-shrink:0">↩ revert</button>
|
|
1711
1728
|
`;
|
|
1712
|
-
|
|
1713
|
-
|
|
1729
|
+
row.querySelector('button').onclick = () => revertChange(h);
|
|
1730
|
+
unsList.appendChild(row);
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
function revertChange(h) {
|
|
1735
|
+
if (h.isCreate) {
|
|
1736
|
+
h.el.remove();
|
|
1737
|
+
} else {
|
|
1738
|
+
// Re-apply previous inline values
|
|
1739
|
+
Object.entries(h.prevProps).forEach(([prop, val]) => {
|
|
1740
|
+
const camel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1741
|
+
h.el.style[camel] = val;
|
|
1714
1742
|
});
|
|
1743
|
+
// Also handle text/html
|
|
1744
|
+
if (h.prevProps.innerHTML !== undefined) h.el.innerHTML = h.prevProps.innerHTML;
|
|
1745
|
+
if (h.prevProps.innerText !== undefined) h.el.innerText = h.prevProps.innerText;
|
|
1715
1746
|
}
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1747
|
+
// Remove from history and add to redo stack
|
|
1748
|
+
const idx = history.findIndex(x => x.hid === h.hid);
|
|
1749
|
+
if (idx >= 0) {
|
|
1750
|
+
redoHistory.push(history[idx]);
|
|
1751
|
+
history.splice(idx, 1);
|
|
1752
|
+
}
|
|
1753
|
+
// Rebuild state.changes — preserve all original fields (#1, #2)
|
|
1754
|
+
state.changes = [];
|
|
1755
|
+
history.forEach(x => {
|
|
1756
|
+
const key = x.selector;
|
|
1757
|
+
const i = state.changes.findIndex(c => (c.pixelshiftId || c.selector) === key);
|
|
1758
|
+
if (i >= 0) {
|
|
1759
|
+
Object.assign(state.changes[i].props, x.props);
|
|
1720
1760
|
} else {
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1761
|
+
state.changes.push({
|
|
1762
|
+
type: x.isCreate ? 'create' : 'css',
|
|
1763
|
+
isCreate: x.isCreate || false,
|
|
1764
|
+
selector: key,
|
|
1765
|
+
props: { ...x.props },
|
|
1766
|
+
outerHTML: x.isCreate && x.el ? x.el.outerHTML : undefined,
|
|
1767
|
+
tagName: x.el ? x.el.tagName?.toLowerCase() : undefined
|
|
1725
1768
|
});
|
|
1726
|
-
// Also handle text/html
|
|
1727
|
-
if (h.prevProps.innerHTML !== undefined) h.el.innerHTML = h.prevProps.innerHTML;
|
|
1728
|
-
if (h.prevProps.innerText !== undefined) h.el.innerText = h.prevProps.innerText;
|
|
1729
1769
|
}
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1770
|
+
});
|
|
1771
|
+
updateUnsUI();
|
|
1772
|
+
toast('↩ Reverted');
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
function redoChange() {
|
|
1776
|
+
if (redoHistory.length === 0) {
|
|
1777
|
+
toast('Nothing to redo');
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
const h = redoHistory.pop();
|
|
1781
|
+
|
|
1782
|
+
if (h.isCreate) {
|
|
1783
|
+
if (h.parent) {
|
|
1784
|
+
if (h.nextSibling) {
|
|
1785
|
+
h.parent.insertBefore(h.el, h.nextSibling);
|
|
1740
1786
|
} else {
|
|
1741
|
-
|
|
1742
|
-
type: x.isCreate ? 'create' : 'css',
|
|
1743
|
-
isCreate: x.isCreate || false,
|
|
1744
|
-
selector: key,
|
|
1745
|
-
props: { ...x.props },
|
|
1746
|
-
outerHTML: x.isCreate && x.el ? x.el.outerHTML : undefined,
|
|
1747
|
-
tagName: x.el ? x.el.tagName?.toLowerCase() : undefined
|
|
1748
|
-
});
|
|
1787
|
+
h.parent.appendChild(h.el);
|
|
1749
1788
|
}
|
|
1789
|
+
} else {
|
|
1790
|
+
document.body.appendChild(h.el);
|
|
1791
|
+
}
|
|
1792
|
+
} else {
|
|
1793
|
+
// Re-apply properties
|
|
1794
|
+
Object.entries(h.props).forEach(([prop, val]) => {
|
|
1795
|
+
const camel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1796
|
+
h.el.style[camel] = val;
|
|
1750
1797
|
});
|
|
1751
|
-
|
|
1752
|
-
|
|
1798
|
+
if (h.props.innerHTML !== undefined) h.el.innerHTML = h.props.innerHTML;
|
|
1799
|
+
if (h.props.innerText !== undefined) h.el.innerText = h.props.innerText;
|
|
1753
1800
|
}
|
|
1801
|
+
|
|
1802
|
+
history.push(h);
|
|
1754
1803
|
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
const key = provider === 'ollama' ? 'local' : prompt(`Enter your ${provider.toUpperCase()} API key:`);
|
|
1771
|
-
if (key) {
|
|
1772
|
-
sv.disabled = true; sv.textContent = 'Validating...';
|
|
1773
|
-
try {
|
|
1774
|
-
const vRes = await fetch('/draply-validate-key', { method: 'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ apiKey: key.trim(), provider: provider.trim() }) });
|
|
1775
|
-
const vData = await vRes.json();
|
|
1776
|
-
if (!vData.valid && provider !== 'ollama') {
|
|
1777
|
-
toast('⚠ Invalid API key'); sv.disabled = false; sv.textContent = 'Save'; return;
|
|
1778
|
-
}
|
|
1779
|
-
} catch { /* allow through if validation endpoint unavailable */ }
|
|
1780
|
-
await fetch('/draply-config', { method: 'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ apiKey: key.trim(), provider: provider.trim() }) });
|
|
1781
|
-
sv.disabled = false; sv.textContent = 'Save';
|
|
1782
|
-
} else {
|
|
1783
|
-
toast('Save aborted: API Key required');
|
|
1784
|
-
return;
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
// Progress indicator with animation (#9)
|
|
1789
|
-
sv.disabled = true;
|
|
1790
|
-
sv.textContent = '';
|
|
1791
|
-
sv.innerHTML = '<span style="display:inline-flex;align-items:center;gap:4px"><span style="animation:spin 1s linear infinite;display:inline-block">@</span> Applying...</span>';
|
|
1792
|
-
// Add spin keyframe if not exists
|
|
1793
|
-
if (!document.getElementById('__ps_spin_style__')) {
|
|
1794
|
-
const spinStyle = document.createElement('style');
|
|
1795
|
-
spinStyle.id = '__ps_spin_style__';
|
|
1796
|
-
spinStyle.textContent = '@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }';
|
|
1797
|
-
document.head.appendChild(spinStyle);
|
|
1798
|
-
}
|
|
1799
|
-
try {
|
|
1800
|
-
const r = await fetch('/draply-ai-apply', {
|
|
1801
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1802
|
-
body: JSON.stringify({ changes: state.changes })
|
|
1804
|
+
// Rebuild state.changes
|
|
1805
|
+
state.changes = [];
|
|
1806
|
+
history.forEach(x => {
|
|
1807
|
+
const key = x.selector;
|
|
1808
|
+
const i = state.changes.findIndex(c => (c.pixelshiftId || c.selector) === key);
|
|
1809
|
+
if (i >= 0) {
|
|
1810
|
+
Object.assign(state.changes[i].props, x.props);
|
|
1811
|
+
} else {
|
|
1812
|
+
state.changes.push({
|
|
1813
|
+
type: x.isCreate ? 'create' : 'css',
|
|
1814
|
+
isCreate: x.isCreate || false,
|
|
1815
|
+
selector: key,
|
|
1816
|
+
props: { ...x.props },
|
|
1817
|
+
outerHTML: x.isCreate && x.el ? x.el.outerHTML : undefined,
|
|
1818
|
+
tagName: x.el ? x.el.tagName?.toLowerCase() : undefined
|
|
1803
1819
|
});
|
|
1804
|
-
const d = await r.json();
|
|
1805
|
-
if (d.ok) {
|
|
1806
|
-
const msg = d.fallback ? 'Saved to draply.css (No AI Key)' : `✅ AI Applied ${d.applied || ''}/${d.total || ''} to Source!`;
|
|
1807
|
-
toast(msg);
|
|
1808
|
-
} else toast('⚠ Error: ' + (d.error || 'unknown'));
|
|
1809
|
-
} catch {
|
|
1810
|
-
toast('⚠ Server unreachable');
|
|
1811
1820
|
}
|
|
1812
|
-
|
|
1813
|
-
state.changes = []; history.length = 0;
|
|
1814
|
-
sv.innerHTML = 'Save'; sv.textContent = 'Save';
|
|
1815
|
-
updateUnsUI();
|
|
1816
1821
|
});
|
|
1817
1822
|
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1823
|
+
updateUnsUI();
|
|
1824
|
+
toast('↷ Redone');
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
sv.addEventListener('click', async () => {
|
|
1828
|
+
// Check key config status
|
|
1829
|
+
let hasKey = false;
|
|
1830
|
+
let cfgProvider = 'groq';
|
|
1831
|
+
try {
|
|
1832
|
+
const cfgRes = await fetch('/draply-config');
|
|
1833
|
+
const cfg = await cfgRes.json();
|
|
1834
|
+
hasKey = cfg.hasKey;
|
|
1835
|
+
cfgProvider = cfg.provider || 'groq';
|
|
1836
|
+
} catch (e) { }
|
|
1837
|
+
|
|
1838
|
+
if (!hasKey) {
|
|
1839
|
+
// Ask for provider first (#8)
|
|
1840
|
+
const provider = prompt('Draply AI Save: Choose provider (groq / openai / anthropic / ollama):', 'groq');
|
|
1841
|
+
if (!provider) { toast('Save aborted'); return; }
|
|
1842
|
+
const key = provider === 'ollama' ? 'local' : prompt(`Enter your ${provider.toUpperCase()} API key:`);
|
|
1843
|
+
if (key) {
|
|
1844
|
+
sv.disabled = true; sv.textContent = 'Validating...';
|
|
1845
|
+
try {
|
|
1846
|
+
const vRes = await fetch('/draply-validate-key', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: key.trim(), provider: provider.trim() }) });
|
|
1847
|
+
const vData = await vRes.json();
|
|
1848
|
+
if (!vData.valid && provider !== 'ollama') {
|
|
1849
|
+
toast('⚠ Invalid API key'); sv.disabled = false; sv.textContent = 'Save'; return;
|
|
1850
|
+
}
|
|
1851
|
+
} catch { /* allow through if validation endpoint unavailable */ }
|
|
1852
|
+
await fetch('/draply-config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: key.trim(), provider: provider.trim() }) });
|
|
1853
|
+
sv.disabled = false; sv.textContent = 'Save';
|
|
1854
|
+
} else {
|
|
1855
|
+
toast('Save aborted: API Key required');
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
// Progress indicator with animation (#9)
|
|
1861
|
+
sv.disabled = true;
|
|
1862
|
+
sv.textContent = '';
|
|
1863
|
+
sv.innerHTML = '<span style="display:inline-flex;align-items:center;gap:4px"><span style="animation:spin 1s linear infinite;display:inline-block">@</span> Applying...</span>';
|
|
1864
|
+
// Add spin keyframe if not exists
|
|
1865
|
+
if (!document.getElementById('__ps_spin_style__')) {
|
|
1866
|
+
const spinStyle = document.createElement('style');
|
|
1867
|
+
spinStyle.id = '__ps_spin_style__';
|
|
1868
|
+
spinStyle.textContent = '@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }';
|
|
1869
|
+
document.head.appendChild(spinStyle);
|
|
1870
|
+
}
|
|
1871
|
+
try {
|
|
1872
|
+
const r = await fetch('/draply-ai-apply', {
|
|
1873
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1874
|
+
body: JSON.stringify({ changes: state.changes })
|
|
1875
|
+
});
|
|
1876
|
+
const d = await r.json();
|
|
1877
|
+
if (d.ok) {
|
|
1878
|
+
const msg = d.fallback ? 'Saved to draply.css (No AI Key)' : `✅ AI Applied ${d.applied || ''}/${d.total || ''} to Source!`;
|
|
1879
|
+
toast(msg);
|
|
1880
|
+
} else toast('⚠ Error: ' + (d.error || 'unknown'));
|
|
1881
|
+
} catch {
|
|
1882
|
+
toast('⚠ Server unreachable');
|
|
1883
|
+
}
|
|
1822
1884
|
|
|
1823
|
-
|
|
1885
|
+
state.changes = []; history.length = 0; redoHistory.length = 0;
|
|
1886
|
+
sv.innerHTML = 'Save'; sv.textContent = 'Save';
|
|
1887
|
+
updateUnsUI();
|
|
1888
|
+
});
|
|
1824
1889
|
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
// Ignore if typing in an input/textarea/contenteditable
|
|
1830
|
-
const tag = e.target.tagName.toLowerCase();
|
|
1831
|
-
if (tag === 'input' || tag === 'textarea' || e.target.isContentEditable) return;
|
|
1890
|
+
document.getElementById('__uns_clear__').onclick = () => {
|
|
1891
|
+
state.changes = []; history.length = 0; redoHistory.length = 0; updateUnsUI();
|
|
1892
|
+
toast('🗑 History cleared');
|
|
1893
|
+
};
|
|
1832
1894
|
|
|
1833
|
-
|
|
1834
|
-
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && e.key === 'z') {
|
|
1835
|
-
e.preventDefault();
|
|
1836
|
-
if (history.length > 0) {
|
|
1837
|
-
revertChange(history[history.length - 1]);
|
|
1838
|
-
} else {
|
|
1839
|
-
toast('Nothing to undo');
|
|
1840
|
-
}
|
|
1841
|
-
return;
|
|
1842
|
-
}
|
|
1895
|
+
document.getElementById('__uns_save__').onclick = () => sv.click();
|
|
1843
1896
|
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1897
|
+
// ══════════════════════════════════════════
|
|
1898
|
+
// KEYBOARD SHORTCUTS (#4, #5, #15)
|
|
1899
|
+
// ══════════════════════════════════════════
|
|
1900
|
+
document.addEventListener('keydown', e => {
|
|
1901
|
+
// Ignore if typing in an input/textarea/contenteditable
|
|
1902
|
+
const tag = e.target.tagName.toLowerCase();
|
|
1903
|
+
if (tag === 'input' || tag === 'textarea' || e.target.isContentEditable) return;
|
|
1850
1904
|
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
el.style.display = 'none';
|
|
1859
|
-
// Deselect
|
|
1860
|
-
el.classList.remove('__ps__', '__ps_multi__');
|
|
1861
|
-
state.selectedEl = null;
|
|
1862
|
-
state.selectedEls = [];
|
|
1863
|
-
hdl.classList.remove('v');
|
|
1864
|
-
toast('🗑 Element hidden (Delete)');
|
|
1865
|
-
}
|
|
1866
|
-
return;
|
|
1905
|
+
// Ctrl+Z — Undo last change (#4)
|
|
1906
|
+
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && e.key.toLowerCase() === 'z') {
|
|
1907
|
+
e.preventDefault();
|
|
1908
|
+
if (history.length > 0) {
|
|
1909
|
+
revertChange(history[history.length - 1]);
|
|
1910
|
+
} else {
|
|
1911
|
+
toast('Nothing to undo');
|
|
1867
1912
|
}
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1868
1915
|
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
return;
|
|
1876
|
-
}
|
|
1877
|
-
});
|
|
1916
|
+
// Ctrl+Shift+Z — Redo (implemented)
|
|
1917
|
+
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'z') {
|
|
1918
|
+
e.preventDefault();
|
|
1919
|
+
redoChange();
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1878
1922
|
|
|
1879
|
-
//
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
});
|
|
1923
|
+
// Delete / Backspace — remove selected element (#5)
|
|
1924
|
+
if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
1925
|
+
if (state.selectedEl && !ps(state.selectedEl)) {
|
|
1926
|
+
e.preventDefault();
|
|
1927
|
+
const el = state.selectedEl;
|
|
1928
|
+
// Record removal in history
|
|
1929
|
+
rec(el, { display: 'none' }, { display: el.style.display || '' });
|
|
1930
|
+
el.style.display = 'none';
|
|
1931
|
+
// Deselect
|
|
1932
|
+
el.classList.remove('__ps__', '__ps_multi__');
|
|
1933
|
+
state.selectedEl = null;
|
|
1934
|
+
state.selectedEls = [];
|
|
1892
1935
|
hdl.classList.remove('v');
|
|
1893
|
-
|
|
1894
|
-
toast('👁 Preview mode — press P to exit');
|
|
1895
|
-
} else {
|
|
1896
|
-
psRoot.style.display = '';
|
|
1897
|
-
toast('Editor restored');
|
|
1936
|
+
toast('🗑 Element hidden (Delete)');
|
|
1898
1937
|
}
|
|
1938
|
+
return;
|
|
1899
1939
|
}
|
|
1900
1940
|
|
|
1901
|
-
//
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
if (el.id) return '#' + el.id;
|
|
1907
|
-
// Build unique path up the DOM
|
|
1908
|
-
const parts = [];
|
|
1909
|
-
let cur = el;
|
|
1910
|
-
while (cur && cur !== document.body && cur !== document.documentElement) {
|
|
1911
|
-
if (cur.id) { parts.unshift('#' + cur.id); break; }
|
|
1912
|
-
const tag = cur.tagName.toLowerCase();
|
|
1913
|
-
const cls = [...cur.classList].filter(c => !c.startsWith('__') && !c.startsWith('ps-')).join('.');
|
|
1914
|
-
const siblings = cur.parentElement
|
|
1915
|
-
? [...cur.parentElement.children].filter(c => c.tagName === cur.tagName)
|
|
1916
|
-
: [];
|
|
1917
|
-
const idx = siblings.length > 1 ? `:nth-of-type(${siblings.indexOf(cur) + 1})` : '';
|
|
1918
|
-
parts.unshift(cls ? `${tag}.${cls}${idx}` : `${tag}${idx}`);
|
|
1919
|
-
cur = cur.parentElement;
|
|
1941
|
+
// Preview mode toggle: P key (#15)
|
|
1942
|
+
if (e.key === 'p' || e.key === 'P') {
|
|
1943
|
+
if (!e.ctrlKey && !e.metaKey) {
|
|
1944
|
+
e.preventDefault();
|
|
1945
|
+
togglePreview();
|
|
1920
1946
|
}
|
|
1921
|
-
return
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
1950
|
+
|
|
1951
|
+
// ══════════════════════════════════════════
|
|
1952
|
+
// PREVIEW MODE (#15)
|
|
1953
|
+
// ══════════════════════════════════════════
|
|
1954
|
+
let previewMode = false;
|
|
1955
|
+
function togglePreview() {
|
|
1956
|
+
previewMode = !previewMode;
|
|
1957
|
+
const psRoot = document.getElementById('__ps__');
|
|
1958
|
+
if (previewMode) {
|
|
1959
|
+
psRoot.style.display = 'none';
|
|
1960
|
+
// Remove all selection highlights
|
|
1961
|
+
document.querySelectorAll('.__ps__, .__ps_multi__, .__ph__').forEach(el => {
|
|
1962
|
+
el.classList.remove('__ps__', '__ps_multi__', '__ph__');
|
|
1963
|
+
});
|
|
1964
|
+
hdl.classList.remove('v');
|
|
1965
|
+
Object.values(rhs).forEach(h => h.classList.remove('v'));
|
|
1966
|
+
toast('👁 Preview mode — press P to exit');
|
|
1967
|
+
} else {
|
|
1968
|
+
psRoot.style.display = '';
|
|
1969
|
+
toast('Editor restored');
|
|
1922
1970
|
}
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// ══════════════════════════════════════════
|
|
1974
|
+
// UTILS
|
|
1975
|
+
// ══════════════════════════════════════════
|
|
1976
|
+
function ps(el) { return el && el.closest('#__ps__'); }
|
|
1977
|
+
function gsel(el) {
|
|
1978
|
+
if (el.id) return '#' + el.id;
|
|
1979
|
+
// Build unique path up the DOM
|
|
1980
|
+
const parts = [];
|
|
1981
|
+
let cur = el;
|
|
1982
|
+
while (cur && cur !== document.body && cur !== document.documentElement) {
|
|
1983
|
+
if (cur.id) { parts.unshift('#' + cur.id); break; }
|
|
1984
|
+
const tag = cur.tagName.toLowerCase();
|
|
1985
|
+
const cls = [...cur.classList].filter(c => !c.startsWith('__') && !c.startsWith('ps-')).join('.');
|
|
1986
|
+
const siblings = cur.parentElement
|
|
1987
|
+
? [...cur.parentElement.children].filter(c => c.tagName === cur.tagName)
|
|
1988
|
+
: [];
|
|
1989
|
+
const idx = siblings.length > 1 ? `:nth-of-type(${siblings.indexOf(cur) + 1})` : '';
|
|
1990
|
+
parts.unshift(cls ? `${tag}.${cls}${idx}` : `${tag}${idx}`);
|
|
1991
|
+
cur = cur.parentElement;
|
|
1926
1992
|
}
|
|
1927
|
-
|
|
1993
|
+
return parts.join(' > ');
|
|
1994
|
+
}
|
|
1995
|
+
function toast(msg) {
|
|
1996
|
+
tst.textContent = msg; tst.classList.add('v');
|
|
1997
|
+
clearTimeout(tst._t); tst._t = setTimeout(() => tst.classList.remove('v'), 2800);
|
|
1998
|
+
}
|
|
1999
|
+
}) ();
|