@accelerated-agency/visual-editor 0.2.7 → 0.2.9

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.
Files changed (3) hide show
  1. package/dist/vite.cjs +264 -23
  2. package/dist/vite.js +264 -23
  3. package/package.json +1 -1
package/dist/vite.cjs CHANGED
@@ -995,6 +995,11 @@ 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;
1002
+ var isDeselectingSelection = false;
998
1003
  var suppressClickUntil = 0;
999
1004
  var dragAttachDoc = null;
1000
1005
  var currentMainTab = 'design';
@@ -1023,6 +1028,8 @@ var iframeDocLoadingListeners = null;
1023
1028
  // Each entry: {selector, label, cssProp, value, targetEl}
1024
1029
  // cssProp is null for non-CSS attributes (href, alt, classes\u2026)
1025
1030
  var stateChanges = [];
1031
+ /** Per-variation unsaved Design-tab state changes (input edits not yet finalized). */
1032
+ var stateChangesByVarId = {};
1026
1033
  /** Pre-apply DOM snapshots for granular + body changesets (used when removing History rows) */
1027
1034
  var appliedChangesetSnapshots = {};
1028
1035
  /** Canonical JSON fingerprints of persisted changesets per variation (last load / finalize) */
@@ -1037,6 +1044,95 @@ function endSuppressIframeMutationDirty() {
1037
1044
  suppressIframeMutationDirty = Math.max(0, suppressIframeMutationDirty - 1);
1038
1045
  }
1039
1046
 
1047
+ function commitStateChangesForActiveVariation() {
1048
+ if (!activeVarId) return;
1049
+ stateChangesByVarId[activeVarId] = (stateChanges || []).slice();
1050
+ }
1051
+
1052
+ function loadStateChangesForActiveVariation() {
1053
+ if (!activeVarId) {
1054
+ stateChanges = [];
1055
+ return;
1056
+ }
1057
+ stateChanges = (stateChangesByVarId[activeVarId] || []).slice();
1058
+ }
1059
+
1060
+ function recoverSelectedElement(forceDeselectOnMiss) {
1061
+ if (selectedEl && selectedEl.ownerDocument && selectedEl.ownerDocument.contains(selectedEl)) {
1062
+ selectedElRecoverMisses = 0;
1063
+ return selectedEl;
1064
+ }
1065
+ if (!selectedElFingerprint) {
1066
+ // Nothing to recover; clear stale reference without calling deselectElement
1067
+ // (which can recurse back through toolbar updates during rapid DOM churn).
1068
+ if (forceDeselectOnMiss && selectedEl) {
1069
+ beginSuppressIframeMutationDirty();
1070
+ try {
1071
+ try { selectedEl.classList.remove('vve-selected'); } catch(_) {}
1072
+ selectedEl = null;
1073
+ selectedElRecoverMisses = 0;
1074
+ } finally {
1075
+ endSuppressIframeMutationDirty();
1076
+ }
1077
+ var noSel = document.getElementById('no-sel');
1078
+ if (noSel) noSel.style.display = '';
1079
+ var elInfo = document.getElementById('el-info');
1080
+ if (elInfo) elInfo.style.display = 'none';
1081
+ var rp = document.getElementById('rp-accordion');
1082
+ if (rp) rp.style.display = 'none';
1083
+ var bc = document.getElementById('bc-path');
1084
+ if (bc) {
1085
+ bc.textContent = 'No element selected';
1086
+ bc.style.color = 'var(--text-3)';
1087
+ }
1088
+ syncDomTreeSelection();
1089
+ }
1090
+ return null;
1091
+ }
1092
+ var iframe = document.getElementById('iframeId');
1093
+ var doc = iframe && iframe.contentDocument;
1094
+ if (!doc) return null;
1095
+ var recovered = querySelectorResolved(doc, selectedElFingerprint);
1096
+ if (recovered) {
1097
+ beginSuppressIframeMutationDirty();
1098
+ try {
1099
+ selectedEl = recovered;
1100
+ if (selectedEl.classList) selectedEl.classList.add('vve-selected');
1101
+ } finally {
1102
+ endSuppressIframeMutationDirty();
1103
+ }
1104
+ selectedElRecoverMisses = 0;
1105
+ return recovered;
1106
+ }
1107
+ selectedElRecoverMisses += 1;
1108
+ if (forceDeselectOnMiss && selectedElRecoverMisses >= MAX_SELECTED_RECOVER_MISSES) {
1109
+ beginSuppressIframeMutationDirty();
1110
+ try {
1111
+ if (selectedEl) {
1112
+ try { selectedEl.classList.remove('vve-selected'); } catch(_) {}
1113
+ }
1114
+ selectedEl = null;
1115
+ selectedElFingerprint = '';
1116
+ selectedElRecoverMisses = 0;
1117
+ } finally {
1118
+ endSuppressIframeMutationDirty();
1119
+ }
1120
+ var noSel2 = document.getElementById('no-sel');
1121
+ if (noSel2) noSel2.style.display = '';
1122
+ var elInfo2 = document.getElementById('el-info');
1123
+ if (elInfo2) elInfo2.style.display = 'none';
1124
+ var rp2 = document.getElementById('rp-accordion');
1125
+ if (rp2) rp2.style.display = 'none';
1126
+ var bc2 = document.getElementById('bc-path');
1127
+ if (bc2) {
1128
+ bc2.textContent = 'No element selected';
1129
+ bc2.style.color = 'var(--text-3)';
1130
+ }
1131
+ syncDomTreeSelection();
1132
+ }
1133
+ return null;
1134
+ }
1135
+
1040
1136
  /** Stable stringify of a variation's changesets field (string or array from API). */
1041
1137
  function fingerprintChangesetsField(raw) {
1042
1138
  if (raw == null) return '[]';
@@ -1293,6 +1389,7 @@ function logChange(selector, inputId, value, targetEl, originalValue) {
1293
1389
  if (idx >= 0) { stateChanges[idx] = entry; } else { stateChanges.push(entry); }
1294
1390
  }
1295
1391
  if (currentMainTab === 'states') renderStatesTab();
1392
+ commitStateChangesForActiveVariation();
1296
1393
  recomputeEditorDirty();
1297
1394
  }
1298
1395
 
