@accelerated-agency/visual-editor 0.2.6 → 0.2.8

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/dist/index.js CHANGED
@@ -1842,6 +1842,31 @@ function ElementIcon({ tag }) {
1842
1842
  }
1843
1843
  return /* @__PURE__ */ jsx("span", { className: "shrink-0 w-5 h-5 rounded flex items-center justify-center", style: { backgroundColor: bgColor }, children: /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ jsx("rect", { x: "1.5", y: "1.5", width: "9", height: "9", rx: "1.5", stroke: iconColor, strokeWidth: "1.2" }) }) });
1844
1844
  }
1845
+
1846
+ // src/lib/normalizeProxyBaseUrl.ts
1847
+ function normalizeProxyBaseUrl(proxyBaseUrl) {
1848
+ const raw = (proxyBaseUrl || "").trim().replace(/\/+$/, "");
1849
+ if (!raw) return "";
1850
+ const isAbsoluteHttpUrl = /^https?:\/\//i.test(raw);
1851
+ if (!isAbsoluteHttpUrl || typeof window === "undefined") {
1852
+ return raw;
1853
+ }
1854
+ try {
1855
+ const parsed = new URL(raw);
1856
+ const pageIsHttps = window.location.protocol === "https:";
1857
+ const targetIsHttp = parsed.protocol === "http:";
1858
+ if (pageIsHttps && targetIsHttp) {
1859
+ if (parsed.hostname === window.location.hostname) {
1860
+ const basePath = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/+$/, "");
1861
+ return `${window.location.origin}${basePath}`;
1862
+ }
1863
+ parsed.protocol = "https:";
1864
+ }
1865
+ return parsed.toString().replace(/\/+$/, "");
1866
+ } catch {
1867
+ return raw;
1868
+ }
1869
+ }
1845
1870
  var CHANNEL = "conversion-editor";
