@accelerated-agency/visual-editor 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/vite.cjs CHANGED
@@ -86,17 +86,26 @@ function hasTrackingMarker(input, markers) {
86
86
  }
87
87
  return false;
88
88
  }
89
+ function patchKnownUnsafeEditorPatterns(scriptTag) {
90
+ let out = String(scriptTag || "");
91
+ out = out.replace(
92
+ /dragRegion\.addEventListener\((['"])pointerdown\1,\s*onPointerDown\);?/g,
93
+ "if (typeof dragRegion !== 'undefined' && dragRegion) dragRegion.addEventListener($1pointerdown$1, onPointerDown);"
94
+ );
95
+ return out;
96
+ }
89
97
  function stripTrackingScriptsFromScrapedHtml(html, markers) {
90
98
  let removedCount = 0;
91
99
  let out = html;
92
100
  out = out.replace(/<script\b[\s\S]*?<\/script>/gi, (tag) => {
101
+ var patchedTag = patchKnownUnsafeEditorPatterns(tag);
93
102
  const srcMatch = tag.match(/\bsrc\s*=\s*(["'])(.*?)\1/i);
94
103
  const src = srcMatch?.[2] || "";
95
104
  if (hasTrackingMarker(src, markers) || hasTrackingMarker(tag, markers)) {
96
105
  removedCount += 1;
97
106
  return "";
98
107
  }
99
- return tag;
108
+ return patchedTag;
100
109
  });
101
110
  out = out.replace(/<noscript\b[\s\S]*?<\/noscript>/gi, (tag) => {
102
111
  if (!hasTrackingMarker(tag, markers)) return tag;
@@ -860,7 +869,7 @@ select.pr-inp{cursor:pointer;background:#fff}
860
869
  <button class="tb-dk-btn" title="More options"><i class="bi bi-three-dots"></i></button>
861
870
  </div>
862
871
  <button class="tb-dk-btn" id="btn-undo" title="Undo (\u2318Z)"><i class="bi bi-arrow-counterclockwise"></i></button>
863
- <button class="tb-dk-btn" id="btn-redo" title="Redo (\u2318\u21E7Z)"><i class="bi bi-arrow-clockwise"></i></button>
872
+ <button class="tb-dk-btn" id="btn-redo" title="Redo (\u2318\u21E7Z)" style="display:none"><i class="bi bi-arrow-clockwise"></i></button>
864
873
  </div>
865
874
 
866
875
  <div id="iframe-loading-toolbar" class="tb-page-loading" aria-live="polite" aria-atomic="true">
@@ -931,8 +940,8 @@ select.pr-inp{cursor:pointer;background:#fff}
931
940
  <!-- Elements -->
932
941
  <div class="lp-sec lp-sec-no-border">
933
942
  <div class="lp-sec-hd">
934
- <span class="lp-sec-hd-left">Elements <i class="bi bi-info-circle lp-info-icon" title="Page elements"></i></span>
935
- <button class="lp-add-btn" title="Add element">+ Add</button>
943
+ <span class="lp-sec-hd-left">Elements <i style="display:none" class="bi bi-info-circle lp-info-icon" title="Page elements"></i></span>
944
+ <button class="lp-add-btn" id="btn-add-element" title="Add element">+ Add</button>
936
945
  </div>
937
946
 
938
947
  <!-- Search (hidden, kept for JS) -->
@@ -1002,14 +1011,16 @@ select.pr-inp{cursor:pointer;background:#fff}
1002
1011
 
1003
1012
  <!-- Right panel -->
1004
1013
  <div id="right-panel">
1005
- <!-- Left-tab controls moved here -->
1006
- <div class="section-components-tabs">
1007
- <div class="lp-tab" onclick="switchSectionComponentsTab('components')">Components</div>
1008
- <div class="lp-tab" onclick="switchSectionComponentsTab('sections')">Sections</div>
1009
- </div>
1010
- <div class="lp-body">
1011
- <div id="tab-components" class="tab-pane"></div>
1012
- <div id="tab-sections" class="tab-pane"></div>
1014
+ <div id="section-components-panel" style="display:none">
1015
+ <!-- Left-tab controls moved here -->
1016
+ <div class="section-components-tabs">
1017
+ <div class="lp-tab" onclick="switchSectionComponentsTab('components')">Components</div>
1018
+ <div class="lp-tab" onclick="switchSectionComponentsTab('sections')">Sections</div>
1019
+ </div>
1020
+ <div class="lp-body">
1021
+ <div id="tab-components" class="tab-pane"></div>
1022
+ <div id="tab-sections" class="tab-pane"></div>
1023
+ </div>
1013
1024
  </div>
1014
1025
  <!-- Element badge (hidden until selection) -->
1015
1026
  <div id="el-info" style="display:none">
@@ -1140,6 +1151,8 @@ select.pr-inp{cursor:pointer;background:#fff}
1140
1151
  </div>
1141
1152
 
1142
1153
  <!-- CDN scripts -->
1154
+ <script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
1155
+ <script src="https://cdn.jsdelivr.net/npm/jquery-ui-dist@1.13.3/jquery-ui.min.js"></script>
1143
1156
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
1144
1157
  <script>
1145
1158
  window.__veLoadFallback = function(el, candidates){
@@ -1167,10 +1180,6 @@ window.__veLoadFallback = function(el, candidates){
1167
1180
  onerror="window.__veLoadFallback(this,'https://cdn.jsdelivr.net/npm/vvvebjs@latest/libs/builder/inputs.js||https://unpkg.com/vvvebjs@latest/libs/builder/inputs.js')"
1168
1181
  ></script>
1169
1182
  <!-- components.js removed (frequently missing on npm CDN); safety stubs below prevent bootstrap/widgets loader errors -->
1170
- // <script
1171
- // src="https://cdn.jsdelivr.net/npm/vvvebjs@latest/libs/builder/components-widgets.js"
1172
- // onerror="window.__veLoadFallback(this,'https://cdn.jsdelivr.net/npm/vvvebjs@latest/libs/builder/components-widgets.js||https://unpkg.com/vvvebjs@latest/libs/builder/components-widgets.js')"
1173
- // ></script>
1174
1183
  <script>
1175
1184
  /* Safety stub: if components.js didn't define these, create empty arrays so
1176
1185
  components-bootstrap5.js doesn't throw ReferenceError on load. */
@@ -1199,7 +1208,7 @@ if (window.Vvveb && window.Vvveb.Components) {
1199
1208
  </script>
1200
1209
  <script
1201
1210
  src="https://cdn.jsdelivr.net/npm/vvvebjs@latest/libs/builder/components-bootstrap4.js"
1202
- onerror="window.__veLoadFallback(this,'https://cdn.jsdelivr.net/npm/vvvebjs@latest/libs/builder/components-bootstrap4.js||https://unpkg.com/vvvebjs@latest/libs/builder/components-bootstrap4.js')"
1211
+ onerror="window.__veLoadFallback(this,'https://cdn.jsdelivr.net/npm/vvvebjs@latest/libs/builder/components-bootstrap5.js||https://cdn.jsdelivr.net/npm/vvvebjs@latest/libs/builder/components-bootstrap4.js||https://unpkg.com/vvvebjs@latest/libs/builder/components-bootstrap5.js||https://unpkg.com/vvvebjs@latest/libs/builder/components-bootstrap4.js')"
1203
1212
  ></script>
1204
1213
  <script
1205
1214
  src="https://cdn.jsdelivr.net/npm/vvvebjs@latest/libs/builder/components-widgets.js"
@@ -1668,6 +1677,19 @@ function switchSectionComponentsTab(tab) {
1668
1677
  renderSidebar(inp ? inp.value : '');
1669
1678
  }
1670
1679
 
1680
+ function toggleSectionComponentsPanel(forceVisible) {
1681
+ var panel = document.getElementById('section-components-panel');
1682
+ if (!panel) return;
1683
+ var shouldShow =
1684
+ typeof forceVisible === 'boolean'
1685
+ ? forceVisible
1686
+ : panel.style.display === 'none';
1687
+ panel.style.display = shouldShow ? '' : 'none';
1688
+ if (shouldShow) {
1689
+ switchSectionComponentsTab(currentSectionComponentsTab || 'components');
1690
+ }
1691
+ }
1692
+
1671
1693
  // \u2500\u2500 Accordion toggle \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\u2500\u2500
1672
1694
  function toggleAcc(name) {
1673
1695
  var sec = document.getElementById('acc-' + name);
@@ -1877,6 +1899,24 @@ function syncDesignInput(change) {
1877
1899
  function removeStateChange(idx) {
1878
1900
  var change = stateChanges[idx];
1879
1901
  if (!change) return;
1902
+ if (change.isStructuralLive) {
1903
+ removeSessionStructuralRowByTimestamp(
1904
+ change.structuralVarId || activeVarId,
1905
+ change.vveTs,
1906
+ );
1907
+ stateChanges.splice(idx, 1);
1908
+ commitStateChangesForActiveVariation();
1909
+ renderStatesTab();
1910
+ if (currentMainTab === 'history') renderHistoryTab();
1911
+ try {
1912
+ delete varHtmlCache[activeVarId];
1913
+ } catch(_) {}
1914
+ appliedStructuralChangesetKeys = {};
1915
+ recomputeEditorDirty();
1916
+ scheduleDomTreeRefresh();
1917
+ softReloadEditorIframe();
1918
+ return;
1919
+ }
1880
1920
  revertChangeOnDom(change);
1881
1921
  syncDesignInput(change);
1882
1922
  stateChanges.splice(idx, 1);
@@ -2233,6 +2273,14 @@ function getLatestHistoryUndoTarget() {
2233
2273
  return list.length ? list[0] : null;
2234
2274
  }
2235
2275
 
2276
+ function getLatestLiveUndoTarget() {
2277
+ var list = getUnifiedHistoryItems();
2278
+ for (var i = 0; i < list.length; i++) {
2279
+ if (list[i] && list[i].source === 'live') return list[i];
2280
+ }
2281
+ return null;
2282
+ }
2283
+
2236
2284
  function changesetListHasStructural(arr) {
2237
2285
  if (!arr || !arr.length) return false;
2238
2286
  for (var i = 0; i < arr.length; i++) {
@@ -2331,8 +2379,15 @@ function removeHistoryChangeset(idx, evt) {
2331
2379
  function clearAllHistoryChangesets() {
2332
2380
  var v = getActiveVariationForHistory();
2333
2381
  if (!v) return;
2334
- if (!parseVariationChangesets(v).length) return;
2382
+ var hasSavedChangesets = parseVariationChangesets(v).length > 0;
2383
+ var hasSessionStructural =
2384
+ !!(activeVarId && sessionStructuralChainRowsByVarId[activeVarId] && sessionStructuralChainRowsByVarId[activeVarId].length);
2385
+ if (!hasSavedChangesets && !hasSessionStructural) return;
2386
+
2335
2387
  persistActiveVariationChangesets([]);
2388
+ if (activeVarId) {
2389
+ sessionStructuralChainRowsByVarId[activeVarId] = [];
2390
+ }
2336
2391
  appliedChangesetSnapshots = {};
2337
2392
  appliedStructuralChangesetKeys = {};
2338
2393
  try {
@@ -2345,6 +2400,10 @@ function clearAllHistoryChangesets() {
2345
2400
  }
2346
2401
 
2347
2402
  function clearAllUnifiedHistory() {
2403
+ if (activeVarId) {
2404
+ // Ensure structural unsaved rows are also removed by "Clear all changes".
2405
+ sessionStructuralChainRowsByVarId[activeVarId] = [];
2406
+ }
2348
2407
  clearAllStates();
2349
2408
  clearAllHistoryChangesets();
2350
2409
  if (currentMainTab === 'history') renderHistoryTab();
@@ -2356,15 +2415,32 @@ var VVE_LOCAL_STORAGE_PREFIX = 'vve:';
2356
2415
 
2357
2416
  function clearVisualEditorLocalStorage() {
2358
2417
  try {
2359
- for (var i = localStorage.length - 1; i >= 0; i--) {
2360
- var k = localStorage.key(i);
2361
- if (k && k.indexOf(VVE_LOCAL_STORAGE_PREFIX) === 0) {
2362
- localStorage.removeItem(k);
2418
+ var stores = [];
2419
+ try { stores.push(localStorage); } catch(_) {}
2420
+ try { stores.push(sessionStorage); } catch(_) {}
2421
+ for (var si = 0; si < stores.length; si++) {
2422
+ var store = stores[si];
2423
+ if (!store) continue;
2424
+ for (var i = store.length - 1; i >= 0; i--) {
2425
+ var k = store.key(i);
2426
+ if (k && k.indexOf(VVE_LOCAL_STORAGE_PREFIX) === 0) {
2427
+ store.removeItem(k);
2428
+ }
2363
2429
  }
2364
2430
  }
2365
2431
  } catch(_) {}
2366
2432
  }
2367
2433
 
2434
+ function clearPersistedActiveVariationForData(data) {
2435
+ try {
2436
+ var sk = activeVariationStorageKeyFromPayload(data);
2437
+ if (sk && sk !== VVE_LOCAL_STORAGE_PREFIX + 'activeVar::') {
2438
+ try { localStorage.removeItem(sk); } catch(_) {}
2439
+ try { sessionStorage.removeItem(sk); } catch(_) {}
2440
+ }
2441
+ } catch(_) {}
2442
+ }
2443
+
2368
2444
  function activeVariationStorageKeyFromPayload(data) {
2369
2445
  return (
2370
2446
  VVE_LOCAL_STORAGE_PREFIX +
@@ -2403,14 +2479,18 @@ function writePersistedActiveVariationId(varId) {
2403
2479
  * @param allowPrevMemory when true, keep in-session activeVarId if still valid (skip-reload path).
2404
2480
  */
2405
2481
  function pickActiveVariationIdForLoad(data, variationsArr, prevMemoryId, allowPrevMemory) {
2406
- var baseline = variationsArr.find(function(v) { return v.baseline; });
2407
- var fallback = (baseline || variationsArr[0] || {})._id || null;
2482
+ var firstNonBaseline = variationsArr.find(function(v) { return !v.baseline; });
2483
+ var fallback = (firstNonBaseline || variationsArr[0] || {})._id || null;
2408
2484
  if (!variationsArr.length) return null;
2409
2485
  if (allowPrevMemory && prevMemoryId && variationsArr.some(function(v) { return v._id === prevMemoryId; })) {
2410
2486
  return prevMemoryId;
2411
2487
  }
2412
2488
  var stored = readPersistedActiveVariationId(data);
2413
2489
  if (stored && variationsArr.some(function(v) { return v._id === stored; })) {
2490
+ var storedVar = variationsArr.find(function(v) { return v._id === stored; });
2491
+ if (storedVar && storedVar.baseline && firstNonBaseline && firstNonBaseline._id) {
2492
+ return firstNonBaseline._id;
2493
+ }
2414
2494
  return stored;
2415
2495
  }
2416
2496
  return fallback;
@@ -2432,12 +2512,24 @@ function handleLoadExperiment(data) {
2432
2512
  var extraTrackingMarkers = Array.isArray(data && data.trackingMarkers)
2433
2513
  ? data.trackingMarkers.filter(function(m){return typeof m === 'string' && m.trim().length > 0;})
2434
2514
  : [];
2515
+ var conversionProxyBaseUrl = (typeof (data && data.conversionProxyBaseUrl) === 'string'
2516
+ ? data.conversionProxyBaseUrl
2517
+ : '').trim().replace(/\\/+$/, '');
2518
+ var proxyPath = '/api/conversion-proxy';
2519
+ // Keep iframe navigation same-origin for DOM access (selection/editing).
2520
+ // If conversionProxyBaseUrl is provided, middleware delegates upstream fetches
2521
+ // server-side while browser URLs stay local.
2522
+ var proxyRoot = proxyPath;
2435
2523
  var trackingMarkersParam = extraTrackingMarkers.length
2436
2524
  ? '&trackingMarkers=' + encodeURIComponent(JSON.stringify(extraTrackingMarkers))
2437
2525
  : '';
2438
- var proxyUrl = '/api/conversion-proxy?password=' + encodeURIComponent(data.editorPassword || '') +
2526
+ var conversionProxyBaseUrlParam = conversionProxyBaseUrl
2527
+ ? '&conversionProxyBaseUrl=' + encodeURIComponent(conversionProxyBaseUrl)
2528
+ : '';
2529
+ var proxyUrl = proxyRoot + '?password=' + encodeURIComponent(data.editorPassword || '') +
2439
2530
  '&url=' + encodeURIComponent(pageUrl) +
2440
2531
  '&strictObserverFreeze=' + encodeURIComponent(data && data.strictObserverFreeze ? '1' : '0') +
2532
+ conversionProxyBaseUrlParam +
2441
2533
  trackingMarkersParam;
2442
2534
 
2443
2535
  // Parent often re-posts load-experiment when React re-renders (new object identity) or
@@ -2622,7 +2714,13 @@ function granularAnySelectorMatches(doc, cs) {
2622
2714
 
2623
2715
  /** Bust bfcache / same-URL no-op reloads so the iframe actually re-parses (loading \u2192 interactive). */
2624
2716
  function appendIframeReloadBust(url) {
2625
- return url;
2717
+ try {
2718
+ var u = new URL(String(url || ''), window.location.href);
2719
+ u.searchParams.set('_vve_reload', String(Date.now()));
2720
+ return u.toString();
2721
+ } catch(_) {
2722
+ return url;
2723
+ }
2626
2724
  }
2627
2725
 
2628
2726
  // True when the iframe contentDocument belongs to the current iframe.src navigation.
@@ -3000,6 +3098,44 @@ function appendSessionStructuralChainRow(varId, row) {
3000
3098
  sessionStructuralChainRowsByVarId[varId].push(row);
3001
3099
  }
3002
3100
 
3101
+ function markStructuralRowApplied(row) {
3102
+ try {
3103
+ var k = structuralChangesetDedupKey(row);
3104
+ if (k) appliedStructuralChangesetKeys[k] = true;
3105
+ } catch(_) {}
3106
+ }
3107
+
3108
+ function logStructuralStateChange(row, label, value, targetEl) {
3109
+ if (!row || !row.selector) return;
3110
+ stateChanges.push({
3111
+ selector: row.selector,
3112
+ inputId: 'vve-struct',
3113
+ label: label || 'Structure change',
3114
+ cssProp: null,
3115
+ value: value != null ? String(value) : String(row.type || ''),
3116
+ targetEl: targetEl || null,
3117
+ originalValue: '',
3118
+ vveTs: row.vveTs || nextHistoryTimestamp(),
3119
+ isStructuralLive: true,
3120
+ structuralVarId: activeVarId || null,
3121
+ structuralType: row.type || '',
3122
+ });
3123
+ if (currentMainTab === 'history') renderHistoryTab();
3124
+ commitStateChangesForActiveVariation();
3125
+ }
3126
+
3127
+ function removeSessionStructuralRowByTimestamp(varId, ts) {
3128
+ if (!varId || !ts) return;
3129
+ var arr = sessionStructuralChainRowsByVarId[varId];
3130
+ if (!arr || !arr.length) return;
3131
+ for (var i = arr.length - 1; i >= 0; i--) {
3132
+ if (arr[i] && arr[i].vveTs === ts) {
3133
+ arr.splice(i, 1);
3134
+ return;
3135
+ }
3136
+ }
3137
+ }
3138
+
3003
3139
  /** One States-tab row -> Conversion.io chain-set shape (matches applyChangesetEntry). */
3004
3140
  function stateChangeToChainSet(c) {
3005
3141
  if (!c || !c.selector) return null;
@@ -3694,12 +3830,15 @@ function duplicateSelectedEl() {
3694
3830
  clone.setAttribute('data-vve-instance', generateVveInstanceId());
3695
3831
  } catch(_) {}
3696
3832
  if (activeVarId) {
3697
- appendSessionStructuralChainRow(activeVarId, {
3833
+ var dupRow = {
3698
3834
  selector: anchorSel,
3699
3835
  type: 'insert',
3700
3836
  action: 'after',
3701
3837
  html: clone.outerHTML,
3702
- });
3838
+ };
3839
+ appendSessionStructuralChainRow(activeVarId, dupRow);
3840
+ markStructuralRowApplied(dupRow);
3841
+ logStructuralStateChange(dupRow, 'Duplicated via toolbar', 'Duplicate inserted', selectedEl);
3703
3842
  }
3704
3843
  selectedEl.parentNode.insertBefore(clone, selectedEl.nextSibling);
3705
3844
  saveCurrentVariationHtml();
@@ -3715,23 +3854,27 @@ function toggleHideSelectedEl() {
3715
3854
  selectedEl.style.visibility = '';
3716
3855
  selectedEl.removeAttribute('data-vve-hidden');
3717
3856
  if (activeVarId) {
3718
- appendSessionStructuralChainRow(activeVarId, {
3857
+ var showRow = {
3719
3858
  selector: hidSel,
3720
3859
  type: 'style',
3721
3860
  property: 'visibility',
3722
3861
  value: '',
3723
- });
3862
+ };
3863
+ appendSessionStructuralChainRow(activeVarId, showRow);
3864
+ logStructuralStateChange(showRow, 'Shown via toolbar', 'Visibility restored', selectedEl);
3724
3865
  }
3725
3866
  } else {
3726
3867
  selectedEl.style.visibility = 'hidden';
3727
3868
  selectedEl.setAttribute('data-vve-hidden', '1');
3728
3869
  if (activeVarId) {
3729
- appendSessionStructuralChainRow(activeVarId, {
3870
+ var hideRow = {
3730
3871
  selector: hidSel,
3731
3872
  type: 'style',
3732
3873
  property: 'visibility',
3733
3874
  value: 'hidden',
3734
- });
3875
+ };
3876
+ appendSessionStructuralChainRow(activeVarId, hideRow);
3877
+ logStructuralStateChange(hideRow, 'Hidden via toolbar', 'Visibility set to hidden', selectedEl);
3735
3878
  }
3736
3879
  }
3737
3880
  saveCurrentVariationHtml();
@@ -3742,7 +3885,11 @@ function deleteSelectedEl() {
3742
3885
  if (!selectedEl || !selectedEl.parentNode) return;
3743
3886
  var delSel = buildSelector(selectedEl);
3744
3887
  selectedEl.remove();
3745
- if (activeVarId) appendSessionStructuralChainRow(activeVarId, { selector: delSel, type: 'remove' });
3888
+ if (activeVarId) {
3889
+ var delRow = { selector: delSel, type: 'remove' };
3890
+ appendSessionStructuralChainRow(activeVarId, delRow);
3891
+ logStructuralStateChange(delRow, 'Deleted via toolbar', 'Element removed', null);
3892
+ }
3746
3893
  saveCurrentVariationHtml();
3747
3894
  recomputeEditorDirty();
3748
3895
  deselectElement();
@@ -4643,19 +4790,25 @@ function recordReorderAfterDrag(movedEl) {
4643
4790
  var prev = movedEl.previousElementSibling;
4644
4791
  var next = movedEl.nextElementSibling;
4645
4792
  if (prev) {
4646
- appendSessionStructuralChainRow(activeVarId, {
4793
+ var reorderAfterRow = {
4647
4794
  selector: buildSelector(movedEl),
4648
4795
  type: 'reorder',
4649
4796
  targetSelector: buildSelector(prev),
4650
4797
  action: 'after',
4651
- });
4798
+ };
4799
+ appendSessionStructuralChainRow(activeVarId, reorderAfterRow);
4800
+ markStructuralRowApplied(reorderAfterRow);
4801
+ logStructuralStateChange(reorderAfterRow, 'Reordered via drag', 'Moved after sibling', movedEl);
4652
4802
  } else if (next) {
4653
- appendSessionStructuralChainRow(activeVarId, {
4803
+ var reorderBeforeRow = {
4654
4804
  selector: buildSelector(movedEl),
4655
4805
  type: 'reorder',
4656
4806
  targetSelector: buildSelector(next),
4657
4807
  action: 'before',
4658
- });
4808
+ };
4809
+ appendSessionStructuralChainRow(activeVarId, reorderBeforeRow);
4810
+ markStructuralRowApplied(reorderBeforeRow);
4811
+ logStructuralStateChange(reorderBeforeRow, 'Reordered via drag', 'Moved before sibling', movedEl);
4659
4812
  }
4660
4813
  }
4661
4814
 
@@ -4936,12 +5089,15 @@ function insertHtml(html) {
4936
5089
  }
4937
5090
  if (firstEl) selectElement(firstEl);
4938
5091
  if (activeVarId) {
4939
- appendSessionStructuralChainRow(activeVarId, {
5092
+ var insertRow = {
4940
5093
  selector: anchorSel,
4941
5094
  type: 'insert',
4942
5095
  action: 'after',
4943
5096
  html: htmlStr,
4944
- });
5097
+ };
5098
+ appendSessionStructuralChainRow(activeVarId, insertRow);
5099
+ markStructuralRowApplied(insertRow);
5100
+ logStructuralStateChange(insertRow, 'Added component/section', 'Inserted new element', firstEl || selectedEl);
4945
5101
  }
4946
5102
  saveCurrentVariationHtml();
4947
5103
  recomputeEditorDirty();
@@ -5031,6 +5187,14 @@ document.getElementById('comp-search').addEventListener('input', function() {
5031
5187
  renderSidebar(this.value);
5032
5188
  }
5033
5189
  });
5190
+ var btnAddElement = document.getElementById('btn-add-element');
5191
+ if (btnAddElement) {
5192
+ btnAddElement.addEventListener('click', function(e) {
5193
+ e.preventDefault();
5194
+ e.stopPropagation();
5195
+ toggleSectionComponentsPanel();
5196
+ });
5197
+ }
5034
5198
 
5035
5199
  // \u2500\u2500 Save / Close \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\u2500\u2500\u2500\u2500\u2500\u2500
5036
5200
  document.getElementById('btn-save').addEventListener('click', handleSave);
@@ -5071,11 +5235,21 @@ function handleSave() {
5071
5235
  }
5072
5236
 
5073
5237
  function handleClose() {
5238
+ clearPersistedActiveVariationForData(experimentData);
5074
5239
  clearVisualEditorLocalStorage();
5075
5240
  // Unsaved-changes UX lives in the parent (PlatformVisualEditorV2); avoid double confirm here.
5076
5241
  send('close-editor', {});
5077
5242
  }
5078
5243
 
5244
+ // Defensive cleanup: if parent closes/unmounts the editor shell without
5245
+ // invoking handleClose(), clear persisted VVE keys on unload as well.
5246
+ window.addEventListener('pagehide', function() {
5247
+ clearVisualEditorLocalStorage();
5248
+ });
5249
+ window.addEventListener('beforeunload', function() {
5250
+ clearVisualEditorLocalStorage();
5251
+ });
5252
+
5079
5253
  // \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
5080
5254
  function isNativeEditableTarget(target) {
5081
5255
  if (!target || target.nodeType !== 1) return false;
@@ -5097,10 +5271,7 @@ document.addEventListener('keydown', function(e) {
5097
5271
  e.preventDefault();
5098
5272
  runEditorUndo();
5099
5273
  }
5100
- if (meta && e.shiftKey && k === 'z') {
5101
- e.preventDefault();
5102
- runEditorRedo();
5103
- }
5274
+ // Redo is intentionally hidden/disabled in this editor flow.
5104
5275
  if (meta && e.key === 's') { e.preventDefault(); handleSave(); }
5105
5276
  if (e.key === 'Escape') {
5106
5277
  var openTips = document.querySelectorAll('.ve-pl-tip.is-tip-open');
@@ -5120,11 +5291,17 @@ document.addEventListener('keydown', function(e) {
5120
5291
  }
5121
5292
  });
5122
5293
  function runEditorUndo() {
5123
- var target = getLatestHistoryUndoTarget();
5124
- if (target) {
5125
- removeHistoryItem(target.source, target.idx);
5294
+ // Undo only unsaved in-session edits; do not remove persisted history rows.
5295
+ if (!isDirty) return;
5296
+
5297
+ // 1) Prefer live unsaved change stack (stateChanges shown in History tab).
5298
+ var liveTarget = getLatestLiveUndoTarget();
5299
+ if (liveTarget) {
5300
+ removeHistoryItem('live', liveTarget.idx);
5126
5301
  return;
5127
5302
  }
5303
+
5304
+ // 2) Fallback to Vvveb internal undo stack (e.g. structural drag/drop ops).
5128
5305
  if (!(typeof Vvveb !== 'undefined' && Vvveb.Undo)) return;
5129
5306
  Vvveb.Undo.undo();
5130
5307
  saveCurrentVariationHtml();
@@ -5147,11 +5324,10 @@ document.getElementById('btn-undo').addEventListener('click', function(e) {
5147
5324
  e.stopPropagation();
5148
5325
  runEditorUndo();
5149
5326
  });
5150
- document.getElementById('btn-redo').addEventListener('click', function(e) {
5151
- e.preventDefault();
5152
- e.stopPropagation();
5153
- runEditorRedo();
5154
- });
5327
+ var btnRedo = document.getElementById('btn-redo');
5328
+ if (btnRedo) {
5329
+ btnRedo.style.display = 'none';
5330
+ }
5155
5331
 
5156
5332
  function layoutLoadingTooltip(host) {
5157
5333
  var tip = host.querySelector('.ve-pl-tooltip');
@@ -5523,6 +5699,8 @@ function createVisualEditorMiddleware(options) {
5523
5699
  const url = new URL(req.url || "", "http://localhost");
5524
5700
  const targetUrl = url.searchParams.get("url");
5525
5701
  const password = url.searchParams.get("password") || "";
5702
+ const conversionProxyBaseUrlForRequest = (url.searchParams.get("conversionProxyBaseUrl") || "").trim().replace(/\/+$/, "");
5703
+ const proxyRootForRequest = "/api/conversion-proxy";
5526
5704
  const extraTrackingMarkersForRequest = parseTrackingMarkersParam(
5527
5705
  url.searchParams.get("trackingMarkers")
5528
5706
  );
@@ -5575,7 +5753,34 @@ function createVisualEditorMiddleware(options) {
5575
5753
  return false;
5576
5754
  }
5577
5755
  };
5756
+ const workerRawFetch = (input, init = {}) => {
5757
+ const workerUrl = new URL("/api/conversion-proxy", conversionProxyBaseUrlForRequest);
5758
+ workerUrl.searchParams.set("url", input);
5759
+ workerUrl.searchParams.set("raw", "1");
5760
+ const method2 = (init?.method || "GET").toUpperCase();
5761
+ const nextHeaders = {};
5762
+ const h = init?.headers;
5763
+ if (h && typeof h.forEach === "function") {
5764
+ h.forEach((v, k) => {
5765
+ if (!v) return;
5766
+ nextHeaders[k] = String(v);
5767
+ });
5768
+ } else {
5769
+ Object.entries(h || {}).forEach(([k, v]) => {
5770
+ if (!v) return;
5771
+ nextHeaders[k] = Array.isArray(v) ? v.join(",") : String(v);
5772
+ });
5773
+ }
5774
+ return fetch(workerUrl.toString(), {
5775
+ ...init,
5776
+ method: method2,
5777
+ headers: nextHeaders
5778
+ });
5779
+ };
5578
5780
  const upstreamFetch = async (input, init = {}) => {
5781
+ if (conversionProxyBaseUrlForRequest) {
5782
+ return workerRawFetch(input, init);
5783
+ }
5579
5784
  const primary = await scraperFetch(input, init);
5580
5785
  if (!await shouldFallbackFromScraper(primary)) {
5581
5786
  return primary;
@@ -5606,7 +5811,7 @@ function createVisualEditorMiddleware(options) {
5606
5811
  )}`,
5607
5812
  redirect: "manual"
5608
5813
  });
5609
- const setCookies = passResp.headers.getSetCookie?.() || [];
5814
+ const setCookies = passResp.headers.getSetCookie?.() || (passResp.headers.get("set-cookie") ? [String(passResp.headers.get("set-cookie"))] : []);
5610
5815
  cookieHeader = setCookies.map((c) => c.split(";")[0]).join("; ");
5611
5816
  }
5612
5817
  const fetchHeaders = { ...headers };
@@ -5704,7 +5909,7 @@ function createVisualEditorMiddleware(options) {
5704
5909
  html = stripTrackingScriptsFromScrapedHtml(html, trackingMarkersForRequest);
5705
5910
  html = html.replace(/(href|src|action)="\/(?!\/)/g, `$1="${origin}/`);
5706
5911
  const escapedOrigin = origin.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5707
- const proxyBase = `/api/conversion-proxy?password=${encodeURIComponent(password)}&url=`;
5912
+ const proxyBase = `${proxyRootForRequest}?password=${encodeURIComponent(password)}&url=`;
5708
5913
  html = html.replace(
5709
5914
  new RegExp(`(href|action)="${escapedOrigin}(/[^"]*)"`, "g"),
5710
5915
  (match, attr, urlPath) => {
@@ -5717,7 +5922,7 @@ function createVisualEditorMiddleware(options) {
5717
5922
  );
5718
5923
  html = html.replace(
5719
5924
  /data-link="\/(?!\/)([^"]*)"/g,
5720
- (_match, pathPart) => `data-link="/api/conversion-proxy?password=${encodeURIComponent(password)}&url=${encodeURIComponent(
5925
+ (_match, pathPart) => `data-link="${proxyRootForRequest}?password=${encodeURIComponent(password)}&url=${encodeURIComponent(
5721
5926
  `${origin}/${pathPart}`
5722
5927
  )}"`
5723
5928
  );
@@ -5741,6 +5946,7 @@ ${iframeAlwaysShowCssGuardScript}
5741
5946
  var TARGET_ORIGIN=${JSON.stringify(origin)};
5742
5947
  var TARGET_PAGE_URL=${JSON.stringify(targetUrl)};
5743
5948
  var PROXY_PASSWORD=${JSON.stringify(password)};
5949
+ var PROXY_BASE_URL="";
5744
5950
  var STRICT_OBSERVER_FREEZE=${JSON.stringify(strictObserverFreezeForRequest)};
5745
5951
  var PARENT_URL_CHANNEL="vvveb-proxy-url";
5746
5952
  window.__CONVERSION_EDITOR_ACTIVE__=true;
@@ -5791,7 +5997,7 @@ function shouldBlockEditorTracking(rawUrl){
5791
5997
  var looksLikeHeartbeatPayload=hasViewportPing&&hasSessionIds;
5792
5998
  var isCrossOriginCollector=host!==TARGET_HOSTNAME&&hasMarker(host,TRACKING_HOST_MARKERS)&&hasMarker(path,TRACKING_PATH_MARKERS);
5793
5999
  var isSnowplowStylePath=path==="/i"||path.indexOf("/com.snowplowanalytics.snowplow/")!==-1||path.indexOf("/tp2")!==-1;
5794
- var isTaboolaUnip=host==="trc-events.taboola.com"&&//log/d+/unip(?:/|$)/.test(path);
6000
+ var isTaboolaUnip=host==="trc-events.taboola.com"&&/\\/log\\/\\d+\\/unip(?:\\/|$)/.test(path);
5795
6001
  var isCollectApiPath=path==="/api/collect"||path.indexOf("/api/collect/")===0;
5796
6002
  var isNbCollectorPath=path==="/nb-collector"||path.indexOf("/nb-collector/")===0;
5797
6003
  return isCrossOriginCollector||(looksLikeHeartbeatPayload&&isSnowplowStylePath)||isCollectApiPath||isTaboolaUnip||isNbCollectorPath;
@@ -5861,7 +6067,7 @@ try{window.addEventListener("popstate",notifyEditorUrlChanged,true);}catch(_){}
5861
6067
  try{window.addEventListener("hashchange",notifyEditorUrlChanged,true);}catch(_){}
5862
6068
  function isSkippable(raw){if(!raw||typeof raw!=="string")return true;return raw.startsWith("data:")||raw.startsWith("blob:")||raw.startsWith("javascript:")||raw.startsWith("#");}
5863
6069
  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;}}
5864
- 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;}}
6070
+ 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;var root="/api/conversion-proxy";return root+"?password="+encodeURIComponent(PROXY_PASSWORD||"")+"&url="+encodeURIComponent(parsed.toString());}catch(_){return null;}}
5865
6071
  var nativeAssign=window.location.assign?window.location.assign.bind(window.location):null;
5866
6072
  var nativeReplace=window.location.replace?window.location.replace.bind(window.location):null;
5867
6073
  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;}}
@@ -5930,8 +6136,22 @@ if(window.fetch){
5930
6136
  try{
5931
6137
  var u=afterUrl?resolveUrl(afterUrl):null;
5932
6138
  var sameOrigin=!!(u&&u.origin===TARGET_ORIGIN);
5933
- var likelyThirdPartyBg=!sameOrigin||!!(u&&/(^|\\/)apps?(\\/|$)|(^|\\/)a(\\/|$)/.test(u.pathname||""));
5934
- if(window.__CONVERSION_EDITOR_ACTIVE__&&likelyThirdPartyBg){
6139
+ var reqMethod=(init&&init.method?String(init.method):(input&&input.method?String(input.method):"GET")).toUpperCase();
6140
+ var isSafeMethod=reqMethod==="GET"||reqMethod==="HEAD";
6141
+ var path=(u&&u.pathname?u.pathname:"").toLowerCase();
6142
+ var qs=(u&&u.search?u.search:"").toLowerCase();
6143
+ var likelyBackgroundEndpoint=!!(u&&(
6144
+ /(^|\\/)apps?(\\/|$)|(^|\\/)a(\\/|$)/.test(path)||
6145
+ path==="/cart"||
6146
+ path==="/cart.js"||
6147
+ path.indexOf("/cart/")===0||
6148
+ path.indexOf("/recommendations/")===0||
6149
+ path.indexOf("/search/suggest")===0||
6150
+ qs.indexOf("sections=")!==-1||
6151
+ qs.indexOf("section_id=")!==-1
6152
+ ));
6153
+ var likelyThirdPartyBg=!sameOrigin||likelyBackgroundEndpoint;
6154
+ if(window.__CONVERSION_EDITOR_ACTIVE__&&isSafeMethod&&likelyThirdPartyBg){
5935
6155
  console.warn("[conversion-proxy] suppressed fetch failure",{url:afterUrl,error:err&&err.message?err.message:String(err)});
5936
6156
  return new Response("{}",{status:200,headers:{"Content-Type":"application/json; charset=utf-8"}});
5937
6157
  }