@@ -1399,6 +1496,7 @@ function removeStateChange(idx) {
1399
1496
  revertChangeOnDom(change);
1400
1497
  syncDesignInput(change);
1401
1498
  stateChanges.splice(idx, 1);
1499
+ commitStateChangesForActiveVariation();
1402
1500
  renderStatesTab();
1403
1501
  recomputeEditorDirty();
1404
1502
  }
@@ -1409,6 +1507,7 @@ function clearAllStates() {
1409
1507
  syncDesignInput(c);
1410
1508
  });
1411
1509
  stateChanges = [];
1510
+ commitStateChangesForActiveVariation();
1412
1511
  renderStatesTab();
1413
1512
  recomputeEditorDirty();
1414
1513
  }
@@ -1783,7 +1882,9 @@ function handleLoadExperiment(data) {
1783
1882
  experimentData = data;
1784
1883
  variations = Array.isArray(data.variations) ? data.variations : [];
1785
1884
  var prevActive = activeVarId;
1885
+ commitStateChangesForActiveVariation();
1786
1886
  activeVarId = pickActiveVariationIdForLoad(data, variations, prevActive, true);
1887
+ loadStateChangesForActiveVariation();
1787
1888
  writePersistedActiveVariationId(activeVarId);
1788
1889
  renderVariationTabs();
1789
1890
  var urlBarSkip = document.getElementById('url-bar');
@@ -1806,6 +1907,8 @@ function handleLoadExperiment(data) {
1806
1907
  if (!experimentData || prevKey !== nextKey) {
1807
1908
  varHtmlCache = {};
1808
1909
  sessionStructuralChainRowsByVarId = {};
1910
+ stateChangesByVarId = {};
1911
+ stateChanges = [];
1809
1912
  appliedChangesetSnapshots = {};
1810
1913
  appliedStructuralChangesetKeys = {};
1811
1914
  }
@@ -1813,6 +1916,7 @@ function handleLoadExperiment(data) {
1813
1916
  variations = Array.isArray(data.variations) ? data.variations : [];
1814
1917
  var sameExpPage = prevKey === nextKey;
1815
1918
  activeVarId = pickActiveVariationIdForLoad(data, variations, activeVarId, sameExpPage);
1919
+ loadStateChangesForActiveVariation();
1816
1920
  writePersistedActiveVariationId(activeVarId);
1817
1921
  renderVariationTabs();
1818
1922
 
@@ -2136,8 +2240,10 @@ function renderVariationTabs() {
2136
2240
  function switchVariation(varId) {
2137
2241
  if (varId === activeVarId) return;
2138
2242
  saveCurrentVariationHtml();
2243
+ commitStateChangesForActiveVariation();
2139
2244
  clearPendingGranularChangesets();
2140
2245
  activeVarId = varId;
2246
+ loadStateChangesForActiveVariation();
2141
2247
  writePersistedActiveVariationId(varId);
2142
2248
  renderVariationTabs();
2143
2249
  deselectElement();
@@ -2173,6 +2279,7 @@ function switchVariation(varId) {
2173
2279
  }
2174
2280
  } catch(_) {}
2175
2281
  if (currentMainTab === 'history') renderHistoryTab();
2282
+ if (currentMainTab === 'states') renderStatesTab();
2176
2283
  recomputeEditorDirty();
2177
2284
  }
2178
2285
 
@@ -2529,12 +2636,13 @@ function buildPersistedChainSetsForVariation(v) {
2529
2636
  var parsed = parseVariationChangesets(v);
2530
2637
  var base = filterGranularChangesetEntries(parsed);
2531
2638
  var sessionExtra = sessionStructuralChainRowsByVarId[v._id] || [];
2532
- if (v._id !== activeVarId) {
2533
- return mergeGranularChainSets(base, sessionExtra);
2534
- }
2639
+ var sourceStateChanges =
2640
+ v._id === activeVarId
2641
+ ? stateChanges
2642
+ : (stateChangesByVarId[v._id] || []);
2535
2643
  var overlay = [];
2536
- for (var si = 0; si < stateChanges.length; si++) {
2537
- var row = stateChangeToChainSet(stateChanges[si]);
2644
+ for (var si = 0; si < sourceStateChanges.length; si++) {
2645
+ var row = stateChangeToChainSet(sourceStateChanges[si]);
2538
2646
  if (row) overlay.push(row);
2539
2647
  }
2540
2648
  return mergeGranularChainSets(mergeGranularChainSets(base, sessionExtra), overlay);
@@ -2640,6 +2748,8 @@ function selectElement(el) {
2640
2748
  try {
2641
2749
  if (selectedEl) { try { selectedEl.classList.remove('vve-selected'); } catch(_) {} }
2642
2750
  selectedEl = el;
2751
+ selectedElFingerprint = buildSelector(el);
2752
+ selectedElRecoverMisses = 0;
2643
2753
  try { el.classList.add('vve-selected'); } catch(_) {}
2644
2754
  } finally {
2645
2755
  endSuppressIframeMutationDirty();
@@ -2656,22 +2766,31 @@ function selectElement(el) {
2656
2766
  }
2657
2767
  }
2658
2768
 
2659
- function deselectElement() {
2660
- setDragHandleActive(false);
2661
- beginSuppressIframeMutationDirty();
2769
+ function deselectElement(options) {
2770
+ if (isDeselectingSelection) return;
2771
+ isDeselectingSelection = true;
2772
+ var skipToolbarUpdate = !!(options && options.skipToolbarUpdate);
2662
2773
  try {
2663
- if (selectedEl) { try { selectedEl.classList.remove('vve-selected'); } catch(_) {} selectedEl = null; }
2774
+ setDragHandleActive(false);
2775
+ beginSuppressIframeMutationDirty();
2776
+ try {
2777
+ if (selectedEl) { try { selectedEl.classList.remove('vve-selected'); } catch(_) {} selectedEl = null; }
2778
+ selectedElFingerprint = '';
2779
+ selectedElRecoverMisses = 0;
2780
+ } finally {
2781
+ endSuppressIframeMutationDirty();
2782
+ }
2783
+ document.getElementById('no-sel').style.display = '';
2784
+ document.getElementById('el-info').style.display = 'none';
2785
+ document.getElementById('rp-accordion').style.display = 'none';
2786
+ document.getElementById('bc-path').textContent = 'No element selected';
2787
+ document.getElementById('bc-path').style.color = 'var(--text-3)';
2788
+ switchMainTab('design');
2789
+ if (!skipToolbarUpdate) updateSelectionToolbar();
2790
+ syncDomTreeSelection();
2664
2791
  } finally {
2665
- endSuppressIframeMutationDirty();
2792
+ isDeselectingSelection = false;
2666
2793
  }
2667
- document.getElementById('no-sel').style.display = '';
2668
- document.getElementById('el-info').style.display = 'none';
2669
- document.getElementById('rp-accordion').style.display = 'none';
2670
- document.getElementById('bc-path').textContent = 'No element selected';
2671
- document.getElementById('bc-path').style.color = 'var(--text-3)';
2672
- switchMainTab('design');
2673
- updateSelectionToolbar();
2674
- syncDomTreeSelection();
2675
2794
  }
2676
2795
 
2677
2796
  // \u2500\u2500 Iframe selection chrome, floater toolbar, DOM tree \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
@@ -2714,7 +2833,13 @@ function positionSelectionToolbar() {
2714
2833
  var bar = document.getElementById('selection-floater');
2715
2834
  var iframe = document.getElementById('iframeId');
2716
2835
  var panel = document.getElementById('iframe-panel');
2717
- if (!bar || !selectedEl || !iframe || !iframe.contentWindow || !panel) return;
2836
+ var liveSelected = recoverSelectedElement(false);
2837
+ if (!bar || !liveSelected || !iframe || !iframe.contentWindow || !panel) return;
2838
+ if (selectedEl !== liveSelected) {
2839
+ selectedEl = liveSelected;
2840
+ renderRightPanel(liveSelected);
2841
+ syncDomTreeSelection();
2842
+ }
2718
2843
  var elR = selectedEl.getBoundingClientRect();
2719
2844
  var iframeR = iframe.getBoundingClientRect();
2720
2845
  var panelR = panel.getBoundingClientRect();
@@ -2730,10 +2855,17 @@ function positionSelectionToolbar() {
2730
2855
  function updateSelectionToolbar() {
2731
2856
  var bar = document.getElementById('selection-floater');
2732
2857
  if (!bar) return;
2733
- if (!selectedEl || currentMode !== 'editor') {
2858
+ if (currentMode !== 'editor') {
2734
2859
  bar.style.display = 'none';
2735
2860
  return;
2736
2861
  }
2862
+ var liveSelected = recoverSelectedElement(true);
2863
+ if (!liveSelected) {
2864
+ bar.style.display = 'none';
2865
+ return;
2866
+ }
2867
+ if (selectedEl !== liveSelected) selectedEl = liveSelected;
2868
+ selectedElFingerprint = buildSelector(liveSelected);
2737
2869
  bar.style.display = 'flex';
2738
2870
  requestAnimationFrame(function() { positionSelectionToolbar(); });
2739
2871
  }
@@ -3701,9 +3833,13 @@ function attachChangeObserver() {
3701
3833
  // Page JS replaced body children; allow structural rows (insert/reorder) to apply again.
3702
3834
  appliedStructuralChangesetKeys = {};
3703
3835
  }
3836
+ // Host scripts can replace selected nodes every few frames (e.g. A/B tool observers).
3837
+ // Keep selection sticky by re-resolving from fingerprint.
3838
+ recoverSelectedElement(false);
3704
3839
  scheduleDomTreeRefresh();
3705
3840
  scheduleGranularChangesetReapply();
3706
3841
  scheduleConsistencyReconcile();
3842
+ updateSelectionToolbar();
3707
3843
  });
3708
3844
  changeObserver.observe(doc.body, {
3709
3845
  childList: true, subtree: true, attributes: true, characterData: true
@@ -3911,6 +4047,7 @@ document.getElementById('btn-close').addEventListener('click', handleClose);
3911
4047
 
3912
4048
  function handleSave() {
3913
4049
  saveCurrentVariationHtml();
4050
+ commitStateChangesForActiveVariation();
3914
4051
  var updatedVariations = variations.map(function(v) {
3915
4052
  var prevParsed = parseVariationChangesets(v);
3916
4053
  var granularPrev = filterGranularChangesetEntries(prevParsed);
@@ -3931,6 +4068,7 @@ function handleSave() {
3931
4068
  variations = updatedVariations;
3932
4069
  varHtmlCache = {};
3933
4070
  sessionStructuralChainRowsByVarId = {};
4071
+ stateChangesByVarId = {};
3934
4072
  stateChanges = [];
3935
4073
  if (currentMainTab === 'states') renderStatesTab();
3936
4074
  captureBaselineFromVariations(variations);
@@ -3948,9 +4086,23 @@ function handleClose() {
3948
4086
  }
3949
4087
 
3950
4088
  // \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
4089
+ function isNativeEditableTarget(target) {
4090
+ if (!target || target.nodeType !== 1) return false;
4091
+ if (target.isContentEditable) return true;
4092
+ if (target.closest && target.closest('[contenteditable=""],[contenteditable="true"],[contenteditable="plaintext-only"]')) {
4093
+ return true;
4094
+ }
4095
+ if (!target.tagName) return false;
4096
+ var tag = String(target.tagName).toLowerCase();
4097
+ return tag === 'input' || tag === 'textarea' || tag === 'select';
4098
+ }
4099
+
3951
4100
  document.addEventListener('keydown', function(e) {
4101
+ // Keep native browser undo/redo inside text inputs/contenteditable fields.
4102
+ if (isNativeEditableTarget(e.target)) return;
3952
4103
  var meta = e.metaKey || e.ctrlKey;
3953
- if (meta && !e.shiftKey && e.key === 'z') {
4104
+ var k = (e.key || '').toLowerCase();
4105
+ if (meta && !e.shiftKey && k === 'z') {
3954
4106
  e.preventDefault();
3955
4107
  if (typeof Vvveb !== 'undefined' && Vvveb.Undo) {
3956
4108
  Vvveb.Undo.undo();
@@ -3958,7 +4110,7 @@ document.addEventListener('keydown', function(e) {
3958
4110
  recomputeEditorDirty();
3959
4111
  }
3960
4112
  }
3961
- if (meta && e.shiftKey && e.key === 'z') {
4113
+ if (meta && e.shiftKey && k === 'z') {
3962
4114
  e.preventDefault();
3963
4115
  if (typeof Vvveb !== 'undefined' && Vvveb.Undo) {
3964
4116
  Vvveb.Undo.redo();
@@ -4427,7 +4579,96 @@ function createVisualEditorMiddleware(options) {
4427
4579
  html = html.replace("</head>", `${popupHideCss}
4428
4580
  </head>`);
4429
4581
  }
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>`;
4582
+ html = html.replace(
4583
+ /<meta[^>]+http-equiv=["']?\s*(x-frame-options|content-security-policy)\s*["']?[^>]*>/gi,
4584
+ ""
4585
+ );
4586
+ html = html.replace(
4587
+ /<meta[^>]+name=["']?\s*content-security-policy\s*["']?[^>]*>/gi,
4588
+ ""
4589
+ );
4590
+ const runtimePreflightScript = `<script>(function(){try{
4591
+ var TARGET_ORIGIN=${JSON.stringify(origin)};
4592
+ var TARGET_PAGE_URL=${JSON.stringify(targetUrl)};
4593
+ var PROXY_PASSWORD=${JSON.stringify(password)};
4594
+ window.__CONVERSION_EDITOR_ACTIVE__=true;
4595
+ function isSkippable(raw){if(!raw||typeof raw!=="string")return true;return raw.startsWith("data:")||raw.startsWith("blob:")||raw.startsWith("javascript:")||raw.startsWith("#");}
4596
+ 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;}}
4597
+ 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;}}
4598
+ var nativeAssign=window.location.assign?window.location.assign.bind(window.location):null;
4599
+ var nativeReplace=window.location.replace?window.location.replace.bind(window.location):null;
4600
+ 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;}}
4601
+ try{if(nativeAssign){window.location.assign=function(url){return safeNavigate(url,"assign");};}}catch(_){}
4602
+ try{if(nativeReplace){window.location.replace=function(url){return safeNavigate(url,"replace");};}}catch(_){}
4603
+ 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(_){}
4604
+ try{
4605
+ var NativeMO=window.MutationObserver;
4606
+ if(NativeMO&&!window.__CONVERSION_MO_GUARDED__){
4607
+ window.__CONVERSION_MO_GUARDED__=true;
4608
+ window.MutationObserver=function(cb){
4609
+ var last=0;
4610
+ var wrapped=function(list,obs){
4611
+ try{
4612
+ if(!window.__CONVERSION_EDITOR_ACTIVE__)return cb(list,obs);
4613
+ var now=Date.now();
4614
+ if(now-last<120)return;
4615
+ last=now;
4616
+ return cb(list,obs);
4617
+ }catch(_){}
4618
+ };
4619
+ return new NativeMO(wrapped);
4620
+ };
4621
+ window.MutationObserver.prototype=NativeMO.prototype;
4622
+ }
4623
+ }catch(_){}
4624
+ }catch(_){}})();</script>`;
4625
+ html = html.replace(/<head([^>]*)>/i, `<head$1>
4626
+ ${runtimePreflightScript}
4627
+ `);
4628
+ const runtimeProxyScript = `<script>(function(){try{
4629
+ var TARGET_ORIGIN=${JSON.stringify(origin)};
4630
+ var TARGET_PAGE_URL=${JSON.stringify(targetUrl)};
4631
+ var EMPTY_JSON_DATA="data:application/json;charset=utf-8,%7B%7D";
4632
+ 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("//");}
4633
+ 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;}}
4634
+ function resolveUrl(s){try{return new URL(s,window.location.href);}catch(_){return null;}}
4635
+ 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;}
4636
+ function skipNestedProxyNetwork(s){var u=typeof s==="string"?resolveUrl(s):null;return u&&isNestedMalformedProxy(u);}
4637
+ function emptyJsonFetchResponse(){return Promise.resolve(new Response("{}",{status:200,headers:{"Content-Type":"application/json; charset=utf-8"}}));}
4638
+ if(window.fetch){
4639
+ var _fetch=window.fetch.bind(window);
4640
+ window.fetch=function(input,init){
4641
+ var afterUrl="";
4642
+ try{
4643
+ var rawUrl=typeof input==="string"?input:(input&&input.url?String(input.url):"");
4644
+ if(rawUrl&&skipNestedProxyNetwork(rawUrl))return emptyJsonFetchResponse();
4645
+ if(typeof input==="string"){
4646
+ input=toAbsoluteOriginUrl(input);
4647
+ }else if(input&&input.url){
4648
+ var next=toAbsoluteOriginUrl(input.url);
4649
+ if(next!==input.url){input=new Request(next,input);}
4650
+ }
4651
+ afterUrl=typeof input==="string"?input:(input&&input.url?String(input.url):"");
4652
+ if(afterUrl&&skipNestedProxyNetwork(afterUrl))return emptyJsonFetchResponse();
4653
+ }catch(_){}
4654
+ return _fetch(input,init).catch(function(err){
4655
+ try{
4656
+ var u=afterUrl?resolveUrl(afterUrl):null;
4657
+ var sameOrigin=!!(u&&u.origin===TARGET_ORIGIN);
4658
+ var likelyThirdPartyBg=!sameOrigin||!!(u&&/(^|\\/)apps?(\\/|$)|(^|\\/)a(\\/|$)/.test(u.pathname||""));
4659
+ if(window.__CONVERSION_EDITOR_ACTIVE__&&likelyThirdPartyBg){
4660
+ console.warn("[conversion-proxy] suppressed fetch failure",{url:afterUrl,error:err&&err.message?err.message:String(err)});
4661
+ return new Response("{}",{status:200,headers:{"Content-Type":"application/json; charset=utf-8"}});
4662
+ }
4663
+ }catch(_){}
4664
+ throw err;
4665
+ });
4666
+ };
4667
+ }
4668
+ 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);};}
4669
+ 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);};}
4670
+ 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"});};}
4671
+ }catch(_){}})();</script>`;
4431
4672
  if (html.includes("</head>")) {
4432
4673
  html = html.replace("</head>", `${runtimeProxyScript}
4433
4674
  </head>`);
package/dist/vite.js CHANGED
@@ -987,6 +987,11 @@ 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;
994
+ var isDeselectingSelection = false;
990
995
  var suppressClickUntil = 0;
991
996
  var dragAttachDoc = null;
992
997
  var currentMainTab = 'design';
@@ -1015,6 +1020,8 @@ var iframeDocLoadingListeners = null;
1015
1020
  // Each entry: {selector, label, cssProp, value, targetEl}
1016
1021
  // cssProp is null for non-CSS attributes (href, alt, classes\u2026)
1017
1022
  var stateChanges = [];
1023
+ /** Per-variation unsaved Design-tab state changes (input edits not yet finalized). */
1024
+ var stateChangesByVarId = {};
1018
1025
  /** Pre-apply DOM snapshots for granular + body changesets (used when removing History rows) */
1019
1026
  var appliedChangesetSnapshots = {};
1020
1027
  /** Canonical JSON fingerprints of persisted changesets per variation (last load / finalize) */
@@ -1029,6 +1036,95 @@ function endSuppressIframeMutationDirty() {
1029
1036
  suppressIframeMutationDirty = Math.max(0, suppressIframeMutationDirty - 1);
1030
1037
  }
1031
1038
 
1039
+ function commitStateChangesForActiveVariation() {
1040
+ if (!activeVarId) return;
1041
+ stateChangesByVarId[activeVarId] = (stateChanges || []).slice();
1042
+ }
1043
+
1044
+ function loadStateChangesForActiveVariation() {
1045
+ if (!activeVarId) {
1046
+ stateChanges = [];
1047
+ return;
1048
+ }
1049
+ stateChanges = (stateChangesByVarId[activeVarId] || []).slice();
1050
+ }
1051
+
1052
+ function recoverSelectedElement(forceDeselectOnMiss) {
1053
+ if (selectedEl && selectedEl.ownerDocument && selectedEl.ownerDocument.contains(selectedEl)) {
1054
+ selectedElRecoverMisses = 0;
1055
+ return selectedEl;
1056
+ }
1057
+ if (!selectedElFingerprint) {
1058
+ // Nothing to recover; clear stale reference without calling deselectElement
1059
+ // (which can recurse back through toolbar updates during rapid DOM churn).
1060
+ if (forceDeselectOnMiss && selectedEl) {
1061
+ beginSuppressIframeMutationDirty();
1062
+ try {
1063
+ try { selectedEl.classList.remove('vve-selected'); } catch(_) {}
1064
+ selectedEl = null;
1065
+ selectedElRecoverMisses = 0;
1066
+ } finally {
1067
+ endSuppressIframeMutationDirty();
1068
+ }
1069
+ var noSel = document.getElementById('no-sel');
1070
+ if (noSel) noSel.style.display = '';
1071
+ var elInfo = document.getElementById('el-info');
1072
+ if (elInfo) elInfo.style.display = 'none';
1073
+ var rp = document.getElementById('rp-accordion');
1074
+ if (rp) rp.style.display = 'none';
1075
+ var bc = document.getElementById('bc-path');
1076
+ if (bc) {
1077
+ bc.textContent = 'No element selected';
1078
+ bc.style.color = 'var(--text-3)';
1079
+ }
1080
+ syncDomTreeSelection();
1081
+ }
1082
+ return null;
1083
+ }
1084
+ var iframe = document.getElementById('iframeId');
1085
+ var doc = iframe && iframe.contentDocument;
1086
+ if (!doc) return null;
1087
+ var recovered = querySelectorResolved(doc, selectedElFingerprint);
1088
+ if (recovered) {
1089
+ beginSuppressIframeMutationDirty();
1090
+ try {
1091
+ selectedEl = recovered;
1092
+ if (selectedEl.classList) selectedEl.classList.add('vve-selected');
1093
+ } finally {
1094
+ endSuppressIframeMutationDirty();
1095
+ }
1096
+ selectedElRecoverMisses = 0;
1097
+ return recovered;
1098
+ }
1099
+ selectedElRecoverMisses += 1;
1100
+ if (forceDeselectOnMiss && selectedElRecoverMisses >= MAX_SELECTED_RECOVER_MISSES) {
1101
+ beginSuppressIframeMutationDirty();
1102
+ try {
1103
+ if (selectedEl) {
1104
+ try { selectedEl.classList.remove('vve-selected'); } catch(_) {}
1105
+ }
1106
+ selectedEl = null;
1107
+ selectedElFingerprint = '';
1108
+ selectedElRecoverMisses = 0;
1109
+ } finally {
1110
+ endSuppressIframeMutationDirty();
1111
+ }
1112
+ var noSel2 = document.getElementById('no-sel');
1113
+ if (noSel2) noSel2.style.display = '';
1114
+ var elInfo2 = document.getElementById('el-info');
1115
+ if (elInfo2) elInfo2.style.display = 'none';
1116
+ var rp2 = document.getElementById('rp-accordion');
1117
+ if (rp2) rp2.style.display = 'none';
1118
+ var bc2 = document.getElementById('bc-path');
1119
+ if (bc2) {
1120
+ bc2.textContent = 'No element selected';
1121
+ bc2.style.color = 'var(--text-3)';
1122
+ }
1123
+ syncDomTreeSelection();
1124
+ }
1125
+ return null;
1126
+ }
1127
+
1032
1128
  /** Stable stringify of a variation's changesets field (string or array from API). */
1033
1129
  function fingerprintChangesetsField(raw) {
1034
1130
  if (raw == null) return '[]';
@@ -1285,6 +1381,7 @@ function logChange(selector, inputId, value, targetEl, originalValue) {
1285
1381
  if (idx >= 0) { stateChanges[idx] = entry; } else { stateChanges.push(entry); }
1286
1382
  }
1287
1383
  if (currentMainTab === 'states') renderStatesTab();
1384
+ commitStateChangesForActiveVariation();
1288
1385
  recomputeEditorDirty();
1289
1386
  }
1290
1387
 
@@ -1391,6 +1488,7 @@ function removeStateChange(idx) {
1391
1488
  revertChangeOnDom(change);
1392
1489
  syncDesignInput(change);
1393
1490
  stateChanges.splice(idx, 1);
1491
+ commitStateChangesForActiveVariation();
1394
1492
  renderStatesTab();
1395
1493
  recomputeEditorDirty();
1396
1494
  }
@@ -1401,6 +1499,7 @@ function clearAllStates() {
1401
1499
  syncDesignInput(c);
1402
1500
  });
1403
1501
  stateChanges = [];
1502
+ commitStateChangesForActiveVariation();
1404
1503
  renderStatesTab();
1405
1504
  recomputeEditorDirty();
1406
1505
  }
@@ -1775,7 +1874,9 @@ function handleLoadExperiment(data) {
1775
1874
  experimentData = data;
1776
1875
  variations = Array.isArray(data.variations) ? data.variations : [];
1777
1876
  var prevActive = activeVarId;
1877
+ commitStateChangesForActiveVariation();
1778
1878
  activeVarId = pickActiveVariationIdForLoad(data, variations, prevActive, true);
1879
+ loadStateChangesForActiveVariation();
1779
1880
  writePersistedActiveVariationId(activeVarId);
1780
1881
  renderVariationTabs();
1781
1882
  var urlBarSkip = document.getElementById('url-bar');
@@ -1798,6 +1899,8 @@ function handleLoadExperiment(data) {
1798
1899
  if (!experimentData || prevKey !== nextKey) {
1799
1900
  varHtmlCache = {};
1800
1901
  sessionStructuralChainRowsByVarId = {};
1902
+ stateChangesByVarId = {};
1903
+ stateChanges = [];
1801
1904
  appliedChangesetSnapshots = {};
1802
1905
  appliedStructuralChangesetKeys = {};
1803
1906
  }
@@ -1805,6 +1908,7 @@ function handleLoadExperiment(data) {
1805
1908
  variations = Array.isArray(data.variations) ? data.variations : [];
1806
1909
  var sameExpPage = prevKey === nextKey;
1807
1910
  activeVarId = pickActiveVariationIdForLoad(data, variations, activeVarId, sameExpPage);
1911
+ loadStateChangesForActiveVariation();
1808
1912
  writePersistedActiveVariationId(activeVarId);
1809
1913
  renderVariationTabs();
1810
1914
 
@@ -2128,8 +2232,10 @@ function renderVariationTabs() {
2128
2232
  function switchVariation(varId) {
2129
2233
  if (varId === activeVarId) return;
2130
2234
  saveCurrentVariationHtml();
2235
+ commitStateChangesForActiveVariation();
2131
2236
  clearPendingGranularChangesets();
2132
2237
  activeVarId = varId;
2238
+ loadStateChangesForActiveVariation();
2133
2239
  writePersistedActiveVariationId(varId);
2134
2240
  renderVariationTabs();
2135
2241
  deselectElement();
@@ -2165,6 +2271,7 @@ function switchVariation(varId) {
2165
2271
  }
2166
2272
  } catch(_) {}
2167
2273
  if (currentMainTab === 'history') renderHistoryTab();
2274
+ if (currentMainTab === 'states') renderStatesTab();
2168
2275
  recomputeEditorDirty();
2169
2276
  }
2170
2277
 
@@ -2521,12 +2628,13 @@ function buildPersistedChainSetsForVariation(v) {
2521
2628
  var parsed = parseVariationChangesets(v);
2522
2629
  var base = filterGranularChangesetEntries(parsed);
2523
2630
  var sessionExtra = sessionStructuralChainRowsByVarId[v._id] || [];
2524
- if (v._id !== activeVarId) {
2525
- return mergeGranularChainSets(base, sessionExtra);
2526
- }
2631
+ var sourceStateChanges =
2632
+ v._id === activeVarId
2633
+ ? stateChanges
2634
+ : (stateChangesByVarId[v._id] || []);
2527
2635
  var overlay = [];
2528
- for (var si = 0; si < stateChanges.length; si++) {
2529
- var row = stateChangeToChainSet(stateChanges[si]);
2636
+ for (var si = 0; si < sourceStateChanges.length; si++) {
2637
+ var row = stateChangeToChainSet(sourceStateChanges[si]);
2530
2638
  if (row) overlay.push(row);
2531
2639
  }
2532
2640
  return mergeGranularChainSets(mergeGranularChainSets(base, sessionExtra), overlay);
@@ -2632,6 +2740,8 @@ function selectElement(el) {
2632
2740
  try {
2633
2741
  if (selectedEl) { try { selectedEl.classList.remove('vve-selected'); } catch(_) {} }
2634
2742
  selectedEl = el;
2743
+ selectedElFingerprint = buildSelector(el);
2744
+ selectedElRecoverMisses = 0;
2635
2745
  try { el.classList.add('vve-selected'); } catch(_) {}
2636
2746
  } finally {
2637
2747
  endSuppressIframeMutationDirty();
@@ -2648,22 +2758,31 @@ function selectElement(el) {
2648
2758
  }
2649
2759
  }
2650
2760
 
2651
- function deselectElement() {
2652
- setDragHandleActive(false);
2653
- beginSuppressIframeMutationDirty();
2761
+ function deselectElement(options) {
2762
+ if (isDeselectingSelection) return;
2763
+ isDeselectingSelection = true;
2764
+ var skipToolbarUpdate = !!(options && options.skipToolbarUpdate);
2654
2765
  try {
2655
- if (selectedEl) { try { selectedEl.classList.remove('vve-selected'); } catch(_) {} selectedEl = null; }
2766
+ setDragHandleActive(false);
2767
+ beginSuppressIframeMutationDirty();
2768
+ try {
2769
+ if (selectedEl) { try { selectedEl.classList.remove('vve-selected'); } catch(_) {} selectedEl = null; }
2770
+ selectedElFingerprint = '';
2771
+ selectedElRecoverMisses = 0;
2772
+ } finally {
2773
+ endSuppressIframeMutationDirty();
2774
+ }
2775
+ document.getElementById('no-sel').style.display = '';
2776
+ document.getElementById('el-info').style.display = 'none';
2777
+ document.getElementById('rp-accordion').style.display = 'none';
2778
+ document.getElementById('bc-path').textContent = 'No element selected';
2779
+ document.getElementById('bc-path').style.color = 'var(--text-3)';
2780
+ switchMainTab('design');
2781
+ if (!skipToolbarUpdate) updateSelectionToolbar();
2782
+ syncDomTreeSelection();
2656
2783
  } finally {
2657
- endSuppressIframeMutationDirty();
2784
+ isDeselectingSelection = false;
2658
2785
  }
2659
- document.getElementById('no-sel').style.display = '';
2660
- document.getElementById('el-info').style.display = 'none';
2661
- document.getElementById('rp-accordion').style.display = 'none';
2662
- document.getElementById('bc-path').textContent = 'No element selected';
2663
- document.getElementById('bc-path').style.color = 'var(--text-3)';
2664
- switchMainTab('design');
2665
- updateSelectionToolbar();
2666
- syncDomTreeSelection();
2667
2786
  }
2668
2787
 
2669
2788
  // \u2500\u2500 Iframe selection chrome, floater toolbar, DOM tree \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
@@ -2706,7 +2825,13 @@ function positionSelectionToolbar() {
2706
2825
  var bar = document.getElementById('selection-floater');
2707
2826
  var iframe = document.getElementById('iframeId');
2708
2827
  var panel = document.getElementById('iframe-panel');
2709
- if (!bar || !selectedEl || !iframe || !iframe.contentWindow || !panel) return;
2828
+ var liveSelected = recoverSelectedElement(false);
2829
+ if (!bar || !liveSelected || !iframe || !iframe.contentWindow || !panel) return;
2830
+ if (selectedEl !== liveSelected) {
2831
+ selectedEl = liveSelected;
2832
+ renderRightPanel(liveSelected);
2833
+ syncDomTreeSelection();
2834
+ }
2710
2835
  var elR = selectedEl.getBoundingClientRect();
2711
2836
  var iframeR = iframe.getBoundingClientRect();
2712
2837
  var panelR = panel.getBoundingClientRect();
@@ -2722,10 +2847,17 @@ function positionSelectionToolbar() {
2722
2847
  function updateSelectionToolbar() {
2723
2848
  var bar = document.getElementById('selection-floater');
2724
2849
  if (!bar) return;
2725
- if (!selectedEl || currentMode !== 'editor') {
2850
+ if (currentMode !== 'editor') {
2726
2851
  bar.style.display = 'none';
2727
2852
  return;
2728
2853
  }
2854
+ var liveSelected = recoverSelectedElement(true);
2855
+ if (!liveSelected) {
2856
+ bar.style.display = 'none';
2857
+ return;
2858
+ }
2859
+ if (selectedEl !== liveSelected) selectedEl = liveSelected;
2860
+ selectedElFingerprint = buildSelector(liveSelected);
2729
2861
  bar.style.display = 'flex';
2730
2862
  requestAnimationFrame(function() { positionSelectionToolbar(); });
2731
2863
  }
@@ -3693,9 +3825,13 @@ function attachChangeObserver() {
3693
3825
  // Page JS replaced body children; allow structural rows (insert/reorder) to apply again.
3694
3826
  appliedStructuralChangesetKeys = {};
3695
3827
  }
3828
+ // Host scripts can replace selected nodes every few frames (e.g. A/B tool observers).
3829
+ // Keep selection sticky by re-resolving from fingerprint.
3830
+ recoverSelectedElement(false);
3696
3831
  scheduleDomTreeRefresh();
3697
3832
  scheduleGranularChangesetReapply();
3698
3833
  scheduleConsistencyReconcile();
3834
+ updateSelectionToolbar();
3699
3835
  });
3700
3836
  changeObserver.observe(doc.body, {
3701
3837
  childList: true, subtree: true, attributes: true, characterData: true
@@ -3903,6 +4039,7 @@ document.getElementById('btn-close').addEventListener('click', handleClose);
3903
4039
 
3904
4040
  function handleSave() {
3905
4041
  saveCurrentVariationHtml();
4042
+ commitStateChangesForActiveVariation();
3906
4043
  var updatedVariations = variations.map(function(v) {
3907
4044
  var prevParsed = parseVariationChangesets(v);
3908
4045
  var granularPrev = filterGranularChangesetEntries(prevParsed);
@@ -3923,6 +4060,7 @@ function handleSave() {
3923
4060
  variations = updatedVariations;
3924
4061
  varHtmlCache = {};
3925
4062
  sessionStructuralChainRowsByVarId = {};
4063
+ stateChangesByVarId = {};
3926
4064
  stateChanges = [];
3927
4065
  if (currentMainTab === 'states') renderStatesTab();
3928
4066
  captureBaselineFromVariations(variations);
@@ -3940,9 +4078,23 @@ function handleClose() {
3940
4078
  }
3941
4079
 
3942
4080
  // \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
4081
+ function isNativeEditableTarget(target) {
4082
+ if (!target || target.nodeType !== 1) return false;
4083
+ if (target.isContentEditable) return true;
4084
+ if (target.closest && target.closest('[contenteditable=""],[contenteditable="true"],[contenteditable="plaintext-only"]')) {
4085
+ return true;
4086
+ }
4087
+ if (!target.tagName) return false;
4088
+ var tag = String(target.tagName).toLowerCase();
4089
+ return tag === 'input' || tag === 'textarea' || tag === 'select';
4090
+ }
4091
+
3943
4092
  document.addEventListener('keydown', function(e) {
4093
+ // Keep native browser undo/redo inside text inputs/contenteditable fields.
4094
+ if (isNativeEditableTarget(e.target)) return;
3944
4095
  var meta = e.metaKey || e.ctrlKey;
3945
- if (meta && !e.shiftKey && e.key === 'z') {
4096
+ var k = (e.key || '').toLowerCase();
4097
+ if (meta && !e.shiftKey && k === 'z') {
3946
4098
  e.preventDefault();
3947
4099
  if (typeof Vvveb !== 'undefined' && Vvveb.Undo) {
3948
4100
  Vvveb.Undo.undo();
@@ -3950,7 +4102,7 @@ document.addEventListener('keydown', function(e) {
3950
4102
  recomputeEditorDirty();
3951
4103
  }
3952
4104
  }
3953
- if (meta && e.shiftKey && e.key === 'z') {
4105
+ if (meta && e.shiftKey && k === 'z') {
3954
4106
  e.preventDefault();
3955
4107
  if (typeof Vvveb !== 'undefined' && Vvveb.Undo) {
3956
4108
  Vvveb.Undo.redo();
@@ -4419,7 +4571,96 @@ function createVisualEditorMiddleware(options) {
4419
4571
  html = html.replace("</head>", `${popupHideCss}
4420
4572
  </head>`);
4421
4573
  }
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>`;
4574
+ html = html.replace(
4575
+ /<meta[^>]+http-equiv=["']?\s*(x-frame-options|content-security-policy)\s*["']?[^>]*>/gi,
4576
+ ""
4577
+ );
4578
+ html = html.replace(
4579
+ /<meta[^>]+name=["']?\s*content-security-policy\s*["']?[^>]*>/gi,
4580
+ ""
4581
+ );
4582
+ const runtimePreflightScript = `<script>(function(){try{
4583
+ var TARGET_ORIGIN=${JSON.stringify(origin)};
4584
+ var TARGET_PAGE_URL=${JSON.stringify(targetUrl)};
4585
+ var PROXY_PASSWORD=${JSON.stringify(password)};
4586
+ window.__CONVERSION_EDITOR_ACTIVE__=true;
4587
+ function isSkippable(raw){if(!raw||typeof raw!=="string")return true;return raw.startsWith("data:")||raw.startsWith("blob:")||raw.startsWith("javascript:")||raw.startsWith("#");}
4588
+ 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;}}
4589
+ 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;}}
4590
+ var nativeAssign=window.location.assign?window.location.assign.bind(window.location):null;
4591
+ var nativeReplace=window.location.replace?window.location.replace.bind(window.location):null;
4592
+ 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;}}
4593
+ try{if(nativeAssign){window.location.assign=function(url){return safeNavigate(url,"assign");};}}catch(_){}
4594
+ try{if(nativeReplace){window.location.replace=function(url){return safeNavigate(url,"replace");};}}catch(_){}
4595
+ 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(_){}
4596
+ try{
4597
+ var NativeMO=window.MutationObserver;
4598
+ if(NativeMO&&!window.__CONVERSION_MO_GUARDED__){
4599
+ window.__CONVERSION_MO_GUARDED__=true;
4600
+ window.MutationObserver=function(cb){
4601
+ var last=0;
4602
+ var wrapped=function(list,obs){
4603
+ try{
4604
+ if(!window.__CONVERSION_EDITOR_ACTIVE__)return cb(list,obs);
4605
+ var now=Date.now();
4606
+ if(now-last<120)return;
4607
+ last=now;
4608
+ return cb(list,obs);
4609
+ }catch(_){}
4610
+ };
4611
+ return new NativeMO(wrapped);
4612
+ };
4613
+ window.MutationObserver.prototype=NativeMO.prototype;
4614
+ }
4615
+ }catch(_){}
4616
+ }catch(_){}})();</script>`;
4617
+ html = html.replace(/<head([^>]*)>/i, `<head$1>
4618
+ ${runtimePreflightScript}
4619
+ `);
4620
+ const runtimeProxyScript = `<script>(function(){try{
4621
+ var TARGET_ORIGIN=${JSON.stringify(origin)};
4622
+ var TARGET_PAGE_URL=${JSON.stringify(targetUrl)};
4623
+ var EMPTY_JSON_DATA="data:application/json;charset=utf-8,%7B%7D";
4624
+ 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("//");}
4625
+ 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;}}
4626
+ function resolveUrl(s){try{return new URL(s,window.location.href);}catch(_){return null;}}
4627
+ 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;}
4628
+ function skipNestedProxyNetwork(s){var u=typeof s==="string"?resolveUrl(s):null;return u&&isNestedMalformedProxy(u);}
4629
+ function emptyJsonFetchResponse(){return Promise.resolve(new Response("{}",{status:200,headers:{"Content-Type":"application/json; charset=utf-8"}}));}
4630
+ if(window.fetch){
4631
+ var _fetch=window.fetch.bind(window);
4632
+ window.fetch=function(input,init){
4633
+ var afterUrl="";
4634
+ try{
4635
+ var rawUrl=typeof input==="string"?input:(input&&input.url?String(input.url):"");
4636
+ if(rawUrl&&skipNestedProxyNetwork(rawUrl))return emptyJsonFetchResponse();
4637
+ if(typeof input==="string"){
4638
+ input=toAbsoluteOriginUrl(input);
4639
+ }else if(input&&input.url){
4640
+ var next=toAbsoluteOriginUrl(input.url);
4641
+ if(next!==input.url){input=new Request(next,input);}
4642
+ }
4643
+ afterUrl=typeof input==="string"?input:(input&&input.url?String(input.url):"");
4644
+ if(afterUrl&&skipNestedProxyNetwork(afterUrl))return emptyJsonFetchResponse();
4645
+ }catch(_){}
4646
+ return _fetch(input,init).catch(function(err){
4647
+ try{
4648
+ var u=afterUrl?resolveUrl(afterUrl):null;
4649
+ var sameOrigin=!!(u&&u.origin===TARGET_ORIGIN);
4650
+ var likelyThirdPartyBg=!sameOrigin||!!(u&&/(^|\\/)apps?(\\/|$)|(^|\\/)a(\\/|$)/.test(u.pathname||""));
4651
+ if(window.__CONVERSION_EDITOR_ACTIVE__&&likelyThirdPartyBg){
4652
+ console.warn("[conversion-proxy] suppressed fetch failure",{url:afterUrl,error:err&&err.message?err.message:String(err)});
4653
+ return new Response("{}",{status:200,headers:{"Content-Type":"application/json; charset=utf-8"}});
4654
+ }
4655
+ }catch(_){}
4656
+ throw err;
4657
+ });
4658
+ };
4659
+ }
4660
+ 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);};}
4661
+ 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);};}
4662
+ 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"});};}
4663
+ }catch(_){}})();</script>`;
4423
4664
  if (html.includes("</head>")) {
4424
4665
  html = html.replace("</head>", `${runtimeProxyScript}
4425
4666
  </head>`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@accelerated-agency/visual-editor",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "private": false,
5
5
  "description": "Conversion visual editor as a reusable React package",
6
6
  "type": "module",