1846
1871
  function IframeCanvas({ url, password, proxyBaseUrl = "", onBridgeReady, onPong }) {
1847
1872
  const iframeElRef = useRef(null);
@@ -1885,11 +1910,12 @@ function IframeCanvas({ url, password, proxyBaseUrl = "", onBridgeReady, onPong
1885
1910
  window.addEventListener("message", handleMessage);
1886
1911
  return () => window.removeEventListener("message", handleMessage);
1887
1912
  }, [handleMessage]);
1913
+ const resolvedProxyBaseUrl = normalizeProxyBaseUrl(proxyBaseUrl);
1888
1914
  let resolvedUrl;
1889
1915
  if (url.toLowerCase() === "test") {
1890
1916
  resolvedUrl = "/test";
1891
1917
  } else if (url.startsWith("http")) {
1892
- resolvedUrl = `${proxyBaseUrl}/api/conversion-proxy?password=${encodeURIComponent(password || "")}&url=${encodeURIComponent(url)}`;
1918
+ resolvedUrl = `${resolvedProxyBaseUrl}/api/conversion-proxy?password=${encodeURIComponent(password || "")}&url=${encodeURIComponent(url)}`;
1893
1919
  } else {
1894
1920
  resolvedUrl = url;
1895
1921
  }
@@ -4777,6 +4803,7 @@ var VVVEB_CHANNEL = "vvveb-bridge";
4777
4803
  function PlatformVisualEditorV2({
4778
4804
  // channel kept for API compatibility; VvvebJs uses its own internal channel
4779
4805
  embeddedGlobalKey = "__CONVERSION_EMBEDDED__",
4806
+ proxyBaseUrl = "",
4780
4807
  className = "fixed inset-0 z-[9999] flex flex-col bg-white",
4781
4808
  editorClassName = "flex-1 min-h-0",
4782
4809
  showHeader = true,
@@ -4839,6 +4866,10 @@ function PlatformVisualEditorV2({
4839
4866
  }),
4840
4867
  [experiment]
4841
4868
  );
4869
+ const editorSrc = useMemo(() => {
4870
+ const safeBaseUrl = normalizeProxyBaseUrl(proxyBaseUrl);
4871
+ return safeBaseUrl ? `${safeBaseUrl}/vvveb-editor` : "/vvveb-editor";
4872
+ }, [proxyBaseUrl]);
4842
4873
  useEffect(() => {
4843
4874
  if (!editorReady) return;
4844
4875
  const key = JSON.stringify(loadPayload);
@@ -4981,7 +5012,7 @@ function PlatformVisualEditorV2({
4981
5012
  "iframe",
4982
5013
  {
4983
5014
  ref: iframeRef,
4984
- src: "/vvveb-editor",
5015
+ src: editorSrc,
4985
5016
  className: "w-full h-full border-0",
4986
5017
  title: "Vvveb Visual Editor",
4987
5018
  allow: "same-origin"
package/dist/vite.cjs CHANGED
@@ -995,6 +995,10 @@ var vvvebReady = false;
995
995
  var currentMode = 'editor';
996
996
  var currentDevice = 'desktop';
997
997
  var selectedEl = null;
998
+ /** Stable selector fingerprint for resilient selection recovery after DOM churn. */
999
+ var selectedElFingerprint = '';
1000
+ var selectedElRecoverMisses = 0;
1001
+ var MAX_SELECTED_RECOVER_MISSES = 12;
998
1002
  var suppressClickUntil = 0;
999
1003
  var dragAttachDoc = null;
1000
1004
  var currentMainTab = 'design';
@@ -1037,6 +1041,37 @@ function endSuppressIframeMutationDirty() {
1037
1041
  suppressIframeMutationDirty = Math.max(0, suppressIframeMutationDirty - 1);
1038
1042
  }
1039
1043
 
1044
+ function recoverSelectedElement(forceDeselectOnMiss) {
1045
+ if (selectedEl && selectedEl.ownerDocument && selectedEl.ownerDocument.contains(selectedEl)) {
1046
+ selectedElRecoverMisses = 0;
1047
+ return selectedEl;
1048
+ }
1049
+ if (!selectedElFingerprint) {
1050
+ if (forceDeselectOnMiss) deselectElement();
1051
+ return null;
1052
+ }
1053
+ var iframe = document.getElementById('iframeId');
1054
+ var doc = iframe && iframe.contentDocument;
1055
+ if (!doc) return null;
1056
+ var recovered = querySelectorResolved(doc, selectedElFingerprint);
1057
+ if (recovered) {
1058
+ beginSuppressIframeMutationDirty();
1059
+ try {
1060
+ selectedEl = recovered;
1061
+ if (selectedEl.classList) selectedEl.classList.add('vve-selected');
1062
+ } finally {
1063
+ endSuppressIframeMutationDirty();
1064
+ }
1065
+ selectedElRecoverMisses = 0;
1066
+ return recovered;
1067
+ }
1068
+ selectedElRecoverMisses += 1;
1069
+ if (forceDeselectOnMiss && selectedElRecoverMisses >= MAX_SELECTED_RECOVER_MISSES) {
1070
+ deselectElement();
1071
+ }
1072
+ return null;
1073
+ }
1074
+
1040
1075
  /** Stable stringify of a variation's changesets field (string or array from API). */
1041
1076
  function fingerprintChangesetsField(raw) {
1042
1077
  if (raw == null) return '[]';
@@ -2640,6 +2675,8 @@ function selectElement(el) {
2640
2675
  try {
2641
2676
  if (selectedEl) { try { selectedEl.classList.remove('vve-selected'); } catch(_) {} }
2642
2677
  selectedEl = el;
2678
+ selectedElFingerprint = buildSelector(el);
2679
+ selectedElRecoverMisses = 0;
2643
2680
  try { el.classList.add('vve-selected'); } catch(_) {}
2644
2681
  } finally {
2645
2682
  endSuppressIframeMutationDirty();
@@ -2661,6 +2698,8 @@ function deselectElement() {
2661
2698
  beginSuppressIframeMutationDirty();
2662
2699
  try {
2663
2700
  if (selectedEl) { try { selectedEl.classList.remove('vve-selected'); } catch(_) {} selectedEl = null; }
2701
+ selectedElFingerprint = '';
2702
+ selectedElRecoverMisses = 0;
2664
2703
  } finally {
2665
2704
  endSuppressIframeMutationDirty();
2666
2705
  }
@@ -2714,7 +2753,13 @@ function positionSelectionToolbar() {
2714
2753
  var bar = document.getElementById('selection-floater');
2715
2754
  var iframe = document.getElementById('iframeId');
2716
2755
  var panel = document.getElementById('iframe-panel');
2717
- if (!bar || !selectedEl || !iframe || !iframe.contentWindow || !panel) return;
2756
+ var liveSelected = recoverSelectedElement(false);
2757
+ if (!bar || !liveSelected || !iframe || !iframe.contentWindow || !panel) return;
2758
+ if (selectedEl !== liveSelected) {
2759
+ selectedEl = liveSelected;
2760
+ renderRightPanel(liveSelected);
2761
+ syncDomTreeSelection();
2762
+ }
2718
2763
  var elR = selectedEl.getBoundingClientRect();
2719
2764
  var iframeR = iframe.getBoundingClientRect();
2720
2765
  var panelR = panel.getBoundingClientRect();
@@ -2730,10 +2775,17 @@ function positionSelectionToolbar() {
2730
2775
  function updateSelectionToolbar() {
2731
2776
  var bar = document.getElementById('selection-floater');
2732
2777
  if (!bar) return;
2733
- if (!selectedEl || currentMode !== 'editor') {
2778
+ if (currentMode !== 'editor') {
2779
+ bar.style.display = 'none';
2780
+ return;
2781
+ }
2782
+ var liveSelected = recoverSelectedElement(true);
2783
+ if (!liveSelected) {
2734
2784
  bar.style.display = 'none';
2735
2785
  return;
2736
2786
  }
2787
+ if (selectedEl !== liveSelected) selectedEl = liveSelected;
2788
+ selectedElFingerprint = buildSelector(liveSelected);
2737
2789
  bar.style.display = 'flex';
2738
2790
  requestAnimationFrame(function() { positionSelectionToolbar(); });
2739
2791
  }
@@ -3701,9 +3753,13 @@ function attachChangeObserver() {
3701
3753
  // Page JS replaced body children; allow structural rows (insert/reorder) to apply again.
3702
3754
  appliedStructuralChangesetKeys = {};
3703
3755
  }
3756
+ // Host scripts can replace selected nodes every few frames (e.g. A/B tool observers).
3757
+ // Keep selection sticky by re-resolving from fingerprint.
3758
+ recoverSelectedElement(false);
3704
3759
  scheduleDomTreeRefresh();
3705
3760
  scheduleGranularChangesetReapply();
3706
3761
  scheduleConsistencyReconcile();
3762
+ updateSelectionToolbar();
3707
3763
  });
3708
3764
  changeObserver.observe(doc.body, {
3709
3765
  childList: true, subtree: true, attributes: true, characterData: true
@@ -3948,9 +4004,23 @@ function handleClose() {
3948
4004
  }
3949
4005
 
3950
4006
  // \u2500\u2500 Keyboard shortcuts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4007
+ function isNativeEditableTarget(target) {
4008
+ if (!target || target.nodeType !== 1) return false;
4009
+ if (target.isContentEditable) return true;
4010
+ if (target.closest && target.closest('[contenteditable=""],[contenteditable="true"],[contenteditable="plaintext-only"]')) {
4011
+ return true;
4012
+ }
4013
+ if (!target.tagName) return false;
4014
+ var tag = String(target.tagName).toLowerCase();
4015
+ return tag === 'input' || tag === 'textarea' || tag === 'select';
4016
+ }
4017
+
3951
4018
  document.addEventListener('keydown', function(e) {
4019
+ // Keep native browser undo/redo inside text inputs/contenteditable fields.
4020
+ if (isNativeEditableTarget(e.target)) return;
3952
4021
  var meta = e.metaKey || e.ctrlKey;
3953
- if (meta && !e.shiftKey && e.key === 'z') {
4022
+ var k = (e.key || '').toLowerCase();
4023
+ if (meta && !e.shiftKey && k === 'z') {
3954
4024
  e.preventDefault();
3955
4025
  if (typeof Vvveb !== 'undefined' && Vvveb.Undo) {
3956
4026
  Vvveb.Undo.undo();
@@ -3958,7 +4028,7 @@ document.addEventListener('keydown', function(e) {
3958
4028
  recomputeEditorDirty();
3959
4029
  }
3960
4030
  }
3961
- if (meta && e.shiftKey && e.key === 'z') {
4031
+ if (meta && e.shiftKey && k === 'z') {
3962
4032
  e.preventDefault();
3963
4033
  if (typeof Vvveb !== 'undefined' && Vvveb.Undo) {
3964
4034
  Vvveb.Undo.redo();
@@ -4427,7 +4497,96 @@ function createVisualEditorMiddleware(options) {
4427
4497
  html = html.replace("</head>", `${popupHideCss}
4428
4498
  </head>`);
4429
4499
  }
4430
- const runtimeProxyScript = `<script>(function(){try{var TARGET_ORIGIN=${JSON.stringify(origin)};var TARGET_PAGE_URL=${JSON.stringify(targetUrl)};var EMPTY_JSON_DATA="data:application/json;charset=utf-8,%7B%7D";function isSkippable(raw){if(!raw||typeof raw!=="string")return true;return raw.startsWith("data:")||raw.startsWith("blob:")||raw.startsWith("javascript:")||raw.startsWith("#")||raw.startsWith("http")||raw.startsWith("//");}function toAbsoluteOriginUrl(raw){if(isSkippable(raw))return raw;try{var base=raw.startsWith("/")?TARGET_ORIGIN:TARGET_PAGE_URL;var abs=new URL(raw,base);if(abs.origin!==TARGET_ORIGIN)return raw;return abs.toString();}catch(_){return raw;}}function resolveUrl(s){try{return new URL(s,window.location.href);}catch(_){return null;}}function isNestedMalformedProxy(u){if(!u)return false;var p=u.pathname||"";if(p==="/api/conversion-proxy"||p.indexOf("/api/conversion-proxy/")===0)return false;return p.indexOf("api/conversion-proxy")!==-1;}function skipNestedProxyNetwork(s){var u=typeof s==="string"?resolveUrl(s):null;return u&&isNestedMalformedProxy(u);}function emptyJsonFetchResponse(){return Promise.resolve(new Response("{}",{status:200,headers:{"Content-Type":"application/json; charset=utf-8"}}));}if(window.fetch){var _fetch=window.fetch.bind(window);window.fetch=function(input,init){try{var rawUrl=typeof input==="string"?input:(input&&input.url?String(input.url):"");if(rawUrl&&skipNestedProxyNetwork(rawUrl))return emptyJsonFetchResponse();if(typeof input==="string"){input=toAbsoluteOriginUrl(input);}else if(input&&input.url){var next=toAbsoluteOriginUrl(input.url);if(next!==input.url){input=new Request(next,input);}}var after=typeof input==="string"?input:(input&&input.url?String(input.url):"");if(after&&skipNestedProxyNetwork(after))return emptyJsonFetchResponse();}catch(_){}return _fetch(input,init);};}if(window.XMLHttpRequest&&window.XMLHttpRequest.prototype&&window.XMLHttpRequest.prototype.open){var _open=window.XMLHttpRequest.prototype.open;window.XMLHttpRequest.prototype.open=function(method,url){try{var u=resolveUrl(String(url));if(u&&isNestedMalformedProxy(u)){arguments[1]=EMPTY_JSON_DATA;}else{arguments[1]=toAbsoluteOriginUrl(url);}}catch(_){}return _open.apply(this,arguments);};}if(window.navigator&&typeof window.navigator.sendBeacon==="function"){var _beacon=window.navigator.sendBeacon.bind(window.navigator);window.navigator.sendBeacon=function(url,data){try{if(skipNestedProxyNetwork(String(url)))return true;}catch(_){}return _beacon(url,data);};}if(window.navigator&&window.navigator.serviceWorker&&typeof window.navigator.serviceWorker.register==="function"){window.navigator.serviceWorker.register=function(){return Promise.resolve({scope:"disabled-in-editor-proxy"});};}}catch(_){}})();</script>`;
4500
+ html = html.replace(
4501
+ /<meta[^>]+http-equiv=["']?\s*(x-frame-options|content-security-policy)\s*["']?[^>]*>/gi,
4502
+ ""
4503
+ );
4504
+ html = html.replace(
4505
+ /<meta[^>]+name=["']?\s*content-security-policy\s*["']?[^>]*>/gi,
4506
+ ""
4507
+ );
4508
+ const runtimePreflightScript = `<script>(function(){try{
4509
+ var TARGET_ORIGIN=${JSON.stringify(origin)};
4510
+ var TARGET_PAGE_URL=${JSON.stringify(targetUrl)};
4511
+ var PROXY_PASSWORD=${JSON.stringify(password)};
4512
+ window.__CONVERSION_EDITOR_ACTIVE__=true;
4513
+ function isSkippable(raw){if(!raw||typeof raw!=="string")return true;return raw.startsWith("data:")||raw.startsWith("blob:")||raw.startsWith("javascript:")||raw.startsWith("#");}
4514
+ function toAbsolute(raw){if(isSkippable(raw))return raw;try{var base=raw.startsWith("/")||raw.startsWith("//")?TARGET_ORIGIN:TARGET_PAGE_URL;return new URL(raw,base).toString();}catch(_){return raw;}}
4515
+ function toProxy(raw){if(isSkippable(raw))return null;var abs=toAbsolute(raw);if(!abs||typeof abs!=="string")return null;try{var parsed=new URL(abs);if(parsed.origin!==TARGET_ORIGIN)return null;return "/api/conversion-proxy?password="+encodeURIComponent(PROXY_PASSWORD||"")+"&url="+encodeURIComponent(parsed.toString());}catch(_){return null;}}
4516
+ var nativeAssign=window.location.assign?window.location.assign.bind(window.location):null;
4517
+ var nativeReplace=window.location.replace?window.location.replace.bind(window.location):null;
4518
+ function safeNavigate(raw,mode){var abs=toAbsolute(raw);var prox=toProxy(raw);if(!prox){try{console.warn("[conversion-proxy] redirect blocked",{mode:mode,requested:raw,resolved:abs,origin:TARGET_ORIGIN});}catch(_){}return false;}try{console.info("[conversion-proxy] redirect intercepted",{mode:mode,requested:raw,resolved:abs,proxied:prox});if(mode==="replace"&&nativeReplace){nativeReplace(prox);return true;}if(nativeAssign){nativeAssign(prox);return true;}window.location.href=prox;return true;}catch(err){try{console.warn("[conversion-proxy] redirect interception failed",{mode:mode,requested:raw,resolved:abs,proxied:prox,error:err&&err.message?err.message:String(err)});}catch(_){}return false;}}
4519
+ try{if(nativeAssign){window.location.assign=function(url){return safeNavigate(url,"assign");};}}catch(_){}
4520
+ try{if(nativeReplace){window.location.replace=function(url){return safeNavigate(url,"replace");};}}catch(_){}
4521
+ try{var hrefDesc=Object.getOwnPropertyDescriptor(Location.prototype,"href");if(hrefDesc&&hrefDesc.configurable&&hrefDesc.get&&hrefDesc.set){Object.defineProperty(Location.prototype,"href",{configurable:true,enumerable:hrefDesc.enumerable,get:function(){return hrefDesc.get.call(this);},set:function(v){safeNavigate(v,"assign");}});}}catch(_){}
4522
+ try{
4523
+ var NativeMO=window.MutationObserver;
4524
+ if(NativeMO&&!window.__CONVERSION_MO_GUARDED__){
4525
+ window.__CONVERSION_MO_GUARDED__=true;
4526
+ window.MutationObserver=function(cb){
4527
+ var last=0;
4528
+ var wrapped=function(list,obs){
4529
+ try{
4530
+ if(!window.__CONVERSION_EDITOR_ACTIVE__)return cb(list,obs);
4531
+ var now=Date.now();
4532
+ if(now-last<120)return;
4533
+ last=now;
4534
+ return cb(list,obs);
4535
+ }catch(_){}
4536
+ };
4537
+ return new NativeMO(wrapped);
4538
+ };
4539
+ window.MutationObserver.prototype=NativeMO.prototype;
4540
+ }
4541
+ }catch(_){}
4542
+ }catch(_){}})();</script>`;
4543
+ html = html.replace(/<head([^>]*)>/i, `<head$1>
4544
+ ${runtimePreflightScript}
4545
+ `);
4546
+ const runtimeProxyScript = `<script>(function(){try{
4547
+ var TARGET_ORIGIN=${JSON.stringify(origin)};
4548
+ var TARGET_PAGE_URL=${JSON.stringify(targetUrl)};
4549
+ var EMPTY_JSON_DATA="data:application/json;charset=utf-8,%7B%7D";
4550
+ function isSkippable(raw){if(!raw||typeof raw!=="string")return true;return raw.startsWith("data:")||raw.startsWith("blob:")||raw.startsWith("javascript:")||raw.startsWith("#")||raw.startsWith("http")||raw.startsWith("//");}
4551
+ function toAbsoluteOriginUrl(raw){if(isSkippable(raw))return raw;try{var base=raw.startsWith("/")?TARGET_ORIGIN:TARGET_PAGE_URL;var abs=new URL(raw,base);if(abs.origin!==TARGET_ORIGIN)return raw;return abs.toString();}catch(_){return raw;}}
4552
+ function resolveUrl(s){try{return new URL(s,window.location.href);}catch(_){return null;}}
4553
+ function isNestedMalformedProxy(u){if(!u)return false;var p=u.pathname||"";if(p==="/api/conversion-proxy"||p.indexOf("/api/conversion-proxy/")===0)return false;return p.indexOf("api/conversion-proxy")!==-1;}
4554
+ function skipNestedProxyNetwork(s){var u=typeof s==="string"?resolveUrl(s):null;return u&&isNestedMalformedProxy(u);}
4555
+ function emptyJsonFetchResponse(){return Promise.resolve(new Response("{}",{status:200,headers:{"Content-Type":"application/json; charset=utf-8"}}));}
4556
+ if(window.fetch){
4557
+ var _fetch=window.fetch.bind(window);
4558
+ window.fetch=function(input,init){
4559
+ var afterUrl="";
4560
+ try{
4561
+ var rawUrl=typeof input==="string"?input:(input&&input.url?String(input.url):"");
4562
+ if(rawUrl&&skipNestedProxyNetwork(rawUrl))return emptyJsonFetchResponse();
4563
+ if(typeof input==="string"){
4564
+ input=toAbsoluteOriginUrl(input);
4565
+ }else if(input&&input.url){
4566
+ var next=toAbsoluteOriginUrl(input.url);
4567
+ if(next!==input.url){input=new Request(next,input);}
4568
+ }
4569
+ afterUrl=typeof input==="string"?input:(input&&input.url?String(input.url):"");
4570
+ if(afterUrl&&skipNestedProxyNetwork(afterUrl))return emptyJsonFetchResponse();
4571
+ }catch(_){}
4572
+ return _fetch(input,init).catch(function(err){
4573
+ try{
4574
+ var u=afterUrl?resolveUrl(afterUrl):null;
4575
+ var sameOrigin=!!(u&&u.origin===TARGET_ORIGIN);
4576
+ var likelyThirdPartyBg=!sameOrigin||!!(u&&/(^|\\/)apps?(\\/|$)|(^|\\/)a(\\/|$)/.test(u.pathname||""));
4577
+ if(window.__CONVERSION_EDITOR_ACTIVE__&&likelyThirdPartyBg){
4578
+ console.warn("[conversion-proxy] suppressed fetch failure",{url:afterUrl,error:err&&err.message?err.message:String(err)});
4579
+ return new Response("{}",{status:200,headers:{"Content-Type":"application/json; charset=utf-8"}});
4580
+ }
4581
+ }catch(_){}
4582
+ throw err;
4583
+ });
4584
+ };
4585
+ }
4586
+ if(window.XMLHttpRequest&&window.XMLHttpRequest.prototype&&window.XMLHttpRequest.prototype.open){var _open=window.XMLHttpRequest.prototype.open;window.XMLHttpRequest.prototype.open=function(method,url){try{var u=resolveUrl(String(url));if(u&&isNestedMalformedProxy(u)){arguments[1]=EMPTY_JSON_DATA;}else{arguments[1]=toAbsoluteOriginUrl(url);}}catch(_){}return _open.apply(this,arguments);};}
4587
+ if(window.navigator&&typeof window.navigator.sendBeacon==="function"){var _beacon=window.navigator.sendBeacon.bind(window.navigator);window.navigator.sendBeacon=function(url,data){try{if(skipNestedProxyNetwork(String(url)))return true;}catch(_){}return _beacon(url,data);};}
4588
+ if(window.navigator&&window.navigator.serviceWorker&&typeof window.navigator.serviceWorker.register==="function"){window.navigator.serviceWorker.register=function(){return Promise.resolve({scope:"disabled-in-editor-proxy"});};}
4589
+ }catch(_){}})();</script>`;
4431
4590
  if (html.includes("</head>")) {
4432
4591
  html = html.replace("</head>", `${runtimeProxyScript}
4433
4592
  </head>`);
package/dist/vite.js CHANGED
@@ -987,6 +987,10 @@ var vvvebReady = false;
987
987
  var currentMode = 'editor';
988
988
  var currentDevice = 'desktop';
989
989
  var selectedEl = null;
990
+ /** Stable selector fingerprint for resilient selection recovery after DOM churn. */
991
+ var selectedElFingerprint = '';
992
+ var selectedElRecoverMisses = 0;
993
+ var MAX_SELECTED_RECOVER_MISSES = 12;
990
994
  var suppressClickUntil = 0;
991
995
  var dragAttachDoc = null;
992
996
  var currentMainTab = 'design';
@@ -1029,6 +1033,37 @@ function endSuppressIframeMutationDirty() {
1029
1033
  suppressIframeMutationDirty = Math.max(0, suppressIframeMutationDirty - 1);
1030
1034
  }
1031
1035
 
1036
+ function recoverSelectedElement(forceDeselectOnMiss) {
1037
+ if (selectedEl && selectedEl.ownerDocument && selectedEl.ownerDocument.contains(selectedEl)) {
1038
+ selectedElRecoverMisses = 0;
1039
+ return selectedEl;
1040
+ }
1041
+ if (!selectedElFingerprint) {
1042
+ if (forceDeselectOnMiss) deselectElement();
1043
+ return null;
1044
+ }
1045
+ var iframe = document.getElementById('iframeId');
1046
+ var doc = iframe && iframe.contentDocument;
1047
+ if (!doc) return null;
1048
+ var recovered = querySelectorResolved(doc, selectedElFingerprint);
1049
+ if (recovered) {
1050
+ beginSuppressIframeMutationDirty();
1051
+ try {
1052
+ selectedEl = recovered;
1053
+ if (selectedEl.classList) selectedEl.classList.add('vve-selected');
1054
+ } finally {
1055
+ endSuppressIframeMutationDirty();
1056
+ }
1057
+ selectedElRecoverMisses = 0;
1058
+ return recovered;
1059
+ }
1060
+ selectedElRecoverMisses += 1;
1061
+ if (forceDeselectOnMiss && selectedElRecoverMisses >= MAX_SELECTED_RECOVER_MISSES) {
1062
+ deselectElement();
1063
+ }
1064
+ return null;
1065
+ }
1066
+
1032
1067
  /** Stable stringify of a variation's changesets field (string or array from API). */
1033
1068
  function fingerprintChangesetsField(raw) {
1034
1069
  if (raw == null) return '[]';
@@ -2632,6 +2667,8 @@ function selectElement(el) {
2632
2667
  try {
2633
2668
  if (selectedEl) { try { selectedEl.classList.remove('vve-selected'); } catch(_) {} }
2634
2669
  selectedEl = el;
2670
+ selectedElFingerprint = buildSelector(el);
2671
+ selectedElRecoverMisses = 0;
2635
2672
  try { el.classList.add('vve-selected'); } catch(_) {}
2636
2673
  } finally {
2637
2674
  endSuppressIframeMutationDirty();
@@ -2653,6 +2690,8 @@ function deselectElement() {
2653
2690
  beginSuppressIframeMutationDirty();
2654
2691
  try {
2655
2692
  if (selectedEl) { try { selectedEl.classList.remove('vve-selected'); } catch(_) {} selectedEl = null; }
2693
+ selectedElFingerprint = '';
2694
+ selectedElRecoverMisses = 0;
2656
2695
  } finally {
2657
2696
  endSuppressIframeMutationDirty();
2658
2697
  }
@@ -2706,7 +2745,13 @@ function positionSelectionToolbar() {
2706
2745
  var bar = document.getElementById('selection-floater');
2707
2746
  var iframe = document.getElementById('iframeId');
2708
2747
  var panel = document.getElementById('iframe-panel');
2709
- if (!bar || !selectedEl || !iframe || !iframe.contentWindow || !panel) return;
2748
+ var liveSelected = recoverSelectedElement(false);
2749
+ if (!bar || !liveSelected || !iframe || !iframe.contentWindow || !panel) return;
2750
+ if (selectedEl !== liveSelected) {
2751
+ selectedEl = liveSelected;
2752
+ renderRightPanel(liveSelected);
2753
+ syncDomTreeSelection();
2754
+ }
2710
2755
  var elR = selectedEl.getBoundingClientRect();
2711
2756
  var iframeR = iframe.getBoundingClientRect();
2712
2757
  var panelR = panel.getBoundingClientRect();
@@ -2722,10 +2767,17 @@ function positionSelectionToolbar() {
2722
2767
  function updateSelectionToolbar() {
2723
2768
  var bar = document.getElementById('selection-floater');
2724
2769
  if (!bar) return;
2725
- if (!selectedEl || currentMode !== 'editor') {
2770
+ if (currentMode !== 'editor') {
2771
+ bar.style.display = 'none';
2772
+ return;
2773
+ }
2774
+ var liveSelected = recoverSelectedElement(true);
2775
+ if (!liveSelected) {
2726
2776
  bar.style.display = 'none';
2727
2777
  return;
2728
2778
  }
2779
+ if (selectedEl !== liveSelected) selectedEl = liveSelected;
2780
+ selectedElFingerprint = buildSelector(liveSelected);
2729
2781
  bar.style.display = 'flex';
2730
2782
  requestAnimationFrame(function() { positionSelectionToolbar(); });
2731
2783
  }
@@ -3693,9 +3745,13 @@ function attachChangeObserver() {
3693
3745
  // Page JS replaced body children; allow structural rows (insert/reorder) to apply again.
3694
3746
  appliedStructuralChangesetKeys = {};
3695
3747
  }
3748
+ // Host scripts can replace selected nodes every few frames (e.g. A/B tool observers).
3749
+ // Keep selection sticky by re-resolving from fingerprint.
3750
+ recoverSelectedElement(false);
3696
3751
  scheduleDomTreeRefresh();
3697
3752
  scheduleGranularChangesetReapply();
3698
3753
  scheduleConsistencyReconcile();
3754
+ updateSelectionToolbar();
3699
3755
  });
3700
3756
  changeObserver.observe(doc.body, {
3701
3757
  childList: true, subtree: true, attributes: true, characterData: true
@@ -3940,9 +3996,23 @@ function handleClose() {
3940
3996
  }
3941
3997
 
3942
3998
  // \u2500\u2500 Keyboard shortcuts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3999
+ function isNativeEditableTarget(target) {
4000
+ if (!target || target.nodeType !== 1) return false;
4001
+ if (target.isContentEditable) return true;
4002
+ if (target.closest && target.closest('[contenteditable=""],[contenteditable="true"],[contenteditable="plaintext-only"]')) {
4003
+ return true;
4004
+ }
4005
+ if (!target.tagName) return false;
4006
+ var tag = String(target.tagName).toLowerCase();
4007
+ return tag === 'input' || tag === 'textarea' || tag === 'select';
4008
+ }
4009
+
3943
4010
  document.addEventListener('keydown', function(e) {
4011
+ // Keep native browser undo/redo inside text inputs/contenteditable fields.
4012
+ if (isNativeEditableTarget(e.target)) return;
3944
4013
  var meta = e.metaKey || e.ctrlKey;
3945
- if (meta && !e.shiftKey && e.key === 'z') {
4014
+ var k = (e.key || '').toLowerCase();
4015
+ if (meta && !e.shiftKey && k === 'z') {
3946
4016
  e.preventDefault();
3947
4017
  if (typeof Vvveb !== 'undefined' && Vvveb.Undo) {
3948
4018
  Vvveb.Undo.undo();
@@ -3950,7 +4020,7 @@ document.addEventListener('keydown', function(e) {
3950
4020
  recomputeEditorDirty();
3951
4021
  }
3952
4022
  }
3953
- if (meta && e.shiftKey && e.key === 'z') {
4023
+ if (meta && e.shiftKey && k === 'z') {
3954
4024
  e.preventDefault();
3955
4025
  if (typeof Vvveb !== 'undefined' && Vvveb.Undo) {
3956
4026
  Vvveb.Undo.redo();
@@ -4419,7 +4489,96 @@ function createVisualEditorMiddleware(options) {
4419
4489
  html = html.replace("</head>", `${popupHideCss}
4420
4490
  </head>`);
4421
4491
  }
4422
- const runtimeProxyScript = `<script>(function(){try{var TARGET_ORIGIN=${JSON.stringify(origin)};var TARGET_PAGE_URL=${JSON.stringify(targetUrl)};var EMPTY_JSON_DATA="data:application/json;charset=utf-8,%7B%7D";function isSkippable(raw){if(!raw||typeof raw!=="string")return true;return raw.startsWith("data:")||raw.startsWith("blob:")||raw.startsWith("javascript:")||raw.startsWith("#")||raw.startsWith("http")||raw.startsWith("//");}function toAbsoluteOriginUrl(raw){if(isSkippable(raw))return raw;try{var base=raw.startsWith("/")?TARGET_ORIGIN:TARGET_PAGE_URL;var abs=new URL(raw,base);if(abs.origin!==TARGET_ORIGIN)return raw;return abs.toString();}catch(_){return raw;}}function resolveUrl(s){try{return new URL(s,window.location.href);}catch(_){return null;}}function isNestedMalformedProxy(u){if(!u)return false;var p=u.pathname||"";if(p==="/api/conversion-proxy"||p.indexOf("/api/conversion-proxy/")===0)return false;return p.indexOf("api/conversion-proxy")!==-1;}function skipNestedProxyNetwork(s){var u=typeof s==="string"?resolveUrl(s):null;return u&&isNestedMalformedProxy(u);}function emptyJsonFetchResponse(){return Promise.resolve(new Response("{}",{status:200,headers:{"Content-Type":"application/json; charset=utf-8"}}));}if(window.fetch){var _fetch=window.fetch.bind(window);window.fetch=function(input,init){try{var rawUrl=typeof input==="string"?input:(input&&input.url?String(input.url):"");if(rawUrl&&skipNestedProxyNetwork(rawUrl))return emptyJsonFetchResponse();if(typeof input==="string"){input=toAbsoluteOriginUrl(input);}else if(input&&input.url){var next=toAbsoluteOriginUrl(input.url);if(next!==input.url){input=new Request(next,input);}}var after=typeof input==="string"?input:(input&&input.url?String(input.url):"");if(after&&skipNestedProxyNetwork(after))return emptyJsonFetchResponse();}catch(_){}return _fetch(input,init);};}if(window.XMLHttpRequest&&window.XMLHttpRequest.prototype&&window.XMLHttpRequest.prototype.open){var _open=window.XMLHttpRequest.prototype.open;window.XMLHttpRequest.prototype.open=function(method,url){try{var u=resolveUrl(String(url));if(u&&isNestedMalformedProxy(u)){arguments[1]=EMPTY_JSON_DATA;}else{arguments[1]=toAbsoluteOriginUrl(url);}}catch(_){}return _open.apply(this,arguments);};}if(window.navigator&&typeof window.navigator.sendBeacon==="function"){var _beacon=window.navigator.sendBeacon.bind(window.navigator);window.navigator.sendBeacon=function(url,data){try{if(skipNestedProxyNetwork(String(url)))return true;}catch(_){}return _beacon(url,data);};}if(window.navigator&&window.navigator.serviceWorker&&typeof window.navigator.serviceWorker.register==="function"){window.navigator.serviceWorker.register=function(){return Promise.resolve({scope:"disabled-in-editor-proxy"});};}}catch(_){}})();</script>`;
4492
+ html = html.replace(
4493
+ /<meta[^>]+http-equiv=["']?\s*(x-frame-options|content-security-policy)\s*["']?[^>]*>/gi,
4494
+ ""
4495
+ );
4496
+ html = html.replace(
4497
+ /<meta[^>]+name=["']?\s*content-security-policy\s*["']?[^>]*>/gi,
4498
+ ""
4499
+ );
4500
+ const runtimePreflightScript = `<script>(function(){try{
4501
+ var TARGET_ORIGIN=${JSON.stringify(origin)};
4502
+ var TARGET_PAGE_URL=${JSON.stringify(targetUrl)};
4503
+ var PROXY_PASSWORD=${JSON.stringify(password)};
4504
+ window.__CONVERSION_EDITOR_ACTIVE__=true;
4505
+ function isSkippable(raw){if(!raw||typeof raw!=="string")return true;return raw.startsWith("data:")||raw.startsWith("blob:")||raw.startsWith("javascript:")||raw.startsWith("#");}
4506
+ function toAbsolute(raw){if(isSkippable(raw))return raw;try{var base=raw.startsWith("/")||raw.startsWith("//")?TARGET_ORIGIN:TARGET_PAGE_URL;return new URL(raw,base).toString();}catch(_){return raw;}}
4507
+ function toProxy(raw){if(isSkippable(raw))return null;var abs=toAbsolute(raw);if(!abs||typeof abs!=="string")return null;try{var parsed=new URL(abs);if(parsed.origin!==TARGET_ORIGIN)return null;return "/api/conversion-proxy?password="+encodeURIComponent(PROXY_PASSWORD||"")+"&url="+encodeURIComponent(parsed.toString());}catch(_){return null;}}
4508
+ var nativeAssign=window.location.assign?window.location.assign.bind(window.location):null;
4509
+ var nativeReplace=window.location.replace?window.location.replace.bind(window.location):null;
4510
+ function safeNavigate(raw,mode){var abs=toAbsolute(raw);var prox=toProxy(raw);if(!prox){try{console.warn("[conversion-proxy] redirect blocked",{mode:mode,requested:raw,resolved:abs,origin:TARGET_ORIGIN});}catch(_){}return false;}try{console.info("[conversion-proxy] redirect intercepted",{mode:mode,requested:raw,resolved:abs,proxied:prox});if(mode==="replace"&&nativeReplace){nativeReplace(prox);return true;}if(nativeAssign){nativeAssign(prox);return true;}window.location.href=prox;return true;}catch(err){try{console.warn("[conversion-proxy] redirect interception failed",{mode:mode,requested:raw,resolved:abs,proxied:prox,error:err&&err.message?err.message:String(err)});}catch(_){}return false;}}
4511
+ try{if(nativeAssign){window.location.assign=function(url){return safeNavigate(url,"assign");};}}catch(_){}
4512
+ try{if(nativeReplace){window.location.replace=function(url){return safeNavigate(url,"replace");};}}catch(_){}
4513
+ try{var hrefDesc=Object.getOwnPropertyDescriptor(Location.prototype,"href");if(hrefDesc&&hrefDesc.configurable&&hrefDesc.get&&hrefDesc.set){Object.defineProperty(Location.prototype,"href",{configurable:true,enumerable:hrefDesc.enumerable,get:function(){return hrefDesc.get.call(this);},set:function(v){safeNavigate(v,"assign");}});}}catch(_){}
4514
+ try{
4515
+ var NativeMO=window.MutationObserver;
4516
+ if(NativeMO&&!window.__CONVERSION_MO_GUARDED__){
4517
+ window.__CONVERSION_MO_GUARDED__=true;
4518
+ window.MutationObserver=function(cb){
4519
+ var last=0;
4520
+ var wrapped=function(list,obs){
4521
+ try{
4522
+ if(!window.__CONVERSION_EDITOR_ACTIVE__)return cb(list,obs);
4523
+ var now=Date.now();
4524
+ if(now-last<120)return;
4525
+ last=now;
4526
+ return cb(list,obs);
4527
+ }catch(_){}
4528
+ };
4529
+ return new NativeMO(wrapped);
4530
+ };
4531
+ window.MutationObserver.prototype=NativeMO.prototype;
4532
+ }
4533
+ }catch(_){}
4534
+ }catch(_){}})();</script>`;
4535
+ html = html.replace(/<head([^>]*)>/i, `<head$1>
4536
+ ${runtimePreflightScript}
4537
+ `);
4538
+ const runtimeProxyScript = `<script>(function(){try{
4539
+ var TARGET_ORIGIN=${JSON.stringify(origin)};
4540
+ var TARGET_PAGE_URL=${JSON.stringify(targetUrl)};
4541
+ var EMPTY_JSON_DATA="data:application/json;charset=utf-8,%7B%7D";
4542
+ function isSkippable(raw){if(!raw||typeof raw!=="string")return true;return raw.startsWith("data:")||raw.startsWith("blob:")||raw.startsWith("javascript:")||raw.startsWith("#")||raw.startsWith("http")||raw.startsWith("//");}
4543
+ function toAbsoluteOriginUrl(raw){if(isSkippable(raw))return raw;try{var base=raw.startsWith("/")?TARGET_ORIGIN:TARGET_PAGE_URL;var abs=new URL(raw,base);if(abs.origin!==TARGET_ORIGIN)return raw;return abs.toString();}catch(_){return raw;}}
4544
+ function resolveUrl(s){try{return new URL(s,window.location.href);}catch(_){return null;}}
4545
+ function isNestedMalformedProxy(u){if(!u)return false;var p=u.pathname||"";if(p==="/api/conversion-proxy"||p.indexOf("/api/conversion-proxy/")===0)return false;return p.indexOf("api/conversion-proxy")!==-1;}
4546
+ function skipNestedProxyNetwork(s){var u=typeof s==="string"?resolveUrl(s):null;return u&&isNestedMalformedProxy(u);}
4547
+ function emptyJsonFetchResponse(){return Promise.resolve(new Response("{}",{status:200,headers:{"Content-Type":"application/json; charset=utf-8"}}));}
4548
+ if(window.fetch){
4549
+ var _fetch=window.fetch.bind(window);
4550
+ window.fetch=function(input,init){
4551
+ var afterUrl="";
4552
+ try{
4553
+ var rawUrl=typeof input==="string"?input:(input&&input.url?String(input.url):"");
4554
+ if(rawUrl&&skipNestedProxyNetwork(rawUrl))return emptyJsonFetchResponse();
4555
+ if(typeof input==="string"){
4556
+ input=toAbsoluteOriginUrl(input);
4557
+ }else if(input&&input.url){
4558
+ var next=toAbsoluteOriginUrl(input.url);
4559
+ if(next!==input.url){input=new Request(next,input);}
4560
+ }
4561
+ afterUrl=typeof input==="string"?input:(input&&input.url?String(input.url):"");
4562
+ if(afterUrl&&skipNestedProxyNetwork(afterUrl))return emptyJsonFetchResponse();
4563
+ }catch(_){}
4564
+ return _fetch(input,init).catch(function(err){
4565
+ try{
4566
+ var u=afterUrl?resolveUrl(afterUrl):null;
4567
+ var sameOrigin=!!(u&&u.origin===TARGET_ORIGIN);
4568
+ var likelyThirdPartyBg=!sameOrigin||!!(u&&/(^|\\/)apps?(\\/|$)|(^|\\/)a(\\/|$)/.test(u.pathname||""));
4569
+ if(window.__CONVERSION_EDITOR_ACTIVE__&&likelyThirdPartyBg){
4570
+ console.warn("[conversion-proxy] suppressed fetch failure",{url:afterUrl,error:err&&err.message?err.message:String(err)});
4571
+ return new Response("{}",{status:200,headers:{"Content-Type":"application/json; charset=utf-8"}});
4572
+ }
4573
+ }catch(_){}
4574
+ throw err;
4575
+ });
4576
+ };
4577
+ }
4578
+ if(window.XMLHttpRequest&&window.XMLHttpRequest.prototype&&window.XMLHttpRequest.prototype.open){var _open=window.XMLHttpRequest.prototype.open;window.XMLHttpRequest.prototype.open=function(method,url){try{var u=resolveUrl(String(url));if(u&&isNestedMalformedProxy(u)){arguments[1]=EMPTY_JSON_DATA;}else{arguments[1]=toAbsoluteOriginUrl(url);}}catch(_){}return _open.apply(this,arguments);};}
4579
+ if(window.navigator&&typeof window.navigator.sendBeacon==="function"){var _beacon=window.navigator.sendBeacon.bind(window.navigator);window.navigator.sendBeacon=function(url,data){try{if(skipNestedProxyNetwork(String(url)))return true;}catch(_){}return _beacon(url,data);};}
4580
+ if(window.navigator&&window.navigator.serviceWorker&&typeof window.navigator.serviceWorker.register==="function"){window.navigator.serviceWorker.register=function(){return Promise.resolve({scope:"disabled-in-editor-proxy"});};}
4581
+ }catch(_){}})();</script>`;
4423
4582
  if (html.includes("</head>")) {
4424
4583
  html = html.replace("</head>", `${runtimeProxyScript}
4425
4584
  </head>`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@accelerated-agency/visual-editor",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "private": false,
5
5
  "description": "Conversion visual editor as a reusable React package",
6
6
  "type": "module",