@accelerated-agency/visual-editor 0.2.5 → 0.2.7

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
@@ -627,7 +627,7 @@ select.pr-inp{cursor:pointer;background:#fff}
627
627
  </div>
628
628
  <!-- btn-close: hidden visually, kept for JS event listener -->
629
629
  <button id="btn-close" style="display:none" title="Close editor"></button>
630
- <button class="tb-sim-btn" id="btn-simulate"><i class="bi bi-lightning-charge-fill"></i> Simulate</button>
630
+ <button class="tb-sim-btn" id="btn-simulate" onclick="simulateExperiment()"><i class="bi bi-lightning-charge-fill"></i> Simulate</button>
631
631
  <button class="tb-fin-btn" id="btn-save">Finalize</button>
632
632
  </div>
633
633
 
@@ -914,6 +914,52 @@ function send(type, payload) {
914
914
  window.parent.postMessage({ channel: CHANNEL, type: type, payload: payload || {} }, '*');
915
915
  }
916
916
 
917
+ function generatePreviewUrlString(args) {
918
+ var baseUrl = (args && args.url) || '';
919
+ var test = (args && args.test) || {};
920
+ var variation = (args && args.variation) || {};
921
+ if (!baseUrl) return '';
922
+ var testId = test.iid || test.experimentId || test._id || '';
923
+ var variationId = variation.iid || variation._id || '';
924
+ var cId = String(testId || '') + '_' + String(variationId || '');
925
+ var hasQueryParams = String(baseUrl).indexOf('?') >= 0;
926
+ return (
927
+ baseUrl +
928
+ (hasQueryParams ? '&' : '?') +
929
+ 'codebase_debug=true&cId=' +
930
+ encodeURIComponent(cId)
931
+ );
932
+ }
933
+
934
+ function simulateExperiment() {
935
+ var test = experimentData || {};
936
+ var activeVariation = null;
937
+ if (Array.isArray(variations) && variations.length) {
938
+ activeVariation =
939
+ variations.find(function(v) { return v && v._id === activeVarId; }) ||
940
+ variations[0];
941
+ }
942
+ var targetUrl =
943
+ (Array.isArray(test.urltargeting) && test.urltargeting[0]) ||
944
+ test.pageUrl ||
945
+ (test.metadata_1 && test.metadata_1.editor_url) ||
946
+ '';
947
+ var url = generatePreviewUrlString({
948
+ url: targetUrl,
949
+ test: test,
950
+ variation: activeVariation || {},
951
+ });
952
+ if (!url) {
953
+ console.warn('[V2] simulateExperiment: missing target URL');
954
+ return;
955
+ }
956
+ try {
957
+ window.open(url, '_blank');
958
+ } catch(err) {
959
+ console.warn('[V2] simulateExperiment:', err);
960
+ }
961
+ }
962
+
917
963
  window.addEventListener('message', function(e) {
918
964
  if (!e.data || e.data.channel !== CHANNEL) return;
919
965
  switch (e.data.type) {
@@ -965,6 +1011,13 @@ var changeObserver = null;
965
1011
  var changeObserverDoc = null;
966
1012
  /** Incremented while applying changesets / selection chrome so MutationObserver does not mark dirty */
967
1013
  var suppressIframeMutationDirty = 0;
1014
+ /** Throttled reconcile timer to keep DOM aligned with saved changesets. */
1015
+ var consistencyReconcileTimer = null;
1016
+ /** Background watchdog for late client-side re-renders that wipe applied changesets. */
1017
+ var consistencyWatchTimer = null;
1018
+ var consistencyWatchDoc = null;
1019
+ var consistencyWatchTicks = 0;
1020
+ var CONSISTENCY_WATCH_MAX_TICKS = 160;
968
1021
  /** { doc, onRS } \u2014 iframe document readystate until complete */
969
1022
  var iframeDocLoadingListeners = null;
970
1023
  // Each entry: {selector, label, cssProp, value, targetEl}
@@ -1948,9 +2001,71 @@ function clearPendingGranularChangesets() {
1948
2001
  }
1949
2002
  }
1950
2003
 
2004
+ function stopConsistencyWatchdog() {
2005
+ if (consistencyWatchTimer) {
2006
+ clearInterval(consistencyWatchTimer);
2007
+ consistencyWatchTimer = null;
2008
+ }
2009
+ consistencyWatchDoc = null;
2010
+ consistencyWatchTicks = 0;
2011
+ }
2012
+
2013
+ function runConsistencyReconcile() {
2014
+ if (!activeVarId) return;
2015
+ var iframe = document.getElementById('iframeId');
2016
+ var doc = iframe && iframe.contentDocument;
2017
+ if (!doc || !doc.body) return;
2018
+ var variation = variations.find(function(v) { return v._id === activeVarId; });
2019
+ var cs = parseVariationChangesets(variation);
2020
+ if (!cs.length || changesetsHaveBodySnapshot(cs)) return;
2021
+ var granular = filterGranularChangesetEntries(cs);
2022
+ var unresolved = countUnresolvedGranularSelectors(doc, granular);
2023
+ if (unresolved > 0 || changesetListHasStructural(cs)) {
2024
+ reapplyActiveVariationGranular(doc);
2025
+ registerPendingGranularChangesets(cs, doc);
2026
+ }
2027
+ }
2028
+
2029
+ function scheduleConsistencyReconcile() {
2030
+ if (consistencyReconcileTimer) return;
2031
+ consistencyReconcileTimer = setTimeout(function() {
2032
+ consistencyReconcileTimer = null;
2033
+ runConsistencyReconcile();
2034
+ }, 80);
2035
+ }
2036
+
2037
+ function startConsistencyWatchdog(doc) {
2038
+ if (!doc || !doc.body) return;
2039
+ if (consistencyWatchDoc === doc && consistencyWatchTimer) return;
2040
+ stopConsistencyWatchdog();
2041
+ consistencyWatchDoc = doc;
2042
+ consistencyWatchTicks = 0;
2043
+ consistencyWatchTimer = setInterval(function() {
2044
+ if (consistencyWatchDoc !== doc) {
2045
+ stopConsistencyWatchdog();
2046
+ return;
2047
+ }
2048
+ var iframe = document.getElementById('iframeId');
2049
+ if (!iframe || iframe.contentDocument !== doc || !doc.body) {
2050
+ stopConsistencyWatchdog();
2051
+ return;
2052
+ }
2053
+ consistencyWatchTicks += 1;
2054
+ scheduleConsistencyReconcile();
2055
+ if (consistencyWatchTicks >= CONSISTENCY_WATCH_MAX_TICKS) {
2056
+ stopConsistencyWatchdog();
2057
+ }
2058
+ }, 400);
2059
+ }
2060
+
1951
2061
  function resetIframeBindings() {
1952
2062
  detachIframeLoadingListeners();
1953
2063
  stopIframeContentApplyWatcher();
2064
+ stopConsistencyWatchdog();
2065
+ if (consistencyReconcileTimer) {
2066
+ clearTimeout(consistencyReconcileTimer);
2067
+ consistencyReconcileTimer = null;
2068
+ }
1954
2069
  appliedChangesetSnapshots = {};
1955
2070
  appliedStructuralChangesetKeys = {};
1956
2071
  clickAttachDoc = null;
@@ -3564,10 +3679,31 @@ function attachChangeObserver() {
3564
3679
  changeObserver = null;
3565
3680
  changeObserverDoc = null;
3566
3681
  }
3567
- changeObserver = new MutationObserver(function() {
3682
+ changeObserver = new MutationObserver(function(mutations) {
3568
3683
  // Dirty state is derived from changesets baseline + stateChanges (not raw DOM mutations).
3684
+ var bodyReplaced = false;
3685
+ for (var mi = 0; mi < mutations.length; mi++) {
3686
+ var m = mutations[mi];
3687
+ if (
3688
+ m &&
3689
+ m.type === 'childList' &&
3690
+ m.target === doc.body &&
3691
+ m.addedNodes &&
3692
+ m.removedNodes &&
3693
+ m.addedNodes.length > 0 &&
3694
+ m.removedNodes.length > 0
3695
+ ) {
3696
+ bodyReplaced = true;
3697
+ break;
3698
+ }
3699
+ }
3700
+ if (bodyReplaced) {
3701
+ // Page JS replaced body children; allow structural rows (insert/reorder) to apply again.
3702
+ appliedStructuralChangesetKeys = {};
3703
+ }
3569
3704
  scheduleDomTreeRefresh();
3570
3705
  scheduleGranularChangesetReapply();
3706
+ scheduleConsistencyReconcile();
3571
3707
  });
3572
3708
  changeObserver.observe(doc.body, {
3573
3709
  childList: true, subtree: true, attributes: true, characterData: true
@@ -3597,6 +3733,8 @@ function syncIframeInteractions(reason) {
3597
3733
  attachClickHandler();
3598
3734
  attachDragReposition();
3599
3735
  attachChangeObserver();
3736
+ startConsistencyWatchdog(doc);
3737
+ scheduleConsistencyReconcile();
3600
3738
  bindSelectionToolbarScroll();
3601
3739
  var inp = document.getElementById('comp-search');
3602
3740
  renderDomTree(inp ? inp.value : '');
package/dist/vite.js CHANGED
@@ -619,7 +619,7 @@ select.pr-inp{cursor:pointer;background:#fff}
619
619
  </div>
620
620
  <!-- btn-close: hidden visually, kept for JS event listener -->
621
621
  <button id="btn-close" style="display:none" title="Close editor"></button>
622
- <button class="tb-sim-btn" id="btn-simulate"><i class="bi bi-lightning-charge-fill"></i> Simulate</button>
622
+ <button class="tb-sim-btn" id="btn-simulate" onclick="simulateExperiment()"><i class="bi bi-lightning-charge-fill"></i> Simulate</button>
623
623
  <button class="tb-fin-btn" id="btn-save">Finalize</button>
624
624
  </div>
625
625
 
@@ -906,6 +906,52 @@ function send(type, payload) {
906
906
  window.parent.postMessage({ channel: CHANNEL, type: type, payload: payload || {} }, '*');
907
907
  }
908
908
 
909
+ function generatePreviewUrlString(args) {
910
+ var baseUrl = (args && args.url) || '';
911
+ var test = (args && args.test) || {};
912
+ var variation = (args && args.variation) || {};
913
+ if (!baseUrl) return '';
914
+ var testId = test.iid || test.experimentId || test._id || '';
915
+ var variationId = variation.iid || variation._id || '';
916
+ var cId = String(testId || '') + '_' + String(variationId || '');
917
+ var hasQueryParams = String(baseUrl).indexOf('?') >= 0;
918
+ return (
919
+ baseUrl +
920
+ (hasQueryParams ? '&' : '?') +
921
+ 'codebase_debug=true&cId=' +
922
+ encodeURIComponent(cId)
923
+ );
924
+ }
925
+
926
+ function simulateExperiment() {
927
+ var test = experimentData || {};
928
+ var activeVariation = null;
929
+ if (Array.isArray(variations) && variations.length) {
930
+ activeVariation =
931
+ variations.find(function(v) { return v && v._id === activeVarId; }) ||
932
+ variations[0];
933
+ }
934
+ var targetUrl =
935
+ (Array.isArray(test.urltargeting) && test.urltargeting[0]) ||
936
+ test.pageUrl ||
937
+ (test.metadata_1 && test.metadata_1.editor_url) ||
938
+ '';
939
+ var url = generatePreviewUrlString({
940
+ url: targetUrl,
941
+ test: test,
942
+ variation: activeVariation || {},
943
+ });
944
+ if (!url) {
945
+ console.warn('[V2] simulateExperiment: missing target URL');
946
+ return;
947
+ }
948
+ try {
949
+ window.open(url, '_blank');
950
+ } catch(err) {
951
+ console.warn('[V2] simulateExperiment:', err);
952
+ }
953
+ }
954
+
909
955
  window.addEventListener('message', function(e) {
910
956
  if (!e.data || e.data.channel !== CHANNEL) return;
911
957
  switch (e.data.type) {
@@ -957,6 +1003,13 @@ var changeObserver = null;
957
1003
  var changeObserverDoc = null;
958
1004
  /** Incremented while applying changesets / selection chrome so MutationObserver does not mark dirty */
959
1005
  var suppressIframeMutationDirty = 0;
1006
+ /** Throttled reconcile timer to keep DOM aligned with saved changesets. */
1007
+ var consistencyReconcileTimer = null;
1008
+ /** Background watchdog for late client-side re-renders that wipe applied changesets. */
1009
+ var consistencyWatchTimer = null;
1010
+ var consistencyWatchDoc = null;
1011
+ var consistencyWatchTicks = 0;
1012
+ var CONSISTENCY_WATCH_MAX_TICKS = 160;
960
1013
  /** { doc, onRS } \u2014 iframe document readystate until complete */
961
1014
  var iframeDocLoadingListeners = null;
962
1015
  // Each entry: {selector, label, cssProp, value, targetEl}
@@ -1940,9 +1993,71 @@ function clearPendingGranularChangesets() {
1940
1993
  }
1941
1994
  }
1942
1995
 
1996
+ function stopConsistencyWatchdog() {
1997
+ if (consistencyWatchTimer) {
1998
+ clearInterval(consistencyWatchTimer);
1999
+ consistencyWatchTimer = null;
2000
+ }
2001
+ consistencyWatchDoc = null;
2002
+ consistencyWatchTicks = 0;
2003
+ }
2004
+
2005
+ function runConsistencyReconcile() {
2006
+ if (!activeVarId) return;
2007
+ var iframe = document.getElementById('iframeId');
2008
+ var doc = iframe && iframe.contentDocument;
2009
+ if (!doc || !doc.body) return;
2010
+ var variation = variations.find(function(v) { return v._id === activeVarId; });
2011
+ var cs = parseVariationChangesets(variation);
2012
+ if (!cs.length || changesetsHaveBodySnapshot(cs)) return;
2013
+ var granular = filterGranularChangesetEntries(cs);
2014
+ var unresolved = countUnresolvedGranularSelectors(doc, granular);
2015
+ if (unresolved > 0 || changesetListHasStructural(cs)) {
2016
+ reapplyActiveVariationGranular(doc);
2017
+ registerPendingGranularChangesets(cs, doc);
2018
+ }
2019
+ }
2020
+
2021
+ function scheduleConsistencyReconcile() {
2022
+ if (consistencyReconcileTimer) return;
2023
+ consistencyReconcileTimer = setTimeout(function() {
2024
+ consistencyReconcileTimer = null;
2025
+ runConsistencyReconcile();
2026
+ }, 80);
2027
+ }
2028
+
2029
+ function startConsistencyWatchdog(doc) {
2030
+ if (!doc || !doc.body) return;
2031
+ if (consistencyWatchDoc === doc && consistencyWatchTimer) return;
2032
+ stopConsistencyWatchdog();
2033
+ consistencyWatchDoc = doc;
2034
+ consistencyWatchTicks = 0;
2035
+ consistencyWatchTimer = setInterval(function() {
2036
+ if (consistencyWatchDoc !== doc) {
2037
+ stopConsistencyWatchdog();
2038
+ return;
2039
+ }
2040
+ var iframe = document.getElementById('iframeId');
2041
+ if (!iframe || iframe.contentDocument !== doc || !doc.body) {
2042
+ stopConsistencyWatchdog();
2043
+ return;
2044
+ }
2045
+ consistencyWatchTicks += 1;
2046
+ scheduleConsistencyReconcile();
2047
+ if (consistencyWatchTicks >= CONSISTENCY_WATCH_MAX_TICKS) {
2048
+ stopConsistencyWatchdog();
2049
+ }
2050
+ }, 400);
2051
+ }
2052
+
1943
2053
  function resetIframeBindings() {
1944
2054
  detachIframeLoadingListeners();
1945
2055
  stopIframeContentApplyWatcher();
2056
+ stopConsistencyWatchdog();
2057
+ if (consistencyReconcileTimer) {
2058
+ clearTimeout(consistencyReconcileTimer);
2059
+ consistencyReconcileTimer = null;
2060
+ }
1946
2061
  appliedChangesetSnapshots = {};
1947
2062
  appliedStructuralChangesetKeys = {};
1948
2063
  clickAttachDoc = null;
@@ -3556,10 +3671,31 @@ function attachChangeObserver() {
3556
3671
  changeObserver = null;
3557
3672
  changeObserverDoc = null;
3558
3673
  }
3559
- changeObserver = new MutationObserver(function() {
3674
+ changeObserver = new MutationObserver(function(mutations) {
3560
3675
  // Dirty state is derived from changesets baseline + stateChanges (not raw DOM mutations).
3676
+ var bodyReplaced = false;
3677
+ for (var mi = 0; mi < mutations.length; mi++) {
3678
+ var m = mutations[mi];
3679
+ if (
3680
+ m &&
3681
+ m.type === 'childList' &&
3682
+ m.target === doc.body &&
3683
+ m.addedNodes &&
3684
+ m.removedNodes &&
3685
+ m.addedNodes.length > 0 &&
3686
+ m.removedNodes.length > 0
3687
+ ) {
3688
+ bodyReplaced = true;
3689
+ break;
3690
+ }
3691
+ }
3692
+ if (bodyReplaced) {
3693
+ // Page JS replaced body children; allow structural rows (insert/reorder) to apply again.
3694
+ appliedStructuralChangesetKeys = {};
3695
+ }
3561
3696
  scheduleDomTreeRefresh();
3562
3697
  scheduleGranularChangesetReapply();
3698
+ scheduleConsistencyReconcile();
3563
3699
  });
3564
3700
  changeObserver.observe(doc.body, {
3565
3701
  childList: true, subtree: true, attributes: true, characterData: true
@@ -3589,6 +3725,8 @@ function syncIframeInteractions(reason) {
3589
3725
  attachClickHandler();
3590
3726
  attachDragReposition();
3591
3727
  attachChangeObserver();
3728
+ startConsistencyWatchdog(doc);
3729
+ scheduleConsistencyReconcile();
3592
3730
  bindSelectionToolbarScroll();
3593
3731
  var inp = document.getElementById('comp-search');
3594
3732
  renderDomTree(inp ? inp.value : '');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@accelerated-agency/visual-editor",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "private": false,
5
5
  "description": "Conversion visual editor as a reusable React package",
6
6
  "type": "module",