draply-dev 1.5.5 → 1.5.6
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 +236 -109
- package/package.json +27 -27
- package/src/overlay.js +1257 -1150
package/src/overlay.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
dragSX: 0, dragSY: 0, dragOrigL: 0, dragOrigT: 0,
|
|
10
10
|
dragOrigPositions: [], // [{el, origL, origT, prevL, prevT}] for multi-drag
|
|
11
11
|
resizeDir: '', resizeOrigW: 0, resizeOrigH: 0, resizeOrigX: 0, resizeOrigY: 0, resizeOrigL: 0, resizeOrigT: 0,
|
|
12
|
-
isPro: true, // set
|
|
12
|
+
isPro: window.__DRAPLY_PRO__ === true, // set via cli.js injection
|
|
13
13
|
changes: []
|
|
14
14
|
};
|
|
15
15
|
|
|
@@ -201,6 +201,10 @@
|
|
|
201
201
|
background: #1e1e2e;
|
|
202
202
|
margin: 5px 6px;
|
|
203
203
|
}
|
|
204
|
+
.pro-font {
|
|
205
|
+
color: #5cdb4f;
|
|
206
|
+
font-weight: 700;
|
|
207
|
+
}
|
|
204
208
|
|
|
205
209
|
/* ── SUB-PANELS ───────────────────────────────────── */
|
|
206
210
|
.ps-sub {
|
|
@@ -612,31 +616,119 @@
|
|
|
612
616
|
<div class="ps-sec" style="padding:8px 0 4px">Font</div>
|
|
613
617
|
<select class="ps-select" id="__typ_font__" style="width:100%;margin-bottom:6px">
|
|
614
618
|
<option value="">— keep current —</option>
|
|
615
|
-
<optgroup label="Sans-
|
|
619
|
+
<optgroup label="Free Sans-Serif">
|
|
616
620
|
<option value="Inter">Inter</option>
|
|
617
621
|
<option value="Roboto">Roboto</option>
|
|
622
|
+
<option value="Open Sans">Open Sans</option>
|
|
623
|
+
<option value="Lato">Lato</option>
|
|
624
|
+
<option value="Montserrat">Montserrat</option>
|
|
618
625
|
<option value="Poppins">Poppins</option>
|
|
619
|
-
<option value="
|
|
620
|
-
<option value="
|
|
621
|
-
<option value="
|
|
622
|
-
<option value="
|
|
626
|
+
<option value="Raleway">Raleway</option>
|
|
627
|
+
<option value="Nunito">Nunito</option>
|
|
628
|
+
<option value="Ubuntu">Ubuntu</option>
|
|
629
|
+
<option value="Rubik">Rubik</option>
|
|
630
|
+
<option value="Work Sans">Work Sans</option>
|
|
631
|
+
<option value="Quicksand">Quicksand</option>
|
|
632
|
+
<option value="Karla">Karla</option>
|
|
633
|
+
<option value="Josefin Sans">Josefin Sans</option>
|
|
634
|
+
<option value="Asap">Asap</option>
|
|
635
|
+
<option value="Hind">Hind</option>
|
|
636
|
+
<option value="Dosis">Dosis</option>
|
|
637
|
+
<option value="Cabin">Cabin</option>
|
|
638
|
+
<option value="Mukta">Mukta</option>
|
|
639
|
+
<option value="PT Sans">PT Sans</option>
|
|
640
|
+
</optgroup>
|
|
641
|
+
<optgroup label="Pro Sans-Serif">
|
|
642
|
+
<option value="Manrope" class="pro-font">Manrope ✦ PRO</option>
|
|
643
|
+
<option value="DM Sans" class="pro-font">DM Sans ✦ PRO</option>
|
|
644
|
+
<option value="Outfit" class="pro-font">Outfit ✦ PRO</option>
|
|
645
|
+
<option value="Plus Jakarta Sans" class="pro-font">Plus Jakarta Sans ✦ PRO</option>
|
|
646
|
+
<option value="Sora" class="pro-font">Sora ✦ PRO</option>
|
|
647
|
+
<option value="Epilogue" class="pro-font">Epilogue ✦ PRO</option>
|
|
648
|
+
<option value="Lexend" class="pro-font">Lexend ✦ PRO</option>
|
|
649
|
+
<option value="Urbanist" class="pro-font">Urbanist ✦ PRO</option>
|
|
650
|
+
<option value="Archivo" class="pro-font">Archivo ✦ PRO</option>
|
|
651
|
+
<option value="Bricolage Grotesque" class="pro-font">Bricolage Grotesque ✦ PRO</option>
|
|
652
|
+
<option value="Albert Sans" class="pro-font">Albert Sans ✦ PRO</option>
|
|
653
|
+
<option value="Schibsted Grotesk" class="pro-font">Schibsted Grotesk ✦ PRO</option>
|
|
654
|
+
<option value="Figtree" class="pro-font">Figtree ✦ PRO</option>
|
|
655
|
+
<option value="Hanken Grotesk" class="pro-font">Hanken Grotesk ✦ PRO</option>
|
|
656
|
+
<option value="Public Sans" class="pro-font">Public Sans ✦ PRO</option>
|
|
657
|
+
<option value="Be Vietnam Pro" class="pro-font">Be Vietnam Pro ✦ PRO</option>
|
|
658
|
+
<option value="Sen" class="pro-font">Sen ✦ PRO</option>
|
|
659
|
+
<option value="Chivo" class="pro-font">Chivo ✦ PRO</option>
|
|
660
|
+
<option value="Questrial" class="pro-font">Questrial ✦ PRO</option>
|
|
661
|
+
<option value="Jost" class="pro-font">Jost ✦ PRO</option>
|
|
623
662
|
</optgroup>
|
|
624
|
-
<optgroup label="Serif">
|
|
663
|
+
<optgroup label="Free Serif">
|
|
625
664
|
<option value="Playfair Display">Playfair Display</option>
|
|
626
665
|
<option value="Merriweather">Merriweather</option>
|
|
627
666
|
<option value="Lora">Lora</option>
|
|
667
|
+
<option value="PT Serif">PT Serif</option>
|
|
668
|
+
<option value="Noto Serif">Noto Serif</option>
|
|
669
|
+
<option value="Libre Baskerville">Libre Baskerville</option>
|
|
670
|
+
<option value="Arvo">Arvo</option>
|
|
671
|
+
<option value="Roboto Slab">Roboto Slab</option>
|
|
672
|
+
<option value="Bitter">Bitter</option>
|
|
673
|
+
<option value="Crimson Text">Crimson Text</option>
|
|
628
674
|
</optgroup>
|
|
629
|
-
<optgroup label="
|
|
630
|
-
<option value="
|
|
675
|
+
<optgroup label="Pro Serif">
|
|
676
|
+
<option value="EB Garamond" class="pro-font">EB Garamond ✦ PRO</option>
|
|
677
|
+
<option value="Cormorant" class="pro-font">Cormorant ✦ PRO</option>
|
|
678
|
+
<option value="Crimson Pro" class="pro-font">Crimson Pro ✦ PRO</option>
|
|
679
|
+
<option value="Zilla Slab" class="pro-font">Zilla Slab ✦ PRO</option>
|
|
680
|
+
<option value="Spectral" class="pro-font">Spectral ✦ PRO</option>
|
|
681
|
+
<option value="Fraunces" class="pro-font">Fraunces ✦ PRO</option>
|
|
682
|
+
<option value="Literata" class="pro-font">Literata ✦ PRO</option>
|
|
683
|
+
<option value="Petrona" class="pro-font">Petrona ✦ PRO</option>
|
|
684
|
+
<option value="Tinos" class="pro-font">Tinos ✦ PRO</option>
|
|
685
|
+
<option value="Domine" class="pro-font">Domine ✦ PRO</option>
|
|
686
|
+
<option value="Vesper Libre" class="pro-font">Vesper Libre ✦ PRO</option>
|
|
687
|
+
<option value="Prata" class="pro-font">Prata ✦ PRO</option>
|
|
688
|
+
<option value="Alice" class="pro-font">Alice ✦ PRO</option>
|
|
689
|
+
</optgroup>
|
|
690
|
+
<optgroup label="Free Display">
|
|
631
691
|
<option value="Oswald">Oswald</option>
|
|
632
|
-
<option value="
|
|
633
|
-
<option value="
|
|
634
|
-
<option value="
|
|
692
|
+
<option value="Bebas Neue">Bebas Neue</option>
|
|
693
|
+
<option value="Righteous">Righteous</option>
|
|
694
|
+
<option value="Fjalla One">Fjalla One</option>
|
|
695
|
+
<option value="Lobster">Lobster</option>
|
|
696
|
+
<option value="Pacifico">Pacifico</option>
|
|
697
|
+
<option value="Shadows Into Light">Shadows Into Light</option>
|
|
698
|
+
<option value="Dancing Script">Dancing Script</option>
|
|
699
|
+
<option value="Caveat">Caveat</option>
|
|
700
|
+
</optgroup>
|
|
701
|
+
<optgroup label="Pro Display">
|
|
702
|
+
<option value="Syne" class="pro-font">Syne ✦ PRO</option>
|
|
703
|
+
<option value="Space Grotesk" class="pro-font">Space Grotesk ✦ PRO</option>
|
|
704
|
+
<option value="Anton" class="pro-font">Anton ✦ PRO</option>
|
|
705
|
+
<option value="Titan One" class="pro-font">Titan One ✦ PRO</option>
|
|
706
|
+
<option value="Knewave" class="pro-font">Knewave ✦ PRO</option>
|
|
707
|
+
<option value="Bungee" class="pro-font">Bungee ✦ PRO</option>
|
|
708
|
+
<option value="Abril Fatface" class="pro-font">Abril Fatface ✦ PRO</option>
|
|
709
|
+
<option value="Cinzel" class="pro-font">Cinzel ✦ PRO</option>
|
|
710
|
+
<option value="Permanent Marker" class="pro-font">Permanent Marker ✦ PRO</option>
|
|
711
|
+
<option value="Alfa Slab One" class="pro-font">Alfa Slab One ✦ PRO</option>
|
|
712
|
+
<option value="Bangers" class="pro-font">Bangers ✦ PRO</option>
|
|
713
|
+
<option value="Creepster" class="pro-font">Creepster ✦ PRO</option>
|
|
714
|
+
<option value="Monoton" class="pro-font">Monoton ✦ PRO</option>
|
|
715
|
+
<option value="Black Ops One" class="pro-font">Black Ops One ✦ PRO</option>
|
|
716
|
+
<option value="Faster One" class="pro-font">Faster One ✦ PRO</option>
|
|
635
717
|
</optgroup>
|
|
636
|
-
<optgroup label="Mono">
|
|
718
|
+
<optgroup label="Free Mono">
|
|
637
719
|
<option value="Space Mono">Space Mono</option>
|
|
638
|
-
<option value="JetBrains Mono">JetBrains Mono</option>
|
|
639
720
|
<option value="Fira Code">Fira Code</option>
|
|
721
|
+
<option value="Roboto Mono">Roboto Mono</option>
|
|
722
|
+
<option value="Inconsolata">Inconsolata</option>
|
|
723
|
+
<option value="Source Code Pro">Source Code Pro</option>
|
|
724
|
+
</optgroup>
|
|
725
|
+
<optgroup label="Pro Mono">
|
|
726
|
+
<option value="JetBrains Mono" class="pro-font">JetBrains Mono ✦ PRO</option>
|
|
727
|
+
<option value="IBM Plex Mono" class="pro-font">IBM Plex Mono ✦ PRO</option>
|
|
728
|
+
<option value="Ubuntu Mono" class="pro-font">Ubuntu Mono ✦ PRO</option>
|
|
729
|
+
<option value="PT Mono" class="pro-font">PT Mono ✦ PRO</option>
|
|
730
|
+
<option value="Anonymous Pro" class="pro-font">Anonymous Pro ✦ PRO</option>
|
|
731
|
+
<option value="Cousine" class="pro-font">Cousine ✦ PRO</option>
|
|
640
732
|
</optgroup>
|
|
641
733
|
</select>
|
|
642
734
|
|
|
@@ -699,16 +791,15 @@
|
|
|
699
791
|
<div id="__ps_tst__"></div>
|
|
700
792
|
|
|
701
793
|
<!-- PAYWALL MODAL -->
|
|
702
|
-
<div id="__ps_paywall__" style="display:none;position:fixed;inset:0;z-index:2147483645;background:rgba(0,0,0,.82);backdrop-filter:blur(8px);align-items:center;justify-content:center;">
|
|
703
|
-
<div style="background
|
|
704
|
-
<div style="position:absolute;top:-
|
|
705
|
-
<div style="font-size:
|
|
706
|
-
<div style="color:#
|
|
707
|
-
<div
|
|
708
|
-
<div style="
|
|
709
|
-
|
|
710
|
-
<
|
|
711
|
-
<button id="__pw_close__" style="background:none;border:1px solid #1e1e3a;color:#44445a;border-radius:8px;padding:9px;font-size:10px;cursor:pointer;letter-spacing:.3px">Maybe later</button>
|
|
794
|
+
<div id="__ps_paywall__" style="display:none;position:fixed;inset:0;z-index:2147483645;background:rgba(0,0,0,.82);backdrop-filter:blur(8px);align-items:center;justify-content:center;font-family:system-ui,-apple-system,sans-serif;">
|
|
795
|
+
<div style="background:#131313;border:1px solid #2a2a3a;border-radius:16px;padding:48px 36px;width:340px;text-align:center;box-shadow:0 24px 80px rgba(0,0,0,.9),0 0 0 1px rgba(92,219,79,.1);position:relative;overflow:hidden;">
|
|
796
|
+
<div style="position:absolute;top:-60px;left:50%;transform:translateX(-50%);width:200px;height:200px;background:radial-gradient(circle,rgba(92,219,79,.1) 0%,transparent 70%);pointer-events:none"></div>
|
|
797
|
+
<div style="color:#fff;font-size:13px;font-weight:800;margin-bottom:8px;letter-spacing:1px;position:relative;text-transform:uppercase;">PRO FEATURE</div>
|
|
798
|
+
<div id="__pw_tool_name__" style="color:#5cdb4f;font-size:24px;font-weight:800;margin-bottom:16px;letter-spacing:.5px;position:relative"></div>
|
|
799
|
+
<div style="color:#a0a0b8;font-size:13px;line-height:1.6;margin-bottom:32px;position:relative">Inspect, Resize, Colors, Typography and Assets are available in <span style="color:#5cdb4f;font-weight:600">Draply Pro</span>.<br><br>Select and Move are always free.</div>
|
|
800
|
+
<div style="position:relative;display:flex;flex-direction:column;gap:12px">
|
|
801
|
+
<a id="__pw_upgrade__" href="http://localhost:3000" target="_blank" style="background:#5cdb4f;color:#131313;border:none;border-radius:10px;padding:14px;font-size:13px;font-weight:800;cursor:pointer;letter-spacing:.6px;text-decoration:none;display:block;">GET DRAPLY PRO →</a>
|
|
802
|
+
<button id="__pw_close__" style="background:none;border:1px solid #2a2a3a;color:#a0a0b8;border-radius:10px;padding:12px;font-size:12px;cursor:pointer;letter-spacing:.3px;">Maybe later</button>
|
|
712
803
|
</div>
|
|
713
804
|
</div>
|
|
714
805
|
</div>
|
|
@@ -772,8 +863,8 @@
|
|
|
772
863
|
|
|
773
864
|
Object.entries(toolBtns).forEach(([t, btn]) => btn.onclick = () => setT(t));
|
|
774
865
|
|
|
775
|
-
// Freemium: only sel
|
|
776
|
-
const FREE_TOOLS = ['sel', 'mov'];
|
|
866
|
+
// Freemium: only sel, mov, typ are free
|
|
867
|
+
const FREE_TOOLS = ['sel', 'mov', 'typ'];
|
|
777
868
|
const TOOL_LABELS = { ins: 'Inspect', rsz: 'Resize', clr: 'Colors', typ: 'Typography', ast: 'Assets' };
|
|
778
869
|
const paywall = document.getElementById('__ps_paywall__');
|
|
779
870
|
const pwToolName = document.getElementById('__pw_tool_name__');
|
|
@@ -829,1225 +920,1241 @@
|
|
|
829
920
|
|
|
830
921
|
// ── HOVER ────────────────────────────────────────────────────────────────
|
|
831
922
|
document.addEventListener('mouseover', e => {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
});
|
|
841
|
-
document.addEventListener('mouseout', e => {
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
});
|
|
845
|
-
document.addEventListener('mousemove', e => {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
});
|
|
849
|
-
|
|
850
|
-
// ── CLICK SELECT ─────────────────────────────────────────────────────────
|
|
851
|
-
document.addEventListener('click', e => {
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
t.classList.remove('__ps_multi__');
|
|
863
|
-
t.classList.remove('__ps__');
|
|
864
|
-
// If we removed the primary, promote the first remaining to primary
|
|
865
|
-
if (t === state.selectedEl) {
|
|
866
|
-
state.selectedEl = state.selectedEls[0] || null;
|
|
867
|
-
if (state.selectedEl) {
|
|
868
|
-
state.selectedEl.classList.remove('__ps_multi__');
|
|
869
|
-
state.selectedEl.classList.add('__ps__');
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
} else {
|
|
873
|
-
// Add to selection
|
|
874
|
-
state.selectedEls.push(t);
|
|
875
|
-
t.classList.add('__ps_multi__');
|
|
876
|
-
// If no primary yet, make this primary
|
|
877
|
-
if (!state.selectedEl) {
|
|
878
|
-
state.selectedEl = t;
|
|
923
|
+
if (!state.tool || ps(e.target)) return;
|
|
924
|
+
document.querySelectorAll('.__ph__').forEach(el => el.classList.remove('__ph__'));
|
|
925
|
+
e.target.classList.add('__ph__');
|
|
926
|
+
if (state.tool === 'ins') {
|
|
927
|
+
const r = e.target.getBoundingClientRect(), cs = getComputedStyle(e.target);
|
|
928
|
+
tip.textContent = `${Math.round(r.width)}×${Math.round(r.height)} ${cs.position} ${cs.display}`;
|
|
929
|
+
tip.classList.add('v');
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
document.addEventListener('mouseout', e => {
|
|
933
|
+
if (!ps(e.target)) e.target.classList.remove('__ph__');
|
|
934
|
+
if (state.tool === 'ins') tip.classList.remove('v');
|
|
935
|
+
});
|
|
936
|
+
document.addEventListener('mousemove', e => {
|
|
937
|
+
if (state.tool === 'ins') { tip.style.left = (e.clientX + 14) + 'px'; tip.style.top = (e.clientY - 28) + 'px'; }
|
|
938
|
+
if (state.dragging || state.resizing) { tip.style.left = (e.clientX + 14) + 'px'; tip.style.top = (e.clientY - 28) + 'px'; }
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
// ── CLICK SELECT ─────────────────────────────────────────────────────────
|
|
942
|
+
document.addEventListener('click', e => {
|
|
943
|
+
if (!state.tool || ps(e.target) || state.tool === 'ins') return;
|
|
944
|
+
e.preventDefault(); e.stopPropagation();
|
|
945
|
+
|
|
946
|
+
if (state.tool === 'mov' && e.ctrlKey) {
|
|
947
|
+
// Ctrl+click: toggle element in multi-select
|
|
948
|
+
const t = e.target;
|
|
949
|
+
const idx = state.selectedEls.indexOf(t);
|
|
950
|
+
if (idx >= 0) {
|
|
951
|
+
// Already in selection — remove it
|
|
952
|
+
state.selectedEls.splice(idx, 1);
|
|
879
953
|
t.classList.remove('__ps_multi__');
|
|
880
|
-
t.classList.
|
|
954
|
+
t.classList.remove('__ps__');
|
|
955
|
+
// If we removed the primary, promote the first remaining to primary
|
|
956
|
+
if (t === state.selectedEl) {
|
|
957
|
+
state.selectedEl = state.selectedEls[0] || null;
|
|
958
|
+
if (state.selectedEl) {
|
|
959
|
+
state.selectedEl.classList.remove('__ps_multi__');
|
|
960
|
+
state.selectedEl.classList.add('__ps__');
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
} else {
|
|
964
|
+
// Add to selection
|
|
965
|
+
state.selectedEls.push(t);
|
|
966
|
+
t.classList.add('__ps_multi__');
|
|
967
|
+
// If no primary yet, make this primary
|
|
968
|
+
if (!state.selectedEl) {
|
|
969
|
+
state.selectedEl = t;
|
|
970
|
+
t.classList.remove('__ps_multi__');
|
|
971
|
+
t.classList.add('__ps__');
|
|
972
|
+
}
|
|
881
973
|
}
|
|
974
|
+
// Show handle on primary if any selected
|
|
975
|
+
if (state.selectedEl) placeHdl(state.selectedEl);
|
|
976
|
+
else hdl.classList.remove('v');
|
|
977
|
+
return;
|
|
882
978
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
979
|
+
|
|
980
|
+
// Normal click (no Ctrl): clear all and select just this
|
|
981
|
+
document.querySelectorAll('.__ps__, .__ps_multi__').forEach(el => {
|
|
982
|
+
el.classList.remove('__ps__');
|
|
983
|
+
el.classList.remove('__ps_multi__');
|
|
984
|
+
});
|
|
985
|
+
state.selectedEls = [e.target];
|
|
986
|
+
state.selectedEl = e.target;
|
|
987
|
+
e.target.classList.add('__ps__');
|
|
988
|
+
|
|
989
|
+
if (state.tool === 'mov') { dEl = null; placeHdl(e.target); }
|
|
990
|
+
if (state.tool === 'rsz') { rzEl = null; rzDir = null; placeRH(e.target); }
|
|
991
|
+
if (state.tool === 'clr') populateColors(e.target);
|
|
992
|
+
if (state.tool === 'typ') populateTypo(e.target);
|
|
993
|
+
}, true);
|
|
994
|
+
|
|
995
|
+
// ══════════════════════════════════════════
|
|
996
|
+
// MOVE
|
|
997
|
+
// ══════════════════════════════════════════
|
|
998
|
+
function placeHdl(el) {
|
|
999
|
+
const r = el.getBoundingClientRect();
|
|
1000
|
+
const hdlW = 80; // approximate handle width
|
|
1001
|
+
// Center above element, but clamp inside viewport
|
|
1002
|
+
let left = r.left + r.width / 2 - hdlW / 2;
|
|
1003
|
+
let top = r.top - 36;
|
|
1004
|
+
// Clamp horizontally
|
|
1005
|
+
left = Math.max(8, Math.min(left, window.innerWidth - hdlW - 8));
|
|
1006
|
+
// If element is at top, show handle below instead
|
|
1007
|
+
if (top < 8) top = r.bottom + 8;
|
|
1008
|
+
hdl.style.left = left + 'px';
|
|
1009
|
+
hdl.style.top = top + 'px';
|
|
1010
|
+
hdl.classList.add('v');
|
|
887
1011
|
}
|
|
888
1012
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
// ══════════════════════════════════════════
|
|
907
|
-
function placeHdl(el) {
|
|
908
|
-
const r = el.getBoundingClientRect();
|
|
909
|
-
const hdlW = 80; // approximate handle width
|
|
910
|
-
// Center above element, but clamp inside viewport
|
|
911
|
-
let left = r.left + r.width / 2 - hdlW / 2;
|
|
912
|
-
let top = r.top - 36;
|
|
913
|
-
// Clamp horizontally
|
|
914
|
-
left = Math.max(8, Math.min(left, window.innerWidth - hdlW - 8));
|
|
915
|
-
// If element is at top, show handle below instead
|
|
916
|
-
if (top < 8) top = r.bottom + 8;
|
|
917
|
-
hdl.style.left = left + 'px';
|
|
918
|
-
hdl.style.top = top + 'px';
|
|
919
|
-
hdl.classList.add('v');
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
let dEl;
|
|
923
|
-
hdl.addEventListener('mousedown', e => {
|
|
924
|
-
if (!state.selectedEl) return;
|
|
925
|
-
e.preventDefault();
|
|
926
|
-
dEl = state.selectedEl;
|
|
927
|
-
state.dragSX = e.clientX; state.dragSY = e.clientY;
|
|
928
|
-
|
|
929
|
-
// Snapshot original positions for ALL selected elements
|
|
930
|
-
const els = state.selectedEls.length > 0 ? state.selectedEls : [state.selectedEl];
|
|
931
|
-
state.dragOrigPositions = els.map(el => {
|
|
932
|
-
const cs = getComputedStyle(el);
|
|
933
|
-
const origL = parseFloat(cs.left) || 0;
|
|
934
|
-
const origT = parseFloat(cs.top) || 0;
|
|
935
|
-
if (cs.position === 'static') el.style.position = 'relative';
|
|
936
|
-
el.style.transition = 'none';
|
|
937
|
-
return { el, origL, origT, prevL: origL + 'px', prevT: origT + 'px' };
|
|
938
|
-
});
|
|
1013
|
+
let dEl;
|
|
1014
|
+
hdl.addEventListener('mousedown', e => {
|
|
1015
|
+
if (!state.selectedEl) return;
|
|
1016
|
+
e.preventDefault();
|
|
1017
|
+
dEl = state.selectedEl;
|
|
1018
|
+
state.dragSX = e.clientX; state.dragSY = e.clientY;
|
|
1019
|
+
|
|
1020
|
+
// Snapshot original positions for ALL selected elements
|
|
1021
|
+
const els = state.selectedEls.length > 0 ? state.selectedEls : [state.selectedEl];
|
|
1022
|
+
state.dragOrigPositions = els.map(el => {
|
|
1023
|
+
const cs = getComputedStyle(el);
|
|
1024
|
+
const origL = parseFloat(cs.left) || 0;
|
|
1025
|
+
const origT = parseFloat(cs.top) || 0;
|
|
1026
|
+
if (cs.position === 'static') el.style.position = 'relative';
|
|
1027
|
+
el.style.transition = 'none';
|
|
1028
|
+
return { el, origL, origT, prevL: Math.round(origL) + 'px', prevT: Math.round(origT) + 'px' };
|
|
1029
|
+
});
|
|
939
1030
|
|
|
940
|
-
|
|
941
|
-
});
|
|
942
|
-
document.addEventListener('mousemove', e => {
|
|
943
|
-
if (!state.dragging) return;
|
|
944
|
-
const dx = e.clientX - state.dragSX, dy = e.clientY - state.dragSY;
|
|
945
|
-
state.dragOrigPositions.forEach(({ el, origL, origT }) => {
|
|
946
|
-
el.style.left = (origL + dx) + 'px';
|
|
947
|
-
el.style.top = (origT + dy) + 'px';
|
|
1031
|
+
state.dragging = true;
|
|
948
1032
|
});
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
});
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
const cs = getComputedStyle(el);
|
|
962
|
-
const newLeft = Math.round(parseFloat(cs.left)) + 'px';
|
|
963
|
-
const newTop = Math.round(parseFloat(cs.top)) + 'px';
|
|
964
|
-
if (newLeft !== prevL || newTop !== prevT) {
|
|
965
|
-
rec(el, { left: newLeft, top: newTop }, { left: prevL, top: prevT });
|
|
1033
|
+
document.addEventListener('mousemove', e => {
|
|
1034
|
+
if (!state.dragging) return;
|
|
1035
|
+
const dx = e.clientX - state.dragSX, dy = e.clientY - state.dragSY;
|
|
1036
|
+
state.dragOrigPositions.forEach(({ el, origL, origT }) => {
|
|
1037
|
+
el.style.left = (origL + dx) + 'px';
|
|
1038
|
+
el.style.top = (origT + dy) + 'px';
|
|
1039
|
+
});
|
|
1040
|
+
if (dEl) {
|
|
1041
|
+
placeHdl(dEl);
|
|
1042
|
+
const { origL, origT } = state.dragOrigPositions[0];
|
|
1043
|
+
tip.textContent = `x:${Math.round(origL + dx)} y:${Math.round(origT + dy)}`;
|
|
1044
|
+
tip.classList.add('v');
|
|
966
1045
|
}
|
|
967
1046
|
});
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1047
|
+
document.addEventListener('mouseup', () => {
|
|
1048
|
+
if (!state.dragging) return;
|
|
1049
|
+
state.dragging = false; tip.classList.remove('v');
|
|
1050
|
+
// Record each element separately
|
|
1051
|
+
state.dragOrigPositions.forEach(({ el, prevL, prevT }) => {
|
|
1052
|
+
const cs = getComputedStyle(el);
|
|
1053
|
+
const newLeft = Math.round(parseFloat(cs.left)) + 'px';
|
|
1054
|
+
const newTop = Math.round(parseFloat(cs.top)) + 'px';
|
|
1055
|
+
if (newLeft !== prevL || newTop !== prevT) {
|
|
1056
|
+
rec(el, { left: newLeft, top: newTop }, { left: prevL, top: prevT });
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
state.dragOrigPositions = [];
|
|
1060
|
+
dEl = null;
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
// ══════════════════════════════════════════
|
|
1064
|
+
// RESIZE
|
|
1065
|
+
// ══════════════════════════════════════════
|
|
1066
|
+
function placeRH(el) {
|
|
1067
|
+
const r = el.getBoundingClientRect();
|
|
1068
|
+
// Use viewport coords (fixed position handles need viewport coords, not page)
|
|
1069
|
+
const t = r.top, l = r.left;
|
|
1070
|
+
const hw = r.width, hh = r.height;
|
|
1071
|
+
const hs = 5; // half handle size for centering
|
|
1072
|
+
|
|
1073
|
+
const pos = {
|
|
1074
|
+
nw: [l - hs, t - hs],
|
|
1075
|
+
n: [l + hw / 2 - hs, t - hs],
|
|
1076
|
+
ne: [l + hw - hs, t - hs],
|
|
1077
|
+
w: [l - hs, t + hh / 2 - hs],
|
|
1078
|
+
e: [l + hw - hs, t + hh / 2 - hs],
|
|
1079
|
+
sw: [l - hs, t + hh - hs],
|
|
1080
|
+
s: [l + hw / 2 - hs, t + hh - hs],
|
|
1081
|
+
se: [l + hw - hs, t + hh - hs],
|
|
1082
|
+
};
|
|
1083
|
+
rhDirs.forEach(d => {
|
|
1084
|
+
rhs[d].style.left = pos[d][0] + 'px';
|
|
1085
|
+
rhs[d].style.top = pos[d][1] + 'px';
|
|
1086
|
+
rhs[d].classList.add('v');
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
let rzEl = null, rzDir = null;
|
|
992
1091
|
rhDirs.forEach(d => {
|
|
993
|
-
rhs[d].
|
|
994
|
-
|
|
995
|
-
|
|
1092
|
+
rhs[d].addEventListener('mousedown', e => {
|
|
1093
|
+
if (!state.selectedEl) return;
|
|
1094
|
+
e.preventDefault(); e.stopPropagation();
|
|
1095
|
+
rzEl = state.selectedEl; rzDir = d;
|
|
1096
|
+
const r = rzEl.getBoundingClientRect();
|
|
1097
|
+
const cs = getComputedStyle(rzEl);
|
|
1098
|
+
state.resizeOrigW = r.width; state.resizeOrigH = r.height;
|
|
1099
|
+
state.resizeOrigX = e.clientX; state.resizeOrigY = e.clientY;
|
|
1100
|
+
state.resizeOrigL = parseFloat(cs.left) || 0;
|
|
1101
|
+
state.resizeOrigT = parseFloat(cs.top) || 0;
|
|
1102
|
+
if (cs.position === 'static') rzEl.style.position = 'relative';
|
|
1103
|
+
rzEl.style.transition = 'none';
|
|
1104
|
+
state.resizing = true;
|
|
1105
|
+
});
|
|
996
1106
|
});
|
|
997
|
-
}
|
|
998
1107
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1108
|
+
document.addEventListener('mousemove', e => {
|
|
1109
|
+
if (!state.resizing || !rzEl) return;
|
|
1110
|
+
const dx = e.clientX - state.resizeOrigX, dy = e.clientY - state.resizeOrigY;
|
|
1111
|
+
let w = state.resizeOrigW, h = state.resizeOrigH;
|
|
1112
|
+
let l = state.resizeOrigL, t = state.resizeOrigT;
|
|
1113
|
+
|
|
1114
|
+
if (rzDir.includes('e')) w = Math.max(20, w + dx);
|
|
1115
|
+
if (rzDir.includes('s')) h = Math.max(20, h + dy);
|
|
1116
|
+
if (rzDir.includes('w')) { w = Math.max(20, w - dx); l = state.resizeOrigL + (state.resizeOrigW - w); }
|
|
1117
|
+
if (rzDir.includes('n')) { h = Math.max(20, h - dy); t = state.resizeOrigT + (state.resizeOrigH - h); }
|
|
1118
|
+
|
|
1119
|
+
rzEl.style.width = Math.round(w) + 'px';
|
|
1120
|
+
rzEl.style.height = Math.round(h) + 'px';
|
|
1121
|
+
rzEl.style.left = Math.round(l) + 'px';
|
|
1122
|
+
rzEl.style.top = Math.round(t) + 'px';
|
|
1123
|
+
placeRH(rzEl);
|
|
1124
|
+
tip.textContent = `${Math.round(w)}×${Math.round(h)}`;
|
|
1125
|
+
tip.classList.add('v');
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
document.addEventListener('mouseup', () => {
|
|
1129
|
+
if (!state.resizing || !rzEl) return;
|
|
1130
|
+
state.resizing = false; tip.classList.remove('v');
|
|
1006
1131
|
const cs = getComputedStyle(rzEl);
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1132
|
+
// prevProps = original values before resize started
|
|
1133
|
+
const prevProps = {
|
|
1134
|
+
width: Math.round(state.resizeOrigW) + 'px',
|
|
1135
|
+
height: Math.round(state.resizeOrigH) + 'px',
|
|
1136
|
+
left: Math.round(state.resizeOrigL) + 'px',
|
|
1137
|
+
top: Math.round(state.resizeOrigT) + 'px',
|
|
1138
|
+
};
|
|
1139
|
+
const newSize = {
|
|
1140
|
+
width: Math.round(parseFloat(cs.width)) + 'px',
|
|
1141
|
+
height: Math.round(parseFloat(cs.height)) + 'px',
|
|
1142
|
+
left: Math.round(parseFloat(cs.left)) + 'px',
|
|
1143
|
+
top: Math.round(parseFloat(cs.top)) + 'px',
|
|
1144
|
+
};
|
|
1145
|
+
if (newSize.width !== prevProps.width || newSize.height !== prevProps.height || newSize.left !== prevProps.left || newSize.top !== prevProps.top) {
|
|
1146
|
+
rec(rzEl, newSize, prevProps);
|
|
1147
|
+
}
|
|
1148
|
+
rzEl = null; rzDir = null;
|
|
1014
1149
|
});
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
const
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
const cs = getComputedStyle(rzEl);
|
|
1041
|
-
// prevProps = original values before resize started
|
|
1042
|
-
const prevProps = {
|
|
1043
|
-
width: Math.round(state.resizeOrigW) + 'px',
|
|
1044
|
-
height: Math.round(state.resizeOrigH) + 'px',
|
|
1045
|
-
left: Math.round(state.resizeOrigL) + 'px',
|
|
1046
|
-
top: Math.round(state.resizeOrigT) + 'px',
|
|
1150
|
+
|
|
1151
|
+
// ══════════════════════════════════════════
|
|
1152
|
+
// COLORS
|
|
1153
|
+
// ══════════════════════════════════════════
|
|
1154
|
+
const clrElName = document.getElementById('__clr_el_name__');
|
|
1155
|
+
const swBg = document.getElementById('__sw_bg__'), cpBg = document.getElementById('__cp_bg__');
|
|
1156
|
+
const swFg = document.getElementById('__sw_fg__'), cpFg = document.getElementById('__cp_fg__');
|
|
1157
|
+
const swBd = document.getElementById('__sw_bd__'), cpBd = document.getElementById('__cp_bd__');
|
|
1158
|
+
const cpBgTrans = document.getElementById('__cp_bg_trans__');
|
|
1159
|
+
const cpBdTrans = document.getElementById('__cp_bd_trans__');
|
|
1160
|
+
|
|
1161
|
+
function setupSwatch(sw, cp) {
|
|
1162
|
+
sw.onclick = () => cp.click();
|
|
1163
|
+
cp.oninput = () => { sw.style.background = cp.value; };
|
|
1164
|
+
}
|
|
1165
|
+
setupSwatch(swBg, cpBg);
|
|
1166
|
+
setupSwatch(swFg, cpFg);
|
|
1167
|
+
setupSwatch(swBd, cpBd);
|
|
1168
|
+
|
|
1169
|
+
// Transparent checkbox toggles swatch appearance
|
|
1170
|
+
cpBgTrans.onchange = () => {
|
|
1171
|
+
swBg.style.background = cpBgTrans.checked
|
|
1172
|
+
? 'repeating-conic-gradient(#444 0% 25%, #222 0% 50%) 0 0/10px 10px'
|
|
1173
|
+
: cpBg.value;
|
|
1174
|
+
swBg.style.opacity = cpBgTrans.checked ? '0.6' : '1';
|
|
1047
1175
|
};
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1176
|
+
cpBdTrans.onchange = () => {
|
|
1177
|
+
swBd.style.background = cpBdTrans.checked
|
|
1178
|
+
? 'repeating-conic-gradient(#444 0% 25%, #222 0% 50%) 0 0/10px 10px'
|
|
1179
|
+
: cpBd.value;
|
|
1180
|
+
swBd.style.opacity = cpBdTrans.checked ? '0.6' : '1';
|
|
1053
1181
|
};
|
|
1054
|
-
|
|
1055
|
-
|
|
1182
|
+
|
|
1183
|
+
function isTransparent(color) {
|
|
1184
|
+
return !color || color === 'transparent' || color === 'rgba(0, 0, 0, 0)';
|
|
1056
1185
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
cpBgTrans.
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1186
|
+
|
|
1187
|
+
function populateColors(el) {
|
|
1188
|
+
const cs = getComputedStyle(el);
|
|
1189
|
+
clrElName.textContent = el.tagName.toLowerCase() + (el.id ? '#' + el.id : el.className ? '.' + [...el.classList][0] : '');
|
|
1190
|
+
let bg = cs.backgroundColor;
|
|
1191
|
+
let fg = cs.color || '#000000';
|
|
1192
|
+
let bd = cs.borderColor || '#cccccc';
|
|
1193
|
+
const bw = cs.borderWidth;
|
|
1194
|
+
|
|
1195
|
+
// #14: Support SVG fill/stroke (#14)
|
|
1196
|
+
const isSvg = el.namespaceURI === 'http://www.w3.org/2000/svg';
|
|
1197
|
+
if (isSvg) {
|
|
1198
|
+
fg = cs.fill;
|
|
1199
|
+
bd = cs.stroke;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Background — detect transparent
|
|
1203
|
+
if (isTransparent(bg)) {
|
|
1204
|
+
cpBgTrans.checked = true;
|
|
1205
|
+
swBg.style.background = 'repeating-conic-gradient(#444 0% 25%, #222 0% 50%) 0 0/10px 10px';
|
|
1206
|
+
swBg.style.opacity = '0.6';
|
|
1207
|
+
} else {
|
|
1208
|
+
cpBgTrans.checked = false;
|
|
1209
|
+
cpBg.value = rgb2hex(bg);
|
|
1210
|
+
swBg.style.background = cpBg.value;
|
|
1211
|
+
swBg.style.opacity = '1';
|
|
1212
|
+
}
|
|
1213
|
+
cpFg.value = rgb2hex(fg); swFg.style.background = cpFg.value;
|
|
1214
|
+
// Border — detect none/transparent
|
|
1215
|
+
if (isTransparent(bd) || bw === '0px') {
|
|
1216
|
+
cpBdTrans.checked = true;
|
|
1217
|
+
swBd.style.background = 'repeating-conic-gradient(#444 0% 25%, #222 0% 50%) 0 0/10px 10px';
|
|
1218
|
+
swBd.style.opacity = '0.6';
|
|
1219
|
+
} else {
|
|
1220
|
+
cpBdTrans.checked = false;
|
|
1221
|
+
cpBd.value = rgb2hex(bd);
|
|
1222
|
+
swBd.style.background = cpBd.value;
|
|
1223
|
+
swBd.style.opacity = '1';
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// #11: Advanced styles detection
|
|
1227
|
+
const opVal = parseFloat(cs.opacity) || 1;
|
|
1228
|
+
document.getElementById('__st_opacity__').value = opVal;
|
|
1229
|
+
document.getElementById('__st_opacity_val__').textContent = opVal.toFixed(2);
|
|
1230
|
+
document.getElementById('__st_radius__').value = parseInt(cs.borderRadius) || 0;
|
|
1231
|
+
document.getElementById('__st_shadow__').value = cs.boxShadow === 'none' ? 'none' : '0 4px 12px rgba(0,0,0,0.15)'; // simple match
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// #11: Live preview for advanced styles
|
|
1235
|
+
document.getElementById('__st_opacity__').oninput = e => {
|
|
1236
|
+
const v = e.target.value;
|
|
1237
|
+
document.getElementById('__st_opacity_val__').textContent = parseFloat(v).toFixed(2);
|
|
1238
|
+
if (state.selectedEl) state.selectedEl.style.opacity = v;
|
|
1239
|
+
};
|
|
1240
|
+
document.getElementById('__st_radius__').oninput = e => {
|
|
1241
|
+
if (state.selectedEl) state.selectedEl.style.borderRadius = e.target.value + 'px';
|
|
1242
|
+
};
|
|
1243
|
+
document.getElementById('__st_shadow__').onchange = e => {
|
|
1244
|
+
if (state.selectedEl) state.selectedEl.style.boxShadow = e.target.value === 'none' ? '' : e.target.value;
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
document.getElementById('__clr_apply__').onclick = () => {
|
|
1248
|
+
if (!state.selectedEl) { toast('⚠ Select an element first'); return; }
|
|
1249
|
+
const el = state.selectedEl;
|
|
1250
|
+
const bgVal = cpBgTrans.checked ? 'transparent' : cpBg.value;
|
|
1251
|
+
const bdVal = cpBdTrans.checked ? 'none' : cpBd.value;
|
|
1252
|
+
const opVal = document.getElementById('__st_opacity__').value;
|
|
1253
|
+
const radVal = document.getElementById('__st_radius__').value + 'px';
|
|
1254
|
+
const shVal = document.getElementById('__st_shadow__').value;
|
|
1255
|
+
|
|
1256
|
+
const prevProps = {
|
|
1257
|
+
'background-color': el.style.backgroundColor || '',
|
|
1258
|
+
'color': el.style.color || '',
|
|
1259
|
+
'border': el.style.border || '',
|
|
1260
|
+
'opacity': el.style.opacity || '',
|
|
1261
|
+
'border-radius': el.style.borderRadius || '',
|
|
1262
|
+
'box-shadow': el.style.boxShadow || ''
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
if (el.namespaceURI === 'http://www.w3.org/2000/svg') {
|
|
1266
|
+
el.style.fill = cpFg.value;
|
|
1267
|
+
el.style.stroke = cpBdTrans.checked ? 'none' : bdVal;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
el.style.backgroundColor = bgVal;
|
|
1271
|
+
el.style.color = cpFg.value;
|
|
1272
|
+
el.style.opacity = opVal;
|
|
1273
|
+
el.style.borderRadius = radVal;
|
|
1274
|
+
el.style.boxShadow = shVal === 'none' ? '' : shVal;
|
|
1275
|
+
|
|
1276
|
+
if (cpBdTrans.checked) {
|
|
1277
|
+
el.style.border = 'none';
|
|
1278
|
+
} else {
|
|
1279
|
+
el.style.borderColor = bdVal;
|
|
1280
|
+
if (getComputedStyle(el).borderWidth === '0px') el.style.borderWidth = '1px';
|
|
1281
|
+
if (getComputedStyle(el).borderStyle === 'none') el.style.borderStyle = 'solid';
|
|
1282
|
+
}
|
|
1283
|
+
rec(el, {
|
|
1284
|
+
'background-color': bgVal, color: cpFg.value,
|
|
1285
|
+
'border': cpBdTrans.checked ? 'none' : `1px solid ${bdVal}`,
|
|
1286
|
+
'opacity': opVal, 'border-radius': radVal, 'box-shadow': shVal === 'none' ? '' : shVal
|
|
1287
|
+
}, prevProps);
|
|
1288
|
+
toast('🎨 Styles applied!');
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
function rgb2hex(rgb) {
|
|
1292
|
+
if (rgb.startsWith('#')) return rgb;
|
|
1293
|
+
const m = rgb.match(/\d+/g);
|
|
1294
|
+
if (!m || m.length < 3) return '#000000';
|
|
1295
|
+
return '#' + m.slice(0, 3).map(x => parseInt(x).toString(16).padStart(2, '0')).join('');
|
|
1109
1296
|
}
|
|
1110
1297
|
|
|
1111
|
-
//
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1298
|
+
// ══════════════════════════════════════════
|
|
1299
|
+
// TYPOGRAPHY
|
|
1300
|
+
// ══════════════════════════════════════════
|
|
1301
|
+
const typElName = document.getElementById('__typ_el_name__');
|
|
1302
|
+
const typSz = document.getElementById('__typ_sz__');
|
|
1303
|
+
const typLh = document.getElementById('__typ_lh__');
|
|
1304
|
+
const typLs = document.getElementById('__typ_ls__');
|
|
1305
|
+
const typFont = document.getElementById('__typ_font__');
|
|
1306
|
+
const typBold = document.getElementById('__typ_bold__');
|
|
1307
|
+
const typItalic = document.getElementById('__typ_italic__');
|
|
1308
|
+
const typUnder = document.getElementById('__typ_under__');
|
|
1309
|
+
const typStrike = document.getElementById('__typ_strike__');
|
|
1310
|
+
const typUpper = document.getElementById('__typ_upper__');
|
|
1311
|
+
const typLower = document.getElementById('__typ_lower__');
|
|
1312
|
+
|
|
1313
|
+
// Track toggle states
|
|
1314
|
+
const typState = { bold: false, italic: false, under: false, strike: false, upper: false, lower: false };
|
|
1315
|
+
|
|
1316
|
+
function setStyleBtn(btn, key, val) {
|
|
1317
|
+
typState[key] = val;
|
|
1318
|
+
btn.classList.toggle('on', val);
|
|
1121
1319
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1320
|
+
|
|
1321
|
+
typBold.onclick = () => setStyleBtn(typBold, 'bold', !typState.bold);
|
|
1322
|
+
typItalic.onclick = () => setStyleBtn(typItalic, 'italic', !typState.italic);
|
|
1323
|
+
typUnder.onclick = () => setStyleBtn(typUnder, 'under', !typState.under);
|
|
1324
|
+
typStrike.onclick = () => setStyleBtn(typStrike, 'strike', !typState.strike);
|
|
1325
|
+
typUpper.onclick = () => { setStyleBtn(typUpper, 'upper', !typState.upper); if (typState.upper) setStyleBtn(typLower, 'lower', false); };
|
|
1326
|
+
typLower.onclick = () => { setStyleBtn(typLower, 'lower', !typState.lower); if (typState.lower) setStyleBtn(typUpper, 'upper', false); };
|
|
1327
|
+
|
|
1328
|
+
// Loaded Google Fonts cache
|
|
1329
|
+
const loadedFonts = new Set();
|
|
1330
|
+
function loadGoogleFont(name) {
|
|
1331
|
+
if (!name || loadedFonts.has(name)) return;
|
|
1332
|
+
loadedFonts.add(name);
|
|
1333
|
+
const link = document.createElement('link');
|
|
1334
|
+
link.rel = 'stylesheet';
|
|
1335
|
+
link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(name)}:wght@400;700&display=swap`;
|
|
1336
|
+
document.head.appendChild(link);
|
|
1133
1337
|
}
|
|
1134
1338
|
|
|
1135
|
-
//
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
if (state.selectedEl) state.selectedEl.style.opacity = v;
|
|
1148
|
-
};
|
|
1149
|
-
document.getElementById('__st_radius__').oninput = e => {
|
|
1150
|
-
if (state.selectedEl) state.selectedEl.style.borderRadius = e.target.value + 'px';
|
|
1151
|
-
};
|
|
1152
|
-
document.getElementById('__st_shadow__').onchange = e => {
|
|
1153
|
-
if (state.selectedEl) state.selectedEl.style.boxShadow = e.target.value === 'none' ? '' : e.target.value;
|
|
1154
|
-
};
|
|
1155
|
-
|
|
1156
|
-
document.getElementById('__clr_apply__').onclick = () => {
|
|
1157
|
-
if (!state.selectedEl) { toast('⚠ Select an element first'); return; }
|
|
1158
|
-
const el = state.selectedEl;
|
|
1159
|
-
const bgVal = cpBgTrans.checked ? 'transparent' : cpBg.value;
|
|
1160
|
-
const bdVal = cpBdTrans.checked ? 'none' : cpBd.value;
|
|
1161
|
-
const opVal = document.getElementById('__st_opacity__').value;
|
|
1162
|
-
const radVal = document.getElementById('__st_radius__').value + 'px';
|
|
1163
|
-
const shVal = document.getElementById('__st_shadow__').value;
|
|
1164
|
-
|
|
1165
|
-
const prevProps = {
|
|
1166
|
-
'background-color': el.style.backgroundColor || '',
|
|
1167
|
-
'color': el.style.color || '',
|
|
1168
|
-
'border': el.style.border || '',
|
|
1169
|
-
'opacity': el.style.opacity || '',
|
|
1170
|
-
'border-radius': el.style.borderRadius || '',
|
|
1171
|
-
'box-shadow': el.style.boxShadow || ''
|
|
1339
|
+
// Live preview when font changes
|
|
1340
|
+
typFont.onchange = () => {
|
|
1341
|
+
const selectedOpt = typFont.options[typFont.selectedIndex];
|
|
1342
|
+
if (selectedOpt && selectedOpt.classList.contains('pro-font') && !state.isPro) {
|
|
1343
|
+
showPaywall('Premium Fonts');
|
|
1344
|
+
typFont.value = ''; // Revert to empty or you could store the previous value
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
if (typFont.value) {
|
|
1348
|
+
loadGoogleFont(typFont.value);
|
|
1349
|
+
if (state.selectedEl) previewTypo(state.selectedEl);
|
|
1350
|
+
}
|
|
1172
1351
|
};
|
|
1173
1352
|
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
el.
|
|
1353
|
+
function populateTypo(el) {
|
|
1354
|
+
// Make text directly editable
|
|
1355
|
+
if (!el.isContentEditable) {
|
|
1356
|
+
el.contentEditable = 'true';
|
|
1357
|
+
el.dataset.origText = el.innerHTML || '';
|
|
1358
|
+
el.focus();
|
|
1359
|
+
|
|
1360
|
+
const finishEdit = () => {
|
|
1361
|
+
el.contentEditable = 'false';
|
|
1362
|
+
el.removeEventListener('blur', finishEdit);
|
|
1363
|
+
if (el.innerHTML !== el.dataset.origText) {
|
|
1364
|
+
rec(el, { innerHTML: el.innerHTML }, { innerHTML: el.dataset.origText });
|
|
1365
|
+
toast('Text updated!');
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
el.addEventListener('blur', finishEdit);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
const cs = getComputedStyle(el);
|
|
1372
|
+
typElName.textContent = el.tagName.toLowerCase() + (el.id ? '#' + el.id : el.className ? '.' + [...el.classList].filter(c => !c.startsWith('__'))[0] : '');
|
|
1373
|
+
typSz.value = Math.round(parseFloat(cs.fontSize)) || 16;
|
|
1374
|
+
typLh.value = parseFloat(cs.lineHeight) ? (parseFloat(cs.lineHeight) / parseFloat(cs.fontSize)).toFixed(1) : '1.5';
|
|
1375
|
+
typLs.value = parseFloat(cs.letterSpacing) || 0;
|
|
1376
|
+
typFont.value = '';
|
|
1377
|
+
|
|
1378
|
+
// Detect current styles
|
|
1379
|
+
setStyleBtn(typBold, 'bold', parseInt(cs.fontWeight) >= 700);
|
|
1380
|
+
setStyleBtn(typItalic, 'italic', cs.fontStyle === 'italic');
|
|
1381
|
+
setStyleBtn(typUnder, 'under', cs.textDecoration.includes('underline'));
|
|
1382
|
+
setStyleBtn(typStrike, 'strike', cs.textDecoration.includes('line-through'));
|
|
1383
|
+
setStyleBtn(typUpper, 'upper', cs.textTransform === 'uppercase');
|
|
1384
|
+
setStyleBtn(typLower, 'lower', cs.textTransform === 'lowercase');
|
|
1385
|
+
|
|
1386
|
+
// Live preview on input change
|
|
1387
|
+
[typSz, typLh, typLs].forEach(inp => { inp.oninput = () => previewTypo(el); });
|
|
1177
1388
|
}
|
|
1178
1389
|
|
|
1179
|
-
el
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
el.style.
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1390
|
+
function previewTypo(el) {
|
|
1391
|
+
el.style.fontSize = typSz.value + 'px';
|
|
1392
|
+
el.style.lineHeight = typLh.value;
|
|
1393
|
+
el.style.letterSpacing = typLs.value + 'px';
|
|
1394
|
+
el.style.setProperty('font-weight', typState.bold ? '700' : '400', 'important');
|
|
1395
|
+
el.style.setProperty('font-style', typState.italic ? 'italic' : 'normal', 'important');
|
|
1396
|
+
const deco = [typState.under && 'underline', typState.strike && 'line-through'].filter(Boolean).join(' ') || 'none';
|
|
1397
|
+
el.style.setProperty('text-decoration', deco, 'important');
|
|
1398
|
+
el.style.textTransform = typState.upper ? 'uppercase' : typState.lower ? 'lowercase' : 'none';
|
|
1399
|
+
if (typFont.value) {
|
|
1400
|
+
loadGoogleFont(typFont.value);
|
|
1401
|
+
el.style.fontFamily = `'${typFont.value}', sans-serif`;
|
|
1402
|
+
}
|
|
1191
1403
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
// ══════════════════════════════════════════
|
|
1208
|
-
// TYPOGRAPHY
|
|
1209
|
-
// ══════════════════════════════════════════
|
|
1210
|
-
const typElName = document.getElementById('__typ_el_name__');
|
|
1211
|
-
const typSz = document.getElementById('__typ_sz__');
|
|
1212
|
-
const typLh = document.getElementById('__typ_lh__');
|
|
1213
|
-
const typLs = document.getElementById('__typ_ls__');
|
|
1214
|
-
const typFont = document.getElementById('__typ_font__');
|
|
1215
|
-
const typBold = document.getElementById('__typ_bold__');
|
|
1216
|
-
const typItalic = document.getElementById('__typ_italic__');
|
|
1217
|
-
const typUnder = document.getElementById('__typ_under__');
|
|
1218
|
-
const typStrike = document.getElementById('__typ_strike__');
|
|
1219
|
-
const typUpper = document.getElementById('__typ_upper__');
|
|
1220
|
-
const typLower = document.getElementById('__typ_lower__');
|
|
1221
|
-
|
|
1222
|
-
// Track toggle states
|
|
1223
|
-
const typState = { bold: false, italic: false, under: false, strike: false, upper: false, lower: false };
|
|
1224
|
-
|
|
1225
|
-
function setStyleBtn(btn, key, val) {
|
|
1226
|
-
typState[key] = val;
|
|
1227
|
-
btn.classList.toggle('on', val);
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
typBold.onclick = () => setStyleBtn(typBold, 'bold', !typState.bold);
|
|
1231
|
-
typItalic.onclick = () => setStyleBtn(typItalic, 'italic', !typState.italic);
|
|
1232
|
-
typUnder.onclick = () => setStyleBtn(typUnder, 'under', !typState.under);
|
|
1233
|
-
typStrike.onclick = () => setStyleBtn(typStrike, 'strike', !typState.strike);
|
|
1234
|
-
typUpper.onclick = () => { setStyleBtn(typUpper, 'upper', !typState.upper); if (typState.upper) setStyleBtn(typLower, 'lower', false); };
|
|
1235
|
-
typLower.onclick = () => { setStyleBtn(typLower, 'lower', !typState.lower); if (typState.lower) setStyleBtn(typUpper, 'upper', false); };
|
|
1236
|
-
|
|
1237
|
-
// Loaded Google Fonts cache
|
|
1238
|
-
const loadedFonts = new Set();
|
|
1239
|
-
function loadGoogleFont(name) {
|
|
1240
|
-
if (!name || loadedFonts.has(name)) return;
|
|
1241
|
-
loadedFonts.add(name);
|
|
1242
|
-
const link = document.createElement('link');
|
|
1243
|
-
link.rel = 'stylesheet';
|
|
1244
|
-
link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(name)}:wght@400;700&display=swap`;
|
|
1245
|
-
document.head.appendChild(link);
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
// Live preview when font changes
|
|
1249
|
-
typFont.onchange = () => {
|
|
1250
|
-
if (typFont.value) loadGoogleFont(typFont.value);
|
|
1251
|
-
};
|
|
1252
|
-
|
|
1253
|
-
function populateTypo(el) {
|
|
1254
|
-
// Make text directly editable
|
|
1255
|
-
if (!el.isContentEditable) {
|
|
1256
|
-
el.contentEditable = 'true';
|
|
1257
|
-
el.dataset.origText = el.innerHTML || '';
|
|
1258
|
-
el.focus();
|
|
1259
|
-
|
|
1260
|
-
const finishEdit = () => {
|
|
1261
|
-
el.contentEditable = 'false';
|
|
1262
|
-
el.removeEventListener('blur', finishEdit);
|
|
1263
|
-
if (el.innerHTML !== el.dataset.origText) {
|
|
1264
|
-
rec(el, { innerHTML: el.innerHTML }, { innerHTML: el.dataset.origText });
|
|
1265
|
-
toast('Text updated!');
|
|
1266
|
-
}
|
|
1404
|
+
|
|
1405
|
+
document.getElementById('__typ_apply__').onclick = () => {
|
|
1406
|
+
if (!state.selectedEl) { toast('⚠ Select a text element first'); return; }
|
|
1407
|
+
const el = state.selectedEl;
|
|
1408
|
+
const deco = [typState.under && 'underline', typState.strike && 'line-through'].filter(Boolean).join(' ') || 'none';
|
|
1409
|
+
const props = {
|
|
1410
|
+
'font-size': typSz.value + 'px',
|
|
1411
|
+
'font-weight': typState.bold ? '700' : '400',
|
|
1412
|
+
'font-style': typState.italic ? 'italic' : 'normal',
|
|
1413
|
+
'text-decoration': deco,
|
|
1414
|
+
'text-transform': typState.upper ? 'uppercase' : typState.lower ? 'lowercase' : 'none',
|
|
1415
|
+
'line-height': typLh.value,
|
|
1416
|
+
'letter-spacing': typLs.value + 'px',
|
|
1267
1417
|
};
|
|
1268
|
-
|
|
1418
|
+
if (typFont.value) {
|
|
1419
|
+
props['font-family'] = `'${typFont.value}', sans-serif`;
|
|
1420
|
+
props['googleFont'] = typFont.value;
|
|
1421
|
+
}
|
|
1422
|
+
// Snapshot BEFORE previewTypo applies styles
|
|
1423
|
+
const prevProps = {};
|
|
1424
|
+
Object.keys(props).forEach(p => {
|
|
1425
|
+
const camel = p.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1426
|
+
prevProps[p] = el.style[camel] || '';
|
|
1427
|
+
});
|
|
1428
|
+
previewTypo(el);
|
|
1429
|
+
rec(el, props, prevProps);
|
|
1430
|
+
toast('📝 Typography applied!');
|
|
1431
|
+
};
|
|
1432
|
+
|
|
1433
|
+
// ══════════════════════════════════════════
|
|
1434
|
+
// ASSETS
|
|
1435
|
+
// ══════════════════════════════════════════
|
|
1436
|
+
const astDrop = document.getElementById('__ast_drop__');
|
|
1437
|
+
const astFile = document.getElementById('__ast_file__');
|
|
1438
|
+
const astList = document.getElementById('__ast_list__');
|
|
1439
|
+
const astHint = document.getElementById('__ast_hint__');
|
|
1440
|
+
const astStore = []; // { id, name, src, url }
|
|
1441
|
+
let pendingAsset = null; // asset waiting to be placed
|
|
1442
|
+
let placingEl = null; // the ghost element following cursor
|
|
1443
|
+
let astDragEl = null; // placed asset being moved
|
|
1444
|
+
let astDragSX, astDragSY, astDragOL, astDragOT;
|
|
1445
|
+
|
|
1446
|
+
// Upload via click or drop
|
|
1447
|
+
astDrop.onclick = () => astFile.click();
|
|
1448
|
+
astFile.onchange = e => loadAssets(e.target.files);
|
|
1449
|
+
astDrop.ondragover = e => { e.preventDefault(); astDrop.style.borderColor = '#7fff6e'; };
|
|
1450
|
+
astDrop.ondragleave = () => { astDrop.style.borderColor = ''; };
|
|
1451
|
+
astDrop.ondrop = e => {
|
|
1452
|
+
e.preventDefault(); astDrop.style.borderColor = '';
|
|
1453
|
+
loadAssets(e.dataTransfer.files);
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
function loadAssets(files) {
|
|
1457
|
+
[...files].forEach(file => {
|
|
1458
|
+
if (!file.type.startsWith('image/')) return;
|
|
1459
|
+
const reader = new FileReader();
|
|
1460
|
+
reader.onload = async ev => {
|
|
1461
|
+
try {
|
|
1462
|
+
const res = await fetch('/draply-upload', {
|
|
1463
|
+
method: 'POST',
|
|
1464
|
+
body: JSON.stringify({ name: file.name, base64: ev.target.result })
|
|
1465
|
+
});
|
|
1466
|
+
const data = await res.json();
|
|
1467
|
+
if (data.ok) {
|
|
1468
|
+
const asset = { id: Date.now() + Math.random(), name: file.name, src: data.url };
|
|
1469
|
+
astStore.push(asset);
|
|
1470
|
+
addThumb(asset);
|
|
1471
|
+
astHint.style.display = 'block';
|
|
1472
|
+
toast('🖼️ ' + file.name + ' loaded');
|
|
1473
|
+
} else {
|
|
1474
|
+
toast('⚠ Upload failed');
|
|
1475
|
+
}
|
|
1476
|
+
} catch (e) {
|
|
1477
|
+
toast('⚠ Upload failed');
|
|
1478
|
+
}
|
|
1479
|
+
};
|
|
1480
|
+
reader.readAsDataURL(file);
|
|
1481
|
+
});
|
|
1269
1482
|
}
|
|
1270
1483
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
setStyleBtn(typLower, 'lower', cs.textTransform === 'lowercase');
|
|
1285
|
-
|
|
1286
|
-
// Live preview on input change
|
|
1287
|
-
[typSz, typLh, typLs].forEach(inp => { inp.oninput = () => previewTypo(el); });
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
function previewTypo(el) {
|
|
1291
|
-
el.style.fontSize = typSz.value + 'px';
|
|
1292
|
-
el.style.lineHeight = typLh.value;
|
|
1293
|
-
el.style.letterSpacing = typLs.value + 'px';
|
|
1294
|
-
el.style.setProperty('font-weight', typState.bold ? '700' : '400', 'important');
|
|
1295
|
-
el.style.setProperty('font-style', typState.italic ? 'italic' : 'normal', 'important');
|
|
1296
|
-
const deco = [typState.under && 'underline', typState.strike && 'line-through'].filter(Boolean).join(' ') || 'none';
|
|
1297
|
-
el.style.setProperty('text-decoration', deco, 'important');
|
|
1298
|
-
el.style.textTransform = typState.upper ? 'uppercase' : typState.lower ? 'lowercase' : 'none';
|
|
1299
|
-
if (typFont.value) {
|
|
1300
|
-
loadGoogleFont(typFont.value);
|
|
1301
|
-
el.style.fontFamily = `'${typFont.value}', sans-serif`;
|
|
1484
|
+
function addThumb(asset) {
|
|
1485
|
+
const img = document.createElement('img');
|
|
1486
|
+
img.className = 'ps-ast-thumb';
|
|
1487
|
+
img.src = asset.src;
|
|
1488
|
+
img.title = asset.name;
|
|
1489
|
+
img.dataset.assetId = asset.id;
|
|
1490
|
+
img.onclick = () => {
|
|
1491
|
+
// Deselect all thumbs
|
|
1492
|
+
document.querySelectorAll('.ps-ast-thumb').forEach(t => t.classList.remove('active'));
|
|
1493
|
+
img.classList.add('active');
|
|
1494
|
+
startPlacing(asset);
|
|
1495
|
+
};
|
|
1496
|
+
astList.appendChild(img);
|
|
1302
1497
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
'
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
props['googleFont'] = typFont.value;
|
|
1498
|
+
|
|
1499
|
+
// Start placing: attach ghost image to cursor
|
|
1500
|
+
function startPlacing(asset) {
|
|
1501
|
+
cancelPlacing();
|
|
1502
|
+
pendingAsset = asset;
|
|
1503
|
+
|
|
1504
|
+
// Create ghost
|
|
1505
|
+
placingEl = document.createElement('div');
|
|
1506
|
+
placingEl.className = 'ps-asset-placed placing';
|
|
1507
|
+
placingEl.style.cssText = 'width:120px;height:120px;pointer-events:none;opacity:.75;';
|
|
1508
|
+
placingEl.innerHTML = `<img src="${asset.src}" draggable="false">`;
|
|
1509
|
+
document.body.appendChild(placingEl);
|
|
1510
|
+
|
|
1511
|
+
toast('Click anywhere on page to place');
|
|
1512
|
+
document.addEventListener('mousemove', onPlacingMove);
|
|
1513
|
+
document.addEventListener('click', onPlacingClick, true);
|
|
1514
|
+
document.addEventListener('keydown', onPlacingCancel);
|
|
1321
1515
|
}
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
//
|
|
1336
|
-
const
|
|
1337
|
-
|
|
1338
|
-
const
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
astDrop.onclick = () => astFile.click();
|
|
1348
|
-
astFile.onchange = e => loadAssets(e.target.files);
|
|
1349
|
-
astDrop.ondragover = e => { e.preventDefault(); astDrop.style.borderColor = '#7fff6e'; };
|
|
1350
|
-
astDrop.ondragleave = () => { astDrop.style.borderColor = ''; };
|
|
1351
|
-
astDrop.ondrop = e => {
|
|
1352
|
-
e.preventDefault(); astDrop.style.borderColor = '';
|
|
1353
|
-
loadAssets(e.dataTransfer.files);
|
|
1354
|
-
};
|
|
1355
|
-
|
|
1356
|
-
function loadAssets(files) {
|
|
1357
|
-
[...files].forEach(file => {
|
|
1358
|
-
if (!file.type.startsWith('image/')) return;
|
|
1359
|
-
const reader = new FileReader();
|
|
1360
|
-
reader.onload = async ev => {
|
|
1361
|
-
try {
|
|
1362
|
-
const res = await fetch('/draply-upload', {
|
|
1363
|
-
method: 'POST',
|
|
1364
|
-
body: JSON.stringify({ name: file.name, base64: ev.target.result })
|
|
1365
|
-
});
|
|
1366
|
-
const data = await res.json();
|
|
1367
|
-
if (data.ok) {
|
|
1368
|
-
const asset = { id: Date.now() + Math.random(), name: file.name, src: data.url };
|
|
1369
|
-
astStore.push(asset);
|
|
1370
|
-
addThumb(asset);
|
|
1371
|
-
astHint.style.display = 'block';
|
|
1372
|
-
toast('🖼️ ' + file.name + ' loaded');
|
|
1516
|
+
|
|
1517
|
+
function onPlacingMove(e) {
|
|
1518
|
+
if (!placingEl) return;
|
|
1519
|
+
placingEl.style.left = (e.clientX - 60) + 'px';
|
|
1520
|
+
placingEl.style.top = (e.clientY - 60) + 'px';
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
function onPlacingClick(e) {
|
|
1524
|
+
if (ps(e.target)) return; // ignore clicks inside our panel
|
|
1525
|
+
e.preventDefault(); e.stopPropagation();
|
|
1526
|
+
if (!pendingAsset || !placingEl) return;
|
|
1527
|
+
|
|
1528
|
+
if (e.shiftKey || e.altKey) {
|
|
1529
|
+
// #6: Better insertion targeting
|
|
1530
|
+
const target = e.target;
|
|
1531
|
+
if (target.tagName.toLowerCase() === 'img') {
|
|
1532
|
+
const prevSrc = target.getAttribute('src');
|
|
1533
|
+
target.src = pendingAsset.src;
|
|
1534
|
+
rec(target, { src: pendingAsset.src }, { src: prevSrc || '' });
|
|
1535
|
+
toast('🖼️ Image replaced!');
|
|
1536
|
+
} else {
|
|
1537
|
+
// Option to insert INSIDE clicked container if Alt is held
|
|
1538
|
+
if (e.altKey) {
|
|
1539
|
+
placeAsset(pendingAsset, 0, 0, 120, 120, target);
|
|
1540
|
+
toast('✦ Placed inside ' + target.tagName.toLowerCase());
|
|
1373
1541
|
} else {
|
|
1374
|
-
|
|
1542
|
+
const cs = getComputedStyle(target);
|
|
1543
|
+
const prevBg = cs.backgroundImage;
|
|
1544
|
+
target.style.backgroundImage = `url('${pendingAsset.src}')`;
|
|
1545
|
+
target.style.backgroundSize = 'cover';
|
|
1546
|
+
rec(target, { backgroundImage: `url('${pendingAsset.src}')`, backgroundSize: 'cover' }, { backgroundImage: prevBg });
|
|
1547
|
+
toast('🖼️ Background set!');
|
|
1375
1548
|
}
|
|
1376
|
-
} catch (e) {
|
|
1377
|
-
toast('⚠ Upload failed');
|
|
1378
1549
|
}
|
|
1379
|
-
};
|
|
1380
|
-
reader.readAsDataURL(file);
|
|
1381
|
-
});
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
function addThumb(asset) {
|
|
1385
|
-
const img = document.createElement('img');
|
|
1386
|
-
img.className = 'ps-ast-thumb';
|
|
1387
|
-
img.src = asset.src;
|
|
1388
|
-
img.title = asset.name;
|
|
1389
|
-
img.dataset.assetId = asset.id;
|
|
1390
|
-
img.onclick = () => {
|
|
1391
|
-
// Deselect all thumbs
|
|
1392
|
-
document.querySelectorAll('.ps-ast-thumb').forEach(t => t.classList.remove('active'));
|
|
1393
|
-
img.classList.add('active');
|
|
1394
|
-
startPlacing(asset);
|
|
1395
|
-
};
|
|
1396
|
-
astList.appendChild(img);
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
// Start placing: attach ghost image to cursor
|
|
1400
|
-
function startPlacing(asset) {
|
|
1401
|
-
cancelPlacing();
|
|
1402
|
-
pendingAsset = asset;
|
|
1403
|
-
|
|
1404
|
-
// Create ghost
|
|
1405
|
-
placingEl = document.createElement('div');
|
|
1406
|
-
placingEl.className = 'ps-asset-placed placing';
|
|
1407
|
-
placingEl.style.cssText = 'width:120px;height:120px;pointer-events:none;opacity:.75;';
|
|
1408
|
-
placingEl.innerHTML = `<img src="${asset.src}" draggable="false">`;
|
|
1409
|
-
document.body.appendChild(placingEl);
|
|
1410
|
-
|
|
1411
|
-
toast('Click anywhere on page to place');
|
|
1412
|
-
document.addEventListener('mousemove', onPlacingMove);
|
|
1413
|
-
document.addEventListener('click', onPlacingClick, true);
|
|
1414
|
-
document.addEventListener('keydown', onPlacingCancel);
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
function onPlacingMove(e) {
|
|
1418
|
-
if (!placingEl) return;
|
|
1419
|
-
placingEl.style.left = (e.clientX - 60) + 'px';
|
|
1420
|
-
placingEl.style.top = (e.clientY - 60) + 'px';
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
function onPlacingClick(e) {
|
|
1424
|
-
if (ps(e.target)) return; // ignore clicks inside our panel
|
|
1425
|
-
e.preventDefault(); e.stopPropagation();
|
|
1426
|
-
if (!pendingAsset || !placingEl) return;
|
|
1427
|
-
|
|
1428
|
-
if (e.shiftKey || e.altKey) {
|
|
1429
|
-
// #6: Better insertion targeting
|
|
1430
|
-
const target = e.target;
|
|
1431
|
-
if (target.tagName.toLowerCase() === 'img') {
|
|
1432
|
-
const prevSrc = target.getAttribute('src');
|
|
1433
|
-
target.src = pendingAsset.src;
|
|
1434
|
-
rec(target, { src: pendingAsset.src }, { src: prevSrc || '' });
|
|
1435
|
-
toast('🖼️ Image replaced!');
|
|
1436
1550
|
} else {
|
|
1437
|
-
//
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
} else {
|
|
1442
|
-
const cs = getComputedStyle(target);
|
|
1443
|
-
const prevBg = cs.backgroundImage;
|
|
1444
|
-
target.style.backgroundImage = `url('${pendingAsset.src}')`;
|
|
1445
|
-
target.style.backgroundSize = 'cover';
|
|
1446
|
-
rec(target, { backgroundImage: `url('${pendingAsset.src}')`, backgroundSize: 'cover' }, { backgroundImage: prevBg });
|
|
1447
|
-
toast('🖼️ Background set!');
|
|
1448
|
-
}
|
|
1551
|
+
// PLACE mode (standard)
|
|
1552
|
+
const x = e.clientX - 60;
|
|
1553
|
+
const y = e.clientY - 60;
|
|
1554
|
+
placeAsset(pendingAsset, x, y, 120, 120);
|
|
1449
1555
|
}
|
|
1450
|
-
|
|
1451
|
-
// PLACE mode (standard)
|
|
1452
|
-
const x = e.clientX - 60;
|
|
1453
|
-
const y = e.clientY - 60;
|
|
1454
|
-
placeAsset(pendingAsset, x, y, 120, 120);
|
|
1556
|
+
cancelPlacing();
|
|
1455
1557
|
}
|
|
1456
|
-
cancelPlacing();
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
function onPlacingCancel(e) {
|
|
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);
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
// #6: Place asset permanently on page (optionally inside a parent)
|
|
1473
|
-
function placeAsset(asset, x, y, w, h, parent = document.body) {
|
|
1474
|
-
const wrap = document.createElement('div');
|
|
1475
|
-
wrap.className = 'ps-asset-placed';
|
|
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
1558
|
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1559
|
+
function onPlacingCancel(e) {
|
|
1560
|
+
if (e.key === 'Escape') cancelPlacing();
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
function cancelPlacing() {
|
|
1564
|
+
if (placingEl) { placingEl.remove(); placingEl = null; }
|
|
1565
|
+
pendingAsset = null;
|
|
1566
|
+
document.querySelectorAll('.ps-ast-thumb').forEach(t => t.classList.remove('active'));
|
|
1567
|
+
document.removeEventListener('mousemove', onPlacingMove);
|
|
1568
|
+
document.removeEventListener('click', onPlacingClick, true);
|
|
1569
|
+
document.removeEventListener('keydown', onPlacingCancel);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// #6: Place asset permanently on page (optionally inside a parent)
|
|
1573
|
+
function placeAsset(asset, x, y, w, h, parent = document.body) {
|
|
1574
|
+
const wrap = document.createElement('div');
|
|
1575
|
+
wrap.className = 'ps-asset-placed';
|
|
1576
|
+
const uid = 'ps-asset-' + Date.now();
|
|
1577
|
+
wrap.id = uid;
|
|
1578
|
+
|
|
1579
|
+
// If we have a specific parent, use relative positioning if needed
|
|
1580
|
+
const posType = parent === document.body ? 'fixed' : 'absolute';
|
|
1581
|
+
wrap.style.cssText = `position:${posType};left:${Math.round(x)}px;top:${Math.round(y)}px;width:${w}px;height:${h}px;z-index:1;`;
|
|
1582
|
+
wrap.innerHTML = `<img src="${asset.src}" draggable="false" alt="${asset.name}">`;
|
|
1583
|
+
parent.appendChild(wrap);
|
|
1584
|
+
|
|
1585
|
+
|
|
1586
|
+
// Click to select this placed asset (for z-index control)
|
|
1587
|
+
wrap.addEventListener('click', e => {
|
|
1588
|
+
if (ps(e.target)) return;
|
|
1589
|
+
selectPlaced(wrap);
|
|
1590
|
+
});
|
|
1591
|
+
|
|
1592
|
+
// Make placed asset draggable
|
|
1593
|
+
wrap.addEventListener('mousedown', e => {
|
|
1594
|
+
if (ps(e.target)) return;
|
|
1595
|
+
e.preventDefault();
|
|
1596
|
+
selectPlaced(wrap);
|
|
1597
|
+
astDragEl = wrap;
|
|
1598
|
+
astDragSX = e.clientX; astDragSY = e.clientY;
|
|
1599
|
+
astDragOL = parseFloat(wrap.style.left) || 0;
|
|
1600
|
+
astDragOT = parseFloat(wrap.style.top) || 0;
|
|
1601
|
+
wrap.style.cursor = 'grabbing';
|
|
1602
|
+
state.dragging = true;
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
rec(wrap, {
|
|
1606
|
+
src: asset.src,
|
|
1607
|
+
left: Math.round(x) + 'px',
|
|
1608
|
+
top: Math.round(y) + 'px',
|
|
1609
|
+
width: w + 'px',
|
|
1610
|
+
height: h + 'px',
|
|
1611
|
+
'z-index': '1',
|
|
1612
|
+
}, null, true); // true = isCreate
|
|
1613
|
+
|
|
1614
|
+
toast('✦ Placed — drag to reposition');
|
|
1496
1615
|
selectPlaced(wrap);
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
astDragOL = parseFloat(wrap.style.left) || 0;
|
|
1500
|
-
astDragOT = parseFloat(wrap.style.top) || 0;
|
|
1501
|
-
wrap.style.cursor = 'grabbing';
|
|
1502
|
-
state.dragging = true;
|
|
1503
|
-
});
|
|
1616
|
+
return wrap;
|
|
1617
|
+
}
|
|
1504
1618
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
let selectedPlaced = null;
|
|
1521
|
-
const zCtrl = document.getElementById('__ast_zctrl__');
|
|
1522
|
-
const zVal = document.getElementById('__z_val__');
|
|
1523
|
-
const zFront = document.getElementById('__z_front__');
|
|
1524
|
-
const zBack = document.getElementById('__z_back__');
|
|
1525
|
-
const zSet = document.getElementById('__z_set__');
|
|
1526
|
-
|
|
1527
|
-
function selectPlaced(wrap) {
|
|
1528
|
-
// Deselect previous
|
|
1529
|
-
if (selectedPlaced) selectedPlaced.style.outline = '';
|
|
1530
|
-
selectedPlaced = wrap;
|
|
1531
|
-
wrap.style.outline = '2px solid #7fff6e';
|
|
1532
|
-
zVal.value = parseInt(wrap.style.zIndex) || 1;
|
|
1533
|
-
zCtrl.style.display = 'block';
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
zFront.onclick = () => {
|
|
1537
|
-
if (!selectedPlaced) return;
|
|
1538
|
-
// Find max z-index of all placed assets
|
|
1539
|
-
const max = Math.max(0, ...[...document.querySelectorAll('.ps-asset-placed')].map(el => parseInt(el.style.zIndex) || 0));
|
|
1540
|
-
const nz = max + 1;
|
|
1541
|
-
selectedPlaced.style.zIndex = nz;
|
|
1542
|
-
zVal.value = nz;
|
|
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;
|
|
1619
|
+
// Track selected placed asset for z-index controls
|
|
1620
|
+
let selectedPlaced = null;
|
|
1621
|
+
const zCtrl = document.getElementById('__ast_zctrl__');
|
|
1622
|
+
const zVal = document.getElementById('__z_val__');
|
|
1623
|
+
const zFront = document.getElementById('__z_front__');
|
|
1624
|
+
const zBack = document.getElementById('__z_back__');
|
|
1625
|
+
const zSet = document.getElementById('__z_set__');
|
|
1626
|
+
|
|
1627
|
+
function selectPlaced(wrap) {
|
|
1628
|
+
// Deselect previous
|
|
1629
|
+
if (selectedPlaced) selectedPlaced.style.outline = '';
|
|
1630
|
+
selectedPlaced = wrap;
|
|
1631
|
+
wrap.style.outline = '2px solid #7fff6e';
|
|
1632
|
+
zVal.value = parseInt(wrap.style.zIndex) || 1;
|
|
1633
|
+
zCtrl.style.display = 'block';
|
|
1572
1634
|
}
|
|
1573
1635
|
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1636
|
+
zFront.onclick = () => {
|
|
1637
|
+
if (!selectedPlaced) return;
|
|
1638
|
+
// Find max z-index of all placed assets
|
|
1639
|
+
const max = Math.max(0, ...[...document.querySelectorAll('.ps-asset-placed')].map(el => parseInt(el.style.zIndex) || 0));
|
|
1640
|
+
const nz = max + 1;
|
|
1641
|
+
selectedPlaced.style.zIndex = nz;
|
|
1642
|
+
zVal.value = nz;
|
|
1643
|
+
rec(selectedPlaced, { 'z-index': String(nz) });
|
|
1644
|
+
toast('▲ Moved to front (z:' + nz + ')');
|
|
1645
|
+
};
|
|
1646
|
+
|
|
1647
|
+
zBack.onclick = () => {
|
|
1648
|
+
if (!selectedPlaced) return;
|
|
1649
|
+
const min = Math.min(0, ...[...document.querySelectorAll('.ps-asset-placed')].map(el => parseInt(el.style.zIndex) || 0));
|
|
1650
|
+
const nz = min - 1;
|
|
1651
|
+
selectedPlaced.style.zIndex = nz;
|
|
1652
|
+
zVal.value = nz;
|
|
1653
|
+
rec(selectedPlaced, { 'z-index': String(nz) });
|
|
1654
|
+
toast('▼ Moved to back (z:' + nz + ')');
|
|
1655
|
+
};
|
|
1656
|
+
|
|
1657
|
+
// #12: LAYERS PANEL LOGIC
|
|
1658
|
+
function updateLayers() {
|
|
1659
|
+
const list = document.getElementById('__lay_list__');
|
|
1660
|
+
list.innerHTML = '';
|
|
1661
|
+
|
|
1662
|
+
// Find interesting elements (headers, sections, divs with classes, images)
|
|
1663
|
+
const items = document.querySelectorAll('body > *:not(#__ps__), section *, header *, .container *');
|
|
1664
|
+
const filtered = [...items].filter(el => {
|
|
1665
|
+
if (ps(el)) return false;
|
|
1666
|
+
return el.tagName !== 'SCRIPT' && el.tagName !== 'STYLE' && (el.children.length === 0 || el.id || el.className);
|
|
1667
|
+
}).slice(0, 50); // limit for performance
|
|
1668
|
+
|
|
1669
|
+
if (!filtered.length) {
|
|
1670
|
+
list.innerHTML = '<div style="color:#444;font-size:10px;text-align:center;padding:10px">No elements found</div>';
|
|
1671
|
+
return;
|
|
1581
1672
|
}
|
|
1582
1673
|
|
|
1583
|
-
|
|
1584
|
-
|
|
1674
|
+
filtered.forEach(el => {
|
|
1675
|
+
const row = document.createElement('div');
|
|
1676
|
+
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';
|
|
1677
|
+
if (state.selectedEl === el) {
|
|
1678
|
+
row.style.borderColor = '#7fff6e';
|
|
1679
|
+
row.style.background = 'rgba(127,255,110,0.1)';
|
|
1680
|
+
row.style.color = '#7fff6e';
|
|
1681
|
+
}
|
|
1585
1682
|
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
zSet.onclick = () => {
|
|
1606
|
-
if (!selectedPlaced) return;
|
|
1607
|
-
const nz = parseInt(zVal.value) || 0;
|
|
1608
|
-
selectedPlaced.style.zIndex = nz;
|
|
1609
|
-
rec(selectedPlaced, { 'z-index': String(nz) });
|
|
1610
|
-
toast('z-index set to ' + nz);
|
|
1611
|
-
};
|
|
1612
|
-
|
|
1613
|
-
zVal.onkeydown = e => { if (e.key === 'Enter') zSet.click(); };
|
|
1614
|
-
|
|
1615
|
-
// Drag placed assets
|
|
1616
|
-
document.addEventListener('mousemove', e => {
|
|
1617
|
-
if (!astDragEl || !state.dragging) return;
|
|
1618
|
-
const dx = e.clientX - astDragSX, dy = e.clientY - astDragSY;
|
|
1619
|
-
astDragEl.style.left = (astDragOL + dx) + 'px';
|
|
1620
|
-
astDragEl.style.top = (astDragOT + dy) + 'px';
|
|
1621
|
-
tip.textContent = `x:${Math.round(astDragOL + dx)} y:${Math.round(astDragOT + dy)}`;
|
|
1622
|
-
tip.style.left = (e.clientX + 14) + 'px'; tip.style.top = (e.clientY - 28) + 'px';
|
|
1623
|
-
tip.classList.add('v');
|
|
1624
|
-
});
|
|
1625
|
-
|
|
1626
|
-
document.addEventListener('mouseup', () => {
|
|
1627
|
-
if (!astDragEl) return;
|
|
1628
|
-
tip.classList.remove('v');
|
|
1629
|
-
const cs = getComputedStyle(astDragEl);
|
|
1630
|
-
const newLeft = Math.round(parseFloat(cs.left)) + 'px';
|
|
1631
|
-
const newTop = Math.round(parseFloat(cs.top)) + 'px';
|
|
1632
|
-
if (newLeft !== (astDragOL + 'px') || newTop !== (astDragOT + 'px')) {
|
|
1633
|
-
rec(astDragEl, { left: newLeft, top: newTop });
|
|
1634
|
-
}
|
|
1635
|
-
astDragEl.style.cursor = 'grab';
|
|
1636
|
-
astDragEl = null;
|
|
1637
|
-
});
|
|
1638
|
-
|
|
1639
|
-
// ══════════════════════════════════════════
|
|
1640
|
-
// RECORD + SAVE
|
|
1641
|
-
// ══════════════════════════════════════════
|
|
1642
|
-
// Full history — every individual action
|
|
1643
|
-
const history = [];
|
|
1644
|
-
const redoHistory = [];
|
|
1645
|
-
|
|
1646
|
-
function rec(el, props, prevPropsOverride, isCreate = false) {
|
|
1647
|
-
const selector = el.dataset.pixelshiftId ? null : gsel(el);
|
|
1648
|
-
const key = el.dataset.pixelshiftId || selector;
|
|
1649
|
-
|
|
1650
|
-
// Use provided prevProps if given (snapshot taken before style was applied),
|
|
1651
|
-
// otherwise snapshot current inline values (for tools like Colors/Typography that call rec before apply)
|
|
1652
|
-
const prevProps = prevPropsOverride || {};
|
|
1653
|
-
if (!prevPropsOverride) {
|
|
1654
|
-
Object.keys(props).forEach(p => {
|
|
1655
|
-
const camel = p.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1656
|
-
prevProps[p] = el.style[camel] || '';
|
|
1683
|
+
const icon = el.tagName === 'IMG' ? '🖼️' : el.tagName.match(/H[1-6]/) ? 'Tt' : '📦';
|
|
1684
|
+
const name = el.tagName.toLowerCase() + (el.id ? '#' + el.id : el.className ? '.' + el.className.split(' ')[0] : '');
|
|
1685
|
+
|
|
1686
|
+
row.innerHTML = `<span>${icon}</span> <span style="flex:1;overflow:hidden;text-overflow:ellipsis">${name}</span>`;
|
|
1687
|
+
row.onclick = () => {
|
|
1688
|
+
// Select element (inline logic — no separate select() function)
|
|
1689
|
+
document.querySelectorAll('.__ps__, .__ps_multi__').forEach(el2 => {
|
|
1690
|
+
el2.classList.remove('__ps__', '__ps_multi__');
|
|
1691
|
+
});
|
|
1692
|
+
state.selectedEls = [el];
|
|
1693
|
+
state.selectedEl = el;
|
|
1694
|
+
el.classList.add('__ps__');
|
|
1695
|
+
if (state.tool === 'mov') placeHdl(el);
|
|
1696
|
+
if (state.tool === 'rsz') placeRH(el);
|
|
1697
|
+
if (state.tool === 'clr') populateColors(el);
|
|
1698
|
+
if (state.tool === 'typ') populateTypo(el);
|
|
1699
|
+
updateLayers();
|
|
1700
|
+
};
|
|
1701
|
+
list.appendChild(row);
|
|
1657
1702
|
});
|
|
1658
1703
|
}
|
|
1659
1704
|
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1705
|
+
zSet.onclick = () => {
|
|
1706
|
+
if (!selectedPlaced) return;
|
|
1707
|
+
const nz = parseInt(zVal.value) || 0;
|
|
1708
|
+
selectedPlaced.style.zIndex = nz;
|
|
1709
|
+
rec(selectedPlaced, { 'z-index': String(nz) });
|
|
1710
|
+
toast('z-index set to ' + nz);
|
|
1711
|
+
};
|
|
1712
|
+
|
|
1713
|
+
zVal.onkeydown = e => { if (e.key === 'Enter') zSet.click(); };
|
|
1714
|
+
|
|
1715
|
+
// Drag placed assets
|
|
1716
|
+
document.addEventListener('mousemove', e => {
|
|
1717
|
+
if (!astDragEl || !state.dragging) return;
|
|
1718
|
+
const dx = e.clientX - astDragSX, dy = e.clientY - astDragSY;
|
|
1719
|
+
astDragEl.style.left = (astDragOL + dx) + 'px';
|
|
1720
|
+
astDragEl.style.top = (astDragOT + dy) + 'px';
|
|
1721
|
+
tip.textContent = `x:${Math.round(astDragOL + dx)} y:${Math.round(astDragOT + dy)}`;
|
|
1722
|
+
tip.style.left = (e.clientX + 14) + 'px'; tip.style.top = (e.clientY - 28) + 'px';
|
|
1723
|
+
tip.classList.add('v');
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
document.addEventListener('mouseup', () => {
|
|
1727
|
+
if (!astDragEl) return;
|
|
1728
|
+
tip.classList.remove('v');
|
|
1729
|
+
const cs = getComputedStyle(astDragEl);
|
|
1730
|
+
const newLeft = Math.round(parseFloat(cs.left)) + 'px';
|
|
1731
|
+
const newTop = Math.round(parseFloat(cs.top)) + 'px';
|
|
1732
|
+
if (newLeft !== (Math.round(astDragOL) + 'px') || newTop !== (Math.round(astDragOT) + 'px')) {
|
|
1733
|
+
rec(astDragEl, { left: newLeft, top: newTop }, { left: Math.round(astDragOL) + 'px', top: Math.round(astDragOT) + 'px' });
|
|
1734
|
+
}
|
|
1735
|
+
astDragEl.style.cursor = 'grab';
|
|
1736
|
+
astDragEl = null;
|
|
1737
|
+
});
|
|
1738
|
+
|
|
1739
|
+
// ══════════════════════════════════════════
|
|
1740
|
+
// RECORD + SAVE
|
|
1741
|
+
// ══════════════════════════════════════════
|
|
1742
|
+
// Full history — every individual action
|
|
1743
|
+
const history = [];
|
|
1744
|
+
const redoHistory = [];
|
|
1745
|
+
|
|
1746
|
+
function rec(el, props, prevPropsOverride, isCreate = false) {
|
|
1747
|
+
const selector = el.dataset.pixelshiftId ? null : gsel(el);
|
|
1748
|
+
const key = el.dataset.pixelshiftId || selector;
|
|
1749
|
+
|
|
1750
|
+
// Use provided prevProps if given (snapshot taken before style was applied),
|
|
1751
|
+
// otherwise snapshot current inline values (for tools like Colors/Typography that call rec before apply)
|
|
1752
|
+
const prevProps = prevPropsOverride || {};
|
|
1753
|
+
if (!prevPropsOverride) {
|
|
1754
|
+
Object.keys(props).forEach(p => {
|
|
1755
|
+
const camel = p.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1756
|
+
prevProps[p] = el.style[camel] || '';
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// Extract exact file from React Fiber if available
|
|
1761
|
+
function getReactSource(element) {
|
|
1762
|
+
for (const key in element) {
|
|
1763
|
+
if (key.startsWith('__reactFiber$')) {
|
|
1764
|
+
let fiber = element[key];
|
|
1765
|
+
while (fiber) {
|
|
1766
|
+
if (fiber._debugSource && fiber._debugSource.fileName) return fiber._debugSource.fileName;
|
|
1767
|
+
fiber = fiber.return;
|
|
1768
|
+
}
|
|
1668
1769
|
}
|
|
1669
1770
|
}
|
|
1771
|
+
return null;
|
|
1670
1772
|
}
|
|
1671
|
-
return null;
|
|
1672
|
-
}
|
|
1673
1773
|
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1774
|
+
// Merge into state.changes (for save/apply)
|
|
1775
|
+
const parentSel = isCreate && el.parentElement && el.parentElement !== document.body ? gsel(el.parentElement) : null;
|
|
1776
|
+
const ch = {
|
|
1777
|
+
type: isCreate ? 'create' : (el.dataset.pixelshiftId ? 'inline' : 'css'),
|
|
1778
|
+
isCreate,
|
|
1779
|
+
pixelshiftId: el.dataset.pixelshiftId || null,
|
|
1780
|
+
selector,
|
|
1781
|
+
parentSelector: parentSel,
|
|
1782
|
+
exactFile: getReactSource(el) || window.location.pathname.replace(/^\//, '') || null, // fallback for vanilla HTML (#7)
|
|
1783
|
+
file: el.dataset.pixelshiftFile || null,
|
|
1784
|
+
props,
|
|
1785
|
+
tagName: el.tagName.toLowerCase(),
|
|
1786
|
+
outerHTML: el.outerHTML // Send the whole element for 'create' actions
|
|
1787
|
+
};
|
|
1788
|
+
const i = state.changes.findIndex(c => (c.pixelshiftId || c.selector) === key);
|
|
1789
|
+
if (i >= 0) Object.assign(state.changes[i].props, props); else state.changes.push(ch);
|
|
1688
1790
|
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1791
|
+
// Deep check: only record if at least one property actually changed
|
|
1792
|
+
let changed = isCreate;
|
|
1793
|
+
if (!isCreate) {
|
|
1794
|
+
for (const p in props) {
|
|
1795
|
+
if (String(props[p]) !== String(prevProps[p])) { changed = true; break; }
|
|
1796
|
+
}
|
|
1694
1797
|
}
|
|
1798
|
+
if (!changed) return;
|
|
1799
|
+
|
|
1800
|
+
// Push to history
|
|
1801
|
+
const hid = Date.now() + Math.random();
|
|
1802
|
+
history.push({
|
|
1803
|
+
hid, el, props, prevProps, selector: key, isCreate,
|
|
1804
|
+
parent: isCreate ? el.parentElement : null,
|
|
1805
|
+
nextSibling: isCreate ? el.nextElementSibling : null
|
|
1806
|
+
});
|
|
1807
|
+
|
|
1808
|
+
// Clear redo stack on new action
|
|
1809
|
+
redoHistory.length = 0;
|
|
1810
|
+
|
|
1811
|
+
updateUnsUI();
|
|
1695
1812
|
}
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
updateUnsUI();
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
function updateUnsUI() {
|
|
1713
|
-
const n = state.changes.length;
|
|
1714
|
-
sv.disabled = n === 0; nb.textContent = history.length;
|
|
1715
|
-
uns.style.display = history.length ? 'flex' : 'none';
|
|
1716
|
-
|
|
1717
|
-
// Rebuild history list (newest first)
|
|
1718
|
-
unsList.innerHTML = '';
|
|
1719
|
-
[...history].reverse().forEach(h => {
|
|
1720
|
-
const propStr = Object.entries(h.props).map(([k, v]) => `${k}: ${v}`).join(', ');
|
|
1721
|
-
const row = document.createElement('div');
|
|
1722
|
-
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;';
|
|
1723
|
-
row.innerHTML = `
|
|
1813
|
+
|
|
1814
|
+
function updateUnsUI() {
|
|
1815
|
+
const n = state.changes.length;
|
|
1816
|
+
sv.disabled = n === 0; nb.textContent = history.length;
|
|
1817
|
+
uns.style.display = history.length ? 'flex' : 'none';
|
|
1818
|
+
|
|
1819
|
+
// Rebuild history list (newest first)
|
|
1820
|
+
unsList.innerHTML = '';
|
|
1821
|
+
[...history].reverse().forEach(h => {
|
|
1822
|
+
const propStr = Object.entries(h.props).map(([k, v]) => `${k}: ${v}`).join(', ');
|
|
1823
|
+
const row = document.createElement('div');
|
|
1824
|
+
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;';
|
|
1825
|
+
row.innerHTML = `
|
|
1724
1826
|
<div style="flex:1;overflow:hidden">
|
|
1725
1827
|
<div style="color:#7fff6e;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${h.selector || 'placed element'}</div>
|
|
1726
1828
|
<div style="color:#555577;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${propStr}</div>
|
|
1727
1829
|
</div>
|
|
1728
1830
|
<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>
|
|
1729
1831
|
`;
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
});
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
function revertChange(h) {
|
|
1736
|
-
if (h.isCreate) {
|
|
1737
|
-
h.el.remove();
|
|
1738
|
-
} else {
|
|
1739
|
-
// Re-apply previous inline values
|
|
1740
|
-
Object.entries(h.prevProps).forEach(([prop, val]) => {
|
|
1741
|
-
const camel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1742
|
-
h.el.style[camel] = val;
|
|
1832
|
+
row.querySelector('button').onclick = () => revertChange(h);
|
|
1833
|
+
unsList.appendChild(row);
|
|
1743
1834
|
});
|
|
1744
|
-
// Also handle text/html
|
|
1745
|
-
if (h.prevProps.innerHTML !== undefined) h.el.innerHTML = h.prevProps.innerHTML;
|
|
1746
|
-
if (h.prevProps.innerText !== undefined) h.el.innerText = h.prevProps.innerText;
|
|
1747
|
-
}
|
|
1748
|
-
// Remove from history and add to redo stack
|
|
1749
|
-
const idx = history.findIndex(x => x.hid === h.hid);
|
|
1750
|
-
if (idx >= 0) {
|
|
1751
|
-
redoHistory.push(history[idx]);
|
|
1752
|
-
history.splice(idx, 1);
|
|
1753
1835
|
}
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
const i = state.changes.findIndex(c => (c.pixelshiftId || c.selector) === key);
|
|
1759
|
-
if (i >= 0) {
|
|
1760
|
-
Object.assign(state.changes[i].props, x.props);
|
|
1836
|
+
|
|
1837
|
+
function revertChange(h) {
|
|
1838
|
+
if (h.isCreate) {
|
|
1839
|
+
h.el.remove();
|
|
1761
1840
|
} else {
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
props: { ...x.props },
|
|
1767
|
-
outerHTML: x.isCreate && x.el ? x.el.outerHTML : undefined,
|
|
1768
|
-
tagName: x.el ? x.el.tagName?.toLowerCase() : undefined
|
|
1841
|
+
// Re-apply previous inline values
|
|
1842
|
+
Object.entries(h.prevProps).forEach(([prop, val]) => {
|
|
1843
|
+
const camel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1844
|
+
h.el.style[camel] = val;
|
|
1769
1845
|
});
|
|
1846
|
+
// Also handle text/html
|
|
1847
|
+
if (h.prevProps.innerHTML !== undefined) h.el.innerHTML = h.prevProps.innerHTML;
|
|
1848
|
+
if (h.prevProps.innerText !== undefined) h.el.innerText = h.prevProps.innerText;
|
|
1770
1849
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
if (isVisible) {
|
|
1777
|
-
if (state.tool === 'mov') placeHdl(state.selectedEl);
|
|
1778
|
-
if (state.tool === 'rsz') placeRH(state.selectedEl);
|
|
1779
|
-
} else if (state.selectedEl === h.el) {
|
|
1780
|
-
state.selectedEl.classList.remove('__ps__', '__ps_multi__');
|
|
1781
|
-
state.selectedEl = null;
|
|
1782
|
-
state.selectedEls = [];
|
|
1783
|
-
hdl.classList.remove('v');
|
|
1784
|
-
Object.values(rhs).forEach(rh => rh.classList.remove('v'));
|
|
1850
|
+
// Remove from history and add to redo stack
|
|
1851
|
+
const idx = history.findIndex(x => x.hid === h.hid);
|
|
1852
|
+
if (idx >= 0) {
|
|
1853
|
+
redoHistory.push(history[idx]);
|
|
1854
|
+
history.splice(idx, 1);
|
|
1785
1855
|
}
|
|
1856
|
+
// Rebuild state.changes — preserve all original fields (#1, #2)
|
|
1857
|
+
state.changes = [];
|
|
1858
|
+
history.forEach(x => {
|
|
1859
|
+
const key = x.selector;
|
|
1860
|
+
const i = state.changes.findIndex(c => (c.pixelshiftId || c.selector) === key);
|
|
1861
|
+
if (i >= 0) {
|
|
1862
|
+
Object.assign(state.changes[i].props, x.props);
|
|
1863
|
+
} else {
|
|
1864
|
+
state.changes.push({
|
|
1865
|
+
type: x.isCreate ? 'create' : 'css',
|
|
1866
|
+
isCreate: x.isCreate || false,
|
|
1867
|
+
selector: key,
|
|
1868
|
+
props: { ...x.props },
|
|
1869
|
+
outerHTML: x.isCreate && x.el ? x.el.outerHTML : undefined,
|
|
1870
|
+
tagName: x.el ? x.el.tagName?.toLowerCase() : undefined
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
});
|
|
1874
|
+
updateUnsUI();
|
|
1875
|
+
|
|
1876
|
+
if (state.selectedEl) {
|
|
1877
|
+
const isVisible = document.body.contains(state.selectedEl) && getComputedStyle(state.selectedEl).display !== 'none';
|
|
1878
|
+
if (isVisible) {
|
|
1879
|
+
if (state.tool === 'mov') placeHdl(state.selectedEl);
|
|
1880
|
+
if (state.tool === 'rsz') placeRH(state.selectedEl);
|
|
1881
|
+
} else if (state.selectedEl === h.el) {
|
|
1882
|
+
state.selectedEl.classList.remove('__ps__', '__ps_multi__');
|
|
1883
|
+
state.selectedEl = null;
|
|
1884
|
+
state.selectedEls = [];
|
|
1885
|
+
hdl.classList.remove('v');
|
|
1886
|
+
Object.values(rhs).forEach(rh => rh.classList.remove('v'));
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
toast('↩ Reverted');
|
|
1786
1891
|
}
|
|
1787
1892
|
|
|
1788
|
-
|
|
1789
|
-
|
|
1893
|
+
function redoChange() {
|
|
1894
|
+
if (redoHistory.length === 0) {
|
|
1895
|
+
toast('Nothing to redo');
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
const h = redoHistory.pop();
|
|
1790
1899
|
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
if (h.isCreate) {
|
|
1799
|
-
if (h.parent) {
|
|
1800
|
-
if (h.nextSibling) {
|
|
1801
|
-
h.parent.insertBefore(h.el, h.nextSibling);
|
|
1900
|
+
if (h.isCreate) {
|
|
1901
|
+
if (h.parent) {
|
|
1902
|
+
if (h.nextSibling) {
|
|
1903
|
+
h.parent.insertBefore(h.el, h.nextSibling);
|
|
1904
|
+
} else {
|
|
1905
|
+
h.parent.appendChild(h.el);
|
|
1906
|
+
}
|
|
1802
1907
|
} else {
|
|
1803
|
-
|
|
1908
|
+
document.body.appendChild(h.el);
|
|
1804
1909
|
}
|
|
1805
1910
|
} else {
|
|
1806
|
-
|
|
1911
|
+
// Re-apply properties
|
|
1912
|
+
Object.entries(h.props).forEach(([prop, val]) => {
|
|
1913
|
+
const camel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1914
|
+
h.el.style[camel] = val;
|
|
1915
|
+
});
|
|
1916
|
+
if (h.props.innerHTML !== undefined) h.el.innerHTML = h.props.innerHTML;
|
|
1917
|
+
if (h.props.innerText !== undefined) h.el.innerText = h.props.innerText;
|
|
1807
1918
|
}
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1919
|
+
|
|
1920
|
+
history.push(h);
|
|
1921
|
+
|
|
1922
|
+
// Rebuild state.changes
|
|
1923
|
+
state.changes = [];
|
|
1924
|
+
history.forEach(x => {
|
|
1925
|
+
const key = x.selector;
|
|
1926
|
+
const i = state.changes.findIndex(c => (c.pixelshiftId || c.selector) === key);
|
|
1927
|
+
if (i >= 0) {
|
|
1928
|
+
Object.assign(state.changes[i].props, x.props);
|
|
1929
|
+
} else {
|
|
1930
|
+
state.changes.push({
|
|
1931
|
+
type: x.isCreate ? 'create' : 'css',
|
|
1932
|
+
isCreate: x.isCreate || false,
|
|
1933
|
+
selector: key,
|
|
1934
|
+
props: { ...x.props },
|
|
1935
|
+
outerHTML: x.isCreate && x.el ? x.el.outerHTML : undefined,
|
|
1936
|
+
tagName: x.el ? x.el.tagName?.toLowerCase() : undefined
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1813
1939
|
});
|
|
1814
|
-
|
|
1815
|
-
|
|
1940
|
+
|
|
1941
|
+
updateUnsUI();
|
|
1942
|
+
|
|
1943
|
+
if (state.selectedEl) {
|
|
1944
|
+
const isVisible = document.body.contains(state.selectedEl) && getComputedStyle(state.selectedEl).display !== 'none';
|
|
1945
|
+
if (isVisible) {
|
|
1946
|
+
if (state.tool === 'mov') placeHdl(state.selectedEl);
|
|
1947
|
+
if (state.tool === 'rsz') placeRH(state.selectedEl);
|
|
1948
|
+
} else if (state.selectedEl === h.el) {
|
|
1949
|
+
state.selectedEl.classList.remove('__ps__', '__ps_multi__');
|
|
1950
|
+
state.selectedEl = null;
|
|
1951
|
+
state.selectedEls = [];
|
|
1952
|
+
hdl.classList.remove('v');
|
|
1953
|
+
Object.values(rhs).forEach(rh => rh.classList.remove('v'));
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
toast('↷ Redone');
|
|
1816
1958
|
}
|
|
1817
|
-
|
|
1818
|
-
history.push(h);
|
|
1819
1959
|
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1960
|
+
sv.addEventListener('click', async () => {
|
|
1961
|
+
// Check key config status
|
|
1962
|
+
let hasKey = false;
|
|
1963
|
+
let cfgProvider = 'groq';
|
|
1964
|
+
try {
|
|
1965
|
+
const cfgRes = await fetch('/draply-config');
|
|
1966
|
+
const cfg = await cfgRes.json();
|
|
1967
|
+
hasKey = cfg.hasKey;
|
|
1968
|
+
cfgProvider = cfg.provider || 'groq';
|
|
1969
|
+
} catch (e) { }
|
|
1970
|
+
|
|
1971
|
+
if (!hasKey) {
|
|
1972
|
+
// Ask for provider first (#8)
|
|
1973
|
+
const provider = prompt('Draply AI Save: Choose provider (groq / openai / anthropic / gemini / ollama):', 'groq');
|
|
1974
|
+
if (!provider) { toast('Save aborted'); return; }
|
|
1975
|
+
const key = provider === 'ollama' ? 'local' : prompt(`Enter your ${provider.toUpperCase()} API key:`);
|
|
1976
|
+
if (key) {
|
|
1977
|
+
sv.disabled = true; sv.textContent = 'Validating...';
|
|
1978
|
+
try {
|
|
1979
|
+
const vRes = await fetch('/draply-validate-key', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: key.trim(), provider: provider.trim() }) });
|
|
1980
|
+
const vData = await vRes.json();
|
|
1981
|
+
if (!vData.valid && provider !== 'ollama') {
|
|
1982
|
+
toast('⚠ Invalid API key'); sv.disabled = false; sv.textContent = 'Save'; return;
|
|
1983
|
+
}
|
|
1984
|
+
} catch { /* allow through if validation endpoint unavailable */ }
|
|
1985
|
+
|
|
1986
|
+
const configPayload = { apiKey: key.trim(), provider: provider.trim() };
|
|
1987
|
+
|
|
1988
|
+
await fetch('/draply-config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(configPayload) });
|
|
1989
|
+
sv.disabled = false; sv.textContent = 'Save';
|
|
1990
|
+
} else {
|
|
1991
|
+
toast('Save aborted: API Key required');
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
// Progress indicator with animation (#9)
|
|
1997
|
+
sv.disabled = true;
|
|
1998
|
+
sv.textContent = '';
|
|
1999
|
+
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>';
|
|
2000
|
+
// Add spin keyframe if not exists
|
|
2001
|
+
if (!document.getElementById('__ps_spin_style__')) {
|
|
2002
|
+
const spinStyle = document.createElement('style');
|
|
2003
|
+
spinStyle.id = '__ps_spin_style__';
|
|
2004
|
+
spinStyle.textContent = '@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }';
|
|
2005
|
+
document.head.appendChild(spinStyle);
|
|
2006
|
+
}
|
|
2007
|
+
try {
|
|
2008
|
+
const r = await fetch('/draply-ai-apply', {
|
|
2009
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2010
|
+
body: JSON.stringify({ changes: state.changes })
|
|
1835
2011
|
});
|
|
2012
|
+
const d = await r.json();
|
|
2013
|
+
if (d.ok) {
|
|
2014
|
+
const msg = d.fallback ? 'Saved to draply.css (No AI Key)' : `✅ AI Applied ${d.applied || ''}/${d.total || ''} to Source!`;
|
|
2015
|
+
toast(msg);
|
|
2016
|
+
} else toast('⚠ Error: ' + (d.error || 'unknown'));
|
|
2017
|
+
} catch {
|
|
2018
|
+
toast('⚠ Server unreachable');
|
|
1836
2019
|
}
|
|
2020
|
+
|
|
2021
|
+
state.changes = []; history.length = 0; redoHistory.length = 0;
|
|
2022
|
+
sv.innerHTML = 'Save'; sv.textContent = 'Save';
|
|
2023
|
+
updateUnsUI();
|
|
1837
2024
|
});
|
|
1838
2025
|
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
if (state.tool === 'rsz') placeRH(state.selectedEl);
|
|
1846
|
-
} else if (state.selectedEl === h.el) {
|
|
1847
|
-
state.selectedEl.classList.remove('__ps__', '__ps_multi__');
|
|
1848
|
-
state.selectedEl = null;
|
|
1849
|
-
state.selectedEls = [];
|
|
1850
|
-
hdl.classList.remove('v');
|
|
1851
|
-
Object.values(rhs).forEach(rh => rh.classList.remove('v'));
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
2026
|
+
document.getElementById('__uns_clear__').onclick = () => {
|
|
2027
|
+
state.changes = []; history.length = 0; redoHistory.length = 0; updateUnsUI();
|
|
2028
|
+
toast('🗑 History cleared');
|
|
2029
|
+
};
|
|
2030
|
+
|
|
2031
|
+
document.getElementById('__uns_save__').onclick = () => sv.click();
|
|
1854
2032
|
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
sv.addEventListener('click', async () => {
|
|
1859
|
-
// Check key config status
|
|
1860
|
-
let hasKey = false;
|
|
1861
|
-
let cfgProvider = 'groq';
|
|
1862
|
-
try {
|
|
1863
|
-
const cfgRes = await fetch('/draply-config');
|
|
1864
|
-
const cfg = await cfgRes.json();
|
|
1865
|
-
hasKey = cfg.hasKey;
|
|
1866
|
-
cfgProvider = cfg.provider || 'groq';
|
|
1867
|
-
} catch (e) { }
|
|
1868
|
-
|
|
1869
|
-
if (!hasKey) {
|
|
1870
|
-
// Ask for provider first (#8)
|
|
1871
|
-
const provider = prompt('Draply AI Save: Choose provider (groq / openai / anthropic / gemini / ollama):', 'groq');
|
|
1872
|
-
if (!provider) { toast('Save aborted'); return; }
|
|
2033
|
+
document.getElementById('__ps_ai_cfg__').onclick = async () => {
|
|
2034
|
+
const provider = prompt('Change AI Provider (groq / openai / anthropic / gemini / ollama):', 'groq');
|
|
2035
|
+
if (!provider) return;
|
|
1873
2036
|
const key = provider === 'ollama' ? 'local' : prompt(`Enter your ${provider.toUpperCase()} API key:`);
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
const vRes = await fetch('/draply-validate-key', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: key.trim(), provider: provider.trim() }) });
|
|
1878
|
-
const vData = await vRes.json();
|
|
1879
|
-
if (!vData.valid && provider !== 'ollama') {
|
|
1880
|
-
toast('⚠ Invalid API key'); sv.disabled = false; sv.textContent = 'Save'; return;
|
|
1881
|
-
}
|
|
1882
|
-
} catch { /* allow through if validation endpoint unavailable */ }
|
|
1883
|
-
await fetch('/draply-config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: key.trim(), provider: provider.trim() }) });
|
|
1884
|
-
sv.disabled = false; sv.textContent = 'Save';
|
|
1885
|
-
} else {
|
|
1886
|
-
toast('Save aborted: API Key required');
|
|
2037
|
+
|
|
2038
|
+
if (!key && provider !== 'ollama') {
|
|
2039
|
+
toast('⚠ API Key required');
|
|
1887
2040
|
return;
|
|
1888
2041
|
}
|
|
1889
|
-
}
|
|
1890
2042
|
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
spinStyle.textContent = '@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }';
|
|
1900
|
-
document.head.appendChild(spinStyle);
|
|
1901
|
-
}
|
|
1902
|
-
try {
|
|
1903
|
-
const r = await fetch('/draply-ai-apply', {
|
|
1904
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1905
|
-
body: JSON.stringify({ changes: state.changes })
|
|
1906
|
-
});
|
|
1907
|
-
const d = await r.json();
|
|
1908
|
-
if (d.ok) {
|
|
1909
|
-
const msg = d.fallback ? 'Saved to draply.css (No AI Key)' : `✅ AI Applied ${d.applied || ''}/${d.total || ''} to Source!`;
|
|
1910
|
-
toast(msg);
|
|
1911
|
-
} else toast('⚠ Error: ' + (d.error || 'unknown'));
|
|
1912
|
-
} catch {
|
|
1913
|
-
toast('⚠ Server unreachable');
|
|
1914
|
-
}
|
|
2043
|
+
toast('Verifying...');
|
|
2044
|
+
try {
|
|
2045
|
+
const vRes = await fetch('/draply-validate-key', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: key.trim(), provider: provider.trim() }) });
|
|
2046
|
+
const vData = await vRes.json();
|
|
2047
|
+
if (!vData.valid && provider !== 'ollama') {
|
|
2048
|
+
toast('⚠ Invalid API key'); return;
|
|
2049
|
+
}
|
|
2050
|
+
} catch { /* ignore validation fail */ }
|
|
1915
2051
|
|
|
1916
|
-
|
|
1917
|
-
sv.innerHTML = 'Save'; sv.textContent = 'Save';
|
|
1918
|
-
updateUnsUI();
|
|
1919
|
-
});
|
|
1920
|
-
|
|
1921
|
-
document.getElementById('__uns_clear__').onclick = () => {
|
|
1922
|
-
state.changes = []; history.length = 0; redoHistory.length = 0; updateUnsUI();
|
|
1923
|
-
toast('🗑 History cleared');
|
|
1924
|
-
};
|
|
1925
|
-
|
|
1926
|
-
document.getElementById('__uns_save__').onclick = () => sv.click();
|
|
1927
|
-
|
|
1928
|
-
document.getElementById('__ps_ai_cfg__').onclick = async () => {
|
|
1929
|
-
const provider = prompt('Change AI Provider (groq / openai / anthropic / gemini / ollama):', 'groq');
|
|
1930
|
-
if (!provider) return;
|
|
1931
|
-
const key = provider === 'ollama' ? 'local' : prompt(`Enter your ${provider.toUpperCase()} API key:`);
|
|
1932
|
-
|
|
1933
|
-
if (!key && provider !== 'ollama') {
|
|
1934
|
-
toast('⚠ API Key required');
|
|
1935
|
-
return;
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
toast('Verifying...');
|
|
1939
|
-
try {
|
|
1940
|
-
const vRes = await fetch('/draply-validate-key', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: key.trim(), provider: provider.trim() }) });
|
|
1941
|
-
const vData = await vRes.json();
|
|
1942
|
-
if (!vData.valid && provider !== 'ollama') {
|
|
1943
|
-
toast('⚠ Invalid API key'); return;
|
|
1944
|
-
}
|
|
1945
|
-
} catch { /* ignore validation fail */ }
|
|
1946
|
-
|
|
1947
|
-
await fetch('/draply-config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: key.trim(), provider: provider.trim() }) });
|
|
1948
|
-
toast('✅ AI Provider saved!');
|
|
1949
|
-
};
|
|
1950
|
-
|
|
1951
|
-
// ══════════════════════════════════════════
|
|
1952
|
-
// KEYBOARD SHORTCUTS (#4, #5, #15)
|
|
1953
|
-
// ══════════════════════════════════════════
|
|
1954
|
-
document.addEventListener('keydown', e => {
|
|
1955
|
-
// Ignore if typing in an input/textarea/contenteditable
|
|
1956
|
-
const tag = e.target.tagName.toLowerCase();
|
|
1957
|
-
if (tag === 'input' || tag === 'textarea' || e.target.isContentEditable) return;
|
|
1958
|
-
|
|
1959
|
-
// Ctrl+Z — Undo last change (#4)
|
|
1960
|
-
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && e.key.toLowerCase() === 'z') {
|
|
1961
|
-
e.preventDefault();
|
|
1962
|
-
if (history.length > 0) {
|
|
1963
|
-
revertChange(history[history.length - 1]);
|
|
1964
|
-
} else {
|
|
1965
|
-
toast('Nothing to undo');
|
|
1966
|
-
}
|
|
1967
|
-
return;
|
|
1968
|
-
}
|
|
2052
|
+
const configPayload = { apiKey: key.trim(), provider: provider.trim() };
|
|
1969
2053
|
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
2054
|
+
await fetch('/draply-config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(configPayload) });
|
|
2055
|
+
toast('✅ AI Provider saved!');
|
|
2056
|
+
};
|
|
2057
|
+
|
|
2058
|
+
// ══════════════════════════════════════════
|
|
2059
|
+
// KEYBOARD SHORTCUTS (#4, #5, #15)
|
|
2060
|
+
// ══════════════════════════════════════════
|
|
2061
|
+
document.addEventListener('keydown', e => {
|
|
2062
|
+
// Ignore if typing in an input/textarea/contenteditable
|
|
2063
|
+
const tag = e.target.tagName.toLowerCase();
|
|
2064
|
+
if (tag === 'input' || tag === 'textarea' || e.target.isContentEditable) return;
|
|
1976
2065
|
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
if (state.selectedEl && !ps(state.selectedEl)) {
|
|
2066
|
+
// Ctrl+Z — Undo last change (#4)
|
|
2067
|
+
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && e.key.toLowerCase() === 'z') {
|
|
1980
2068
|
e.preventDefault();
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
state.selectedEl = null;
|
|
1988
|
-
state.selectedEls = [];
|
|
1989
|
-
hdl.classList.remove('v');
|
|
1990
|
-
toast('🗑 Element hidden (Delete)');
|
|
2069
|
+
if (history.length > 0) {
|
|
2070
|
+
revertChange(history[history.length - 1]);
|
|
2071
|
+
} else {
|
|
2072
|
+
toast('Nothing to undo');
|
|
2073
|
+
}
|
|
2074
|
+
return;
|
|
1991
2075
|
}
|
|
1992
|
-
return;
|
|
1993
|
-
}
|
|
1994
2076
|
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
if (!e.ctrlKey && !e.metaKey) {
|
|
2077
|
+
// Ctrl+Shift+Z — Redo (implemented)
|
|
2078
|
+
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'z') {
|
|
1998
2079
|
e.preventDefault();
|
|
1999
|
-
|
|
2080
|
+
redoChange();
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// Delete / Backspace — remove selected element (#5)
|
|
2085
|
+
if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
2086
|
+
if (state.selectedEl && !ps(state.selectedEl)) {
|
|
2087
|
+
e.preventDefault();
|
|
2088
|
+
const el = state.selectedEl;
|
|
2089
|
+
// Record removal in history
|
|
2090
|
+
rec(el, { display: 'none' }, { display: el.style.display || '' });
|
|
2091
|
+
el.style.display = 'none';
|
|
2092
|
+
// Deselect
|
|
2093
|
+
el.classList.remove('__ps__', '__ps_multi__');
|
|
2094
|
+
state.selectedEl = null;
|
|
2095
|
+
state.selectedEls = [];
|
|
2096
|
+
hdl.classList.remove('v');
|
|
2097
|
+
toast('🗑 Element hidden (Delete)');
|
|
2098
|
+
}
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// Preview mode toggle: P key (#15)
|
|
2103
|
+
if (e.key === 'p' || e.key === 'P') {
|
|
2104
|
+
if (!e.ctrlKey && !e.metaKey) {
|
|
2105
|
+
e.preventDefault();
|
|
2106
|
+
togglePreview();
|
|
2107
|
+
}
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
});
|
|
2111
|
+
|
|
2112
|
+
// ══════════════════════════════════════════
|
|
2113
|
+
// PREVIEW MODE (#15)
|
|
2114
|
+
// ══════════════════════════════════════════
|
|
2115
|
+
let previewMode = false;
|
|
2116
|
+
function togglePreview() {
|
|
2117
|
+
previewMode = !previewMode;
|
|
2118
|
+
const psRoot = document.getElementById('__ps__');
|
|
2119
|
+
if (previewMode) {
|
|
2120
|
+
psRoot.style.display = 'none';
|
|
2121
|
+
// Remove all selection highlights
|
|
2122
|
+
document.querySelectorAll('.__ps__, .__ps_multi__, .__ph__').forEach(el => {
|
|
2123
|
+
el.classList.remove('__ps__', '__ps_multi__', '__ph__');
|
|
2124
|
+
});
|
|
2125
|
+
hdl.classList.remove('v');
|
|
2126
|
+
Object.values(rhs).forEach(h => h.classList.remove('v'));
|
|
2127
|
+
toast('👁 Preview mode — press P to exit');
|
|
2128
|
+
} else {
|
|
2129
|
+
psRoot.style.display = '';
|
|
2130
|
+
toast('Editor restored');
|
|
2000
2131
|
}
|
|
2001
|
-
return;
|
|
2002
2132
|
}
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
//
|
|
2006
|
-
//
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2133
|
+
|
|
2134
|
+
// ══════════════════════════════════════════
|
|
2135
|
+
// UTILS
|
|
2136
|
+
// ══════════════════════════════════════════
|
|
2137
|
+
function ps(el) { return el && el.closest('#__ps__'); }
|
|
2138
|
+
function gsel(el) {
|
|
2139
|
+
if (el.id) return '#' + el.id;
|
|
2140
|
+
// Build unique path up the DOM
|
|
2141
|
+
const parts = [];
|
|
2142
|
+
let cur = el;
|
|
2143
|
+
while (cur && cur !== document.body && cur !== document.documentElement) {
|
|
2144
|
+
if (cur.id) { parts.unshift('#' + cur.id); break; }
|
|
2145
|
+
const tag = cur.tagName.toLowerCase();
|
|
2146
|
+
const cls = [...cur.classList].filter(c => !c.startsWith('__') && !c.startsWith('ps-')).join('.');
|
|
2147
|
+
const siblings = cur.parentElement
|
|
2148
|
+
? [...cur.parentElement.children].filter(c => c.tagName === cur.tagName)
|
|
2149
|
+
: [];
|
|
2150
|
+
const idx = siblings.length > 1 ? `:nth-of-type(${siblings.indexOf(cur) + 1})` : '';
|
|
2151
|
+
parts.unshift(cls ? `${tag}.${cls}${idx}` : `${tag}${idx}`);
|
|
2152
|
+
cur = cur.parentElement;
|
|
2153
|
+
}
|
|
2154
|
+
return parts.join(' > ');
|
|
2024
2155
|
}
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
// UTILS
|
|
2029
|
-
// ══════════════════════════════════════════
|
|
2030
|
-
function ps(el) { return el && el.closest('#__ps__'); }
|
|
2031
|
-
function gsel(el) {
|
|
2032
|
-
if (el.id) return '#' + el.id;
|
|
2033
|
-
// Build unique path up the DOM
|
|
2034
|
-
const parts = [];
|
|
2035
|
-
let cur = el;
|
|
2036
|
-
while (cur && cur !== document.body && cur !== document.documentElement) {
|
|
2037
|
-
if (cur.id) { parts.unshift('#' + cur.id); break; }
|
|
2038
|
-
const tag = cur.tagName.toLowerCase();
|
|
2039
|
-
const cls = [...cur.classList].filter(c => !c.startsWith('__') && !c.startsWith('ps-')).join('.');
|
|
2040
|
-
const siblings = cur.parentElement
|
|
2041
|
-
? [...cur.parentElement.children].filter(c => c.tagName === cur.tagName)
|
|
2042
|
-
: [];
|
|
2043
|
-
const idx = siblings.length > 1 ? `:nth-of-type(${siblings.indexOf(cur) + 1})` : '';
|
|
2044
|
-
parts.unshift(cls ? `${tag}.${cls}${idx}` : `${tag}${idx}`);
|
|
2045
|
-
cur = cur.parentElement;
|
|
2156
|
+
function toast(msg) {
|
|
2157
|
+
tst.textContent = msg; tst.classList.add('v');
|
|
2158
|
+
clearTimeout(tst._t); tst._t = setTimeout(() => tst.classList.remove('v'), 2800);
|
|
2046
2159
|
}
|
|
2047
|
-
|
|
2048
|
-
}
|
|
2049
|
-
function toast(msg) {
|
|
2050
|
-
tst.textContent = msg; tst.classList.add('v');
|
|
2051
|
-
clearTimeout(tst._t); tst._t = setTimeout(() => tst.classList.remove('v'), 2800);
|
|
2052
|
-
}
|
|
2053
|
-
}) ();
|
|
2160
|
+
})();
|