@accelerated-agency/visual-editor 0.2.4 → 0.2.5
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 +17 -0
- package/dist/vite.cjs +716 -98
- package/dist/vite.js +716 -98
- package/package.json +1 -1
package/dist/vite.cjs
CHANGED
|
@@ -850,6 +850,7 @@ select.pr-inp{cursor:pointer;background:#fff}
|
|
|
850
850
|
<script src="https://cdn.jsdelivr.net/gh/givanz/VvvebJs@master/libs/builder/inputs.js"></script>
|
|
851
851
|
<!-- components.js defines shared colour-class arrays used by bootstrap5/widgets components -->
|
|
852
852
|
<script src="https://cdn.jsdelivr.net/gh/givanz/VvvebJs@master/libs/builder/components.js"></script>
|
|
853
|
+
<script src="https://cdn.jsdelivr.net/gh/givanz/VvvebJs@master/libs/builder/components-html.js"></script>
|
|
853
854
|
<script>
|
|
854
855
|
/* Safety stub: if components.js didn't define these, create empty arrays so
|
|
855
856
|
components-bootstrap5.js doesn't throw ReferenceError on load. */
|
|
@@ -863,6 +864,18 @@ if (typeof colorSelectOptions === 'undefined') window.colorSelectOptions =
|
|
|
863
864
|
if (typeof textColorSelectOptions=== 'undefined') window.textColorSelectOptions= [];
|
|
864
865
|
if (typeof borderSelectOptions === 'undefined') window.borderSelectOptions = [];
|
|
865
866
|
if (typeof sizeSelectOptions === 'undefined') window.sizeSelectOptions = [];
|
|
867
|
+
if (window.Vvveb && window.Vvveb.Components) {
|
|
868
|
+
if (!window.Vvveb.ComponentsGroup) window.Vvveb.ComponentsGroup = {};
|
|
869
|
+
if (!window.Vvveb.ComponentsGroup['Bootstrap 5']) window.Vvveb.ComponentsGroup['Bootstrap 5'] = [];
|
|
870
|
+
try {
|
|
871
|
+
var baseExists =
|
|
872
|
+
window.Vvveb.Components._components &&
|
|
873
|
+
Object.prototype.hasOwnProperty.call(window.Vvveb.Components._components, '_base');
|
|
874
|
+
if (!baseExists && typeof window.Vvveb.Components.add === 'function') {
|
|
875
|
+
window.Vvveb.Components.add('_base', { name: 'Base', properties: [] });
|
|
876
|
+
}
|
|
877
|
+
} catch(_) {}
|
|
878
|
+
}
|
|
866
879
|
</script>
|
|
867
880
|
<script src="https://cdn.jsdelivr.net/gh/givanz/VvvebJs@master/libs/builder/components-bootstrap5.js"></script>
|
|
868
881
|
<script src="https://cdn.jsdelivr.net/gh/givanz/VvvebJs@master/libs/builder/components-widgets.js"></script>
|
|
@@ -914,6 +927,8 @@ var experimentData = null;
|
|
|
914
927
|
var variations = [];
|
|
915
928
|
var activeVarId = null;
|
|
916
929
|
var varHtmlCache = {};
|
|
930
|
+
/** Per-variation chain rows from structural actions (insert/duplicate/delete/reorder/hide), merged on Finalize. */
|
|
931
|
+
var sessionStructuralChainRowsByVarId = {};
|
|
917
932
|
/** Last iframe proxy URL we navigated to \u2014 used to skip redundant reloads when parent re-sends load-experiment */
|
|
918
933
|
var lastLoadedProxyUrl = '';
|
|
919
934
|
/** API changeset rows (excluding __vvveb_body__) reapplied until selectors match late-hydrated DOM */
|
|
@@ -927,6 +942,8 @@ var iframeContentNavGen = 0;
|
|
|
927
942
|
var iframeContentApplyTimer = null;
|
|
928
943
|
var iframeEarlyGranularPrimedForGen = null;
|
|
929
944
|
var iframeEarlySyncPrimedForGen = null;
|
|
945
|
+
/** insert/reorder entries are applied from early granular + full apply \u2014 skip exact duplicates per iframe nav */
|
|
946
|
+
var appliedStructuralChangesetKeys = {};
|
|
930
947
|
var isDirty = false;
|
|
931
948
|
var vvvebReady = false;
|
|
932
949
|
var currentMode = 'editor';
|
|
@@ -946,6 +963,8 @@ var selectionResizeBound = false;
|
|
|
946
963
|
var clickAttachDoc = null;
|
|
947
964
|
var changeObserver = null;
|
|
948
965
|
var changeObserverDoc = null;
|
|
966
|
+
/** Incremented while applying changesets / selection chrome so MutationObserver does not mark dirty */
|
|
967
|
+
var suppressIframeMutationDirty = 0;
|
|
949
968
|
/** { doc, onRS } \u2014 iframe document readystate until complete */
|
|
950
969
|
var iframeDocLoadingListeners = null;
|
|
951
970
|
// Each entry: {selector, label, cssProp, value, targetEl}
|
|
@@ -953,12 +972,89 @@ var iframeDocLoadingListeners = null;
|
|
|
953
972
|
var stateChanges = [];
|
|
954
973
|
/** Pre-apply DOM snapshots for granular + body changesets (used when removing History rows) */
|
|
955
974
|
var appliedChangesetSnapshots = {};
|
|
975
|
+
/** Canonical JSON fingerprints of persisted changesets per variation (last load / finalize) */
|
|
976
|
+
var baselineChangesetsByVarId = {};
|
|
977
|
+
|
|
978
|
+
// \u2500\u2500 Dirty tracking (compare DB baseline + session stateChanges vs current export) \u2500\u2500
|
|
979
|
+
function beginSuppressIframeMutationDirty() {
|
|
980
|
+
suppressIframeMutationDirty += 1;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function endSuppressIframeMutationDirty() {
|
|
984
|
+
suppressIframeMutationDirty = Math.max(0, suppressIframeMutationDirty - 1);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/** Stable stringify of a variation's changesets field (string or array from API). */
|
|
988
|
+
function fingerprintChangesetsField(raw) {
|
|
989
|
+
if (raw == null) return '[]';
|
|
990
|
+
if (Array.isArray(raw)) {
|
|
991
|
+
try {
|
|
992
|
+
return JSON.stringify(raw);
|
|
993
|
+
} catch(_) {
|
|
994
|
+
return '[]';
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if (typeof raw !== 'string') return '[]';
|
|
998
|
+
var s = raw.trim();
|
|
999
|
+
if (!s) return '[]';
|
|
1000
|
+
try {
|
|
1001
|
+
var p = JSON.parse(s);
|
|
1002
|
+
return JSON.stringify(Array.isArray(p) ? p : []);
|
|
1003
|
+
} catch(_) {
|
|
1004
|
+
return '[]';
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function captureBaselineFromVariations(list) {
|
|
1009
|
+
baselineChangesetsByVarId = {};
|
|
1010
|
+
if (!list || !list.length) return;
|
|
1011
|
+
for (var i = 0; i < list.length; i++) {
|
|
1012
|
+
var v = list[i];
|
|
1013
|
+
if (!v || !v._id) continue;
|
|
1014
|
+
baselineChangesetsByVarId[v._id] = fingerprintChangesetsField(v.changesets);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/** Fingerprint of what Finalize would send for this variation (matches buildPersistedChainSetsForVariation). */
|
|
1019
|
+
function persistedExportFingerprintForVariation(v) {
|
|
1020
|
+
if (!v || !v._id) return '[]';
|
|
1021
|
+
try {
|
|
1022
|
+
var rows = buildPersistedChainSetsForVariation(v);
|
|
1023
|
+
return JSON.stringify(rows || []);
|
|
1024
|
+
} catch(_) {
|
|
1025
|
+
return '[]';
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function setEditorDirty(dirty) {
|
|
1030
|
+
var was = isDirty;
|
|
1031
|
+
isDirty = !!dirty;
|
|
1032
|
+
var dot = document.getElementById('dirty-dot');
|
|
1033
|
+
if (dot) dot.classList.toggle('on', isDirty);
|
|
1034
|
+
if (isDirty && !was) send('mutations-changed', {});
|
|
1035
|
+
if (!isDirty && was) send('editor-dirty', { dirty: false });
|
|
1036
|
+
if (!isDirty) {
|
|
1037
|
+
savedAt = Date.now();
|
|
1038
|
+
updateSaveTime();
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
956
1041
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1042
|
+
function recomputeEditorDirty() {
|
|
1043
|
+
var d = stateChanges.length > 0;
|
|
1044
|
+
if (!d && variations && variations.length) {
|
|
1045
|
+
for (var i = 0; i < variations.length; i++) {
|
|
1046
|
+
var v = variations[i];
|
|
1047
|
+
var vid = v._id;
|
|
1048
|
+
var cur = persistedExportFingerprintForVariation(v);
|
|
1049
|
+
var base = baselineChangesetsByVarId[vid];
|
|
1050
|
+
if (base == null) base = '[]';
|
|
1051
|
+
if (cur !== base) {
|
|
1052
|
+
d = true;
|
|
1053
|
+
break;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
setEditorDirty(d);
|
|
962
1058
|
}
|
|
963
1059
|
var savedAt = null;
|
|
964
1060
|
function updateSaveTime() {
|
|
@@ -968,12 +1064,6 @@ function updateSaveTime() {
|
|
|
968
1064
|
el.textContent = s < 60 ? s + 's ago' : Math.floor(s / 60) + 'm ago';
|
|
969
1065
|
}
|
|
970
1066
|
setInterval(updateSaveTime, 10000);
|
|
971
|
-
function markClean() {
|
|
972
|
-
isDirty = false;
|
|
973
|
-
savedAt = Date.now();
|
|
974
|
-
document.getElementById('dirty-dot').classList.remove('on');
|
|
975
|
-
updateSaveTime();
|
|
976
|
-
}
|
|
977
1067
|
|
|
978
1068
|
// \u2500\u2500 Mode 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\u2500\u2500\u2500\u2500\u2500
|
|
979
1069
|
function setMode(mode) {
|
|
@@ -1150,6 +1240,7 @@ function logChange(selector, inputId, value, targetEl, originalValue) {
|
|
|
1150
1240
|
if (idx >= 0) { stateChanges[idx] = entry; } else { stateChanges.push(entry); }
|
|
1151
1241
|
}
|
|
1152
1242
|
if (currentMainTab === 'states') renderStatesTab();
|
|
1243
|
+
recomputeEditorDirty();
|
|
1153
1244
|
}
|
|
1154
1245
|
|
|
1155
1246
|
function renderStatesTab() {
|
|
@@ -1183,7 +1274,7 @@ function renderStatesTab() {
|
|
|
1183
1274
|
|
|
1184
1275
|
// Resolve a live DOM element for a state-change entry.
|
|
1185
1276
|
// Tries the stored direct reference first; if it's detached or missing,
|
|
1186
|
-
// falls back to querySelector(
|
|
1277
|
+
// falls back to querySelector (with .vve-* class stripped) inside the iframe document.
|
|
1187
1278
|
function resolveChangeEl(change) {
|
|
1188
1279
|
try {
|
|
1189
1280
|
var iframeDoc = document.getElementById('iframeId').contentDocument;
|
|
@@ -1193,7 +1284,7 @@ function resolveChangeEl(change) {
|
|
|
1193
1284
|
return change.targetEl;
|
|
1194
1285
|
}
|
|
1195
1286
|
// Fallback: re-query by the stored CSS selector
|
|
1196
|
-
return iframeDoc
|
|
1287
|
+
return querySelectorResolved(iframeDoc, change.selector);
|
|
1197
1288
|
} catch (e) {
|
|
1198
1289
|
console.warn('[V2] resolveChangeEl:', e);
|
|
1199
1290
|
return null;
|
|
@@ -1256,7 +1347,7 @@ function removeStateChange(idx) {
|
|
|
1256
1347
|
syncDesignInput(change);
|
|
1257
1348
|
stateChanges.splice(idx, 1);
|
|
1258
1349
|
renderStatesTab();
|
|
1259
|
-
|
|
1350
|
+
recomputeEditorDirty();
|
|
1260
1351
|
}
|
|
1261
1352
|
|
|
1262
1353
|
function clearAllStates() {
|
|
@@ -1266,7 +1357,7 @@ function clearAllStates() {
|
|
|
1266
1357
|
});
|
|
1267
1358
|
stateChanges = [];
|
|
1268
1359
|
renderStatesTab();
|
|
1269
|
-
|
|
1360
|
+
recomputeEditorDirty();
|
|
1270
1361
|
}
|
|
1271
1362
|
|
|
1272
1363
|
// \u2500\u2500 History tab (saved changesets from DB for active variation) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
@@ -1291,10 +1382,11 @@ function persistActiveVariationChangesets(arr) {
|
|
|
1291
1382
|
|
|
1292
1383
|
function entrySnapshotKey(entry) {
|
|
1293
1384
|
if (!entry || !entry.selector) return '';
|
|
1385
|
+
var selKey = sanitizeSelectorForMatch(entry.selector) || entry.selector;
|
|
1294
1386
|
return (
|
|
1295
|
-
|
|
1387
|
+
selKey +
|
|
1296
1388
|
'\0' +
|
|
1297
|
-
(entry
|
|
1389
|
+
normalizeChangesetType(entry) +
|
|
1298
1390
|
'\0' +
|
|
1299
1391
|
String(entry.property || '') +
|
|
1300
1392
|
'\0' +
|
|
@@ -1312,7 +1404,7 @@ function captureChangesetSnapshotBeforeApply(entry, el, iframeDoc) {
|
|
|
1312
1404
|
if (!entry || !el || entry.selector === '__vvveb_body__') return;
|
|
1313
1405
|
var k = entrySnapshotKey(entry);
|
|
1314
1406
|
if (appliedChangesetSnapshots[k]) return;
|
|
1315
|
-
switch (entry
|
|
1407
|
+
switch (normalizeChangesetType(entry)) {
|
|
1316
1408
|
case 'content':
|
|
1317
1409
|
if (entry.html != null) {
|
|
1318
1410
|
appliedChangesetSnapshots[k] = { kind: 'innerHTML', v: el.innerHTML };
|
|
@@ -1380,10 +1472,7 @@ function revertChangesetEntryOnDom(entry) {
|
|
|
1380
1472
|
if (!iframeDoc) return false;
|
|
1381
1473
|
var k = entrySnapshotKey(entry);
|
|
1382
1474
|
var snap = appliedChangesetSnapshots[k];
|
|
1383
|
-
var el =
|
|
1384
|
-
try {
|
|
1385
|
-
el = iframeDoc.querySelector(entry.selector);
|
|
1386
|
-
} catch(_) {}
|
|
1475
|
+
var el = querySelectorResolved(iframeDoc, entry.selector);
|
|
1387
1476
|
if (!snap || !el) {
|
|
1388
1477
|
softReloadEditorIframe();
|
|
1389
1478
|
delete appliedChangesetSnapshots[k];
|
|
@@ -1410,7 +1499,7 @@ function revertChangesetEntryOnDom(entry) {
|
|
|
1410
1499
|
function historyEntryTypeLabel(entry) {
|
|
1411
1500
|
if (!entry) return 'Change';
|
|
1412
1501
|
if (entry.selector === '__vvveb_body__') return 'Full page HTML';
|
|
1413
|
-
var t = (entry
|
|
1502
|
+
var t = normalizeChangesetType(entry);
|
|
1414
1503
|
if (t === 'content') return entry.html != null ? 'Inner HTML' : 'Text / content';
|
|
1415
1504
|
if (t === 'style') return 'Style: ' + (entry.property || '');
|
|
1416
1505
|
if (t === 'attribute') return 'Attribute: ' + (entry.attribute || '');
|
|
@@ -1424,7 +1513,8 @@ function historyEntryValuePreview(entry) {
|
|
|
1424
1513
|
if (entry.selector === '__vvveb_body__') return '(body snapshot)';
|
|
1425
1514
|
if (entry.html != null) return String(entry.html).slice(0, 120);
|
|
1426
1515
|
if (entry.value != null) return String(entry.value).slice(0, 120);
|
|
1427
|
-
|
|
1516
|
+
var nt = normalizeChangesetType(entry);
|
|
1517
|
+
if (nt === 'style' || nt === 'attribute') return String(entry.value != null ? entry.value : '').slice(0, 120);
|
|
1428
1518
|
return '';
|
|
1429
1519
|
}
|
|
1430
1520
|
|
|
@@ -1458,6 +1548,9 @@ function renderHistoryTab() {
|
|
|
1458
1548
|
var val = historyEntryValuePreview(item.entry);
|
|
1459
1549
|
html +=
|
|
1460
1550
|
'<div class="state-item">' +
|
|
1551
|
+
'<span class="state-item-idx" style="opacity:0.55;font-size:10px;min-width:2.2em;display:inline-block" title="Order in saved changesets">#' +
|
|
1552
|
+
item.idx +
|
|
1553
|
+
'</span>' +
|
|
1461
1554
|
'<span class="state-item-label">' +
|
|
1462
1555
|
esc(lab) +
|
|
1463
1556
|
'</span>' +
|
|
@@ -1466,7 +1559,9 @@ function renderHistoryTab() {
|
|
|
1466
1559
|
'">' +
|
|
1467
1560
|
esc(val) +
|
|
1468
1561
|
'</span>' +
|
|
1469
|
-
'<button type="button" class="state-remove" title="Remove
|
|
1562
|
+
'<button type="button" class="state-remove" title="Remove this saved row (#' +
|
|
1563
|
+
item.idx +
|
|
1564
|
+
')" onclick="removeHistoryChangeset(' +
|
|
1470
1565
|
item.idx +
|
|
1471
1566
|
')">✕</button>' +
|
|
1472
1567
|
'</div>';
|
|
@@ -1476,6 +1571,16 @@ function renderHistoryTab() {
|
|
|
1476
1571
|
container.innerHTML = html;
|
|
1477
1572
|
}
|
|
1478
1573
|
|
|
1574
|
+
function changesetListHasStructural(arr) {
|
|
1575
|
+
if (!arr || !arr.length) return false;
|
|
1576
|
+
for (var i = 0; i < arr.length; i++) {
|
|
1577
|
+
var e = arr[i];
|
|
1578
|
+
var t = normalizeChangesetType(e);
|
|
1579
|
+
if (e && (t === 'insert' || t === 'reorder')) return true;
|
|
1580
|
+
}
|
|
1581
|
+
return false;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1479
1584
|
function removeHistoryChangeset(idx) {
|
|
1480
1585
|
var v = getActiveVariationForHistory();
|
|
1481
1586
|
if (!v) return;
|
|
@@ -1488,18 +1593,31 @@ function removeHistoryChangeset(idx) {
|
|
|
1488
1593
|
try {
|
|
1489
1594
|
delete varHtmlCache[activeVarId];
|
|
1490
1595
|
} catch(_) {}
|
|
1491
|
-
|
|
1596
|
+
// Re-applying remaining rows on top of current DOM duplicates insert/reorder nodes; reload when any
|
|
1597
|
+
// structural row remains or was removed (revert may already have started a reload for insert/body).
|
|
1598
|
+
var removedType = normalizeChangesetType(removed);
|
|
1599
|
+
var needsStructuralReload =
|
|
1600
|
+
!didReload &&
|
|
1601
|
+
(removedType === 'insert' ||
|
|
1602
|
+
removedType === 'reorder' ||
|
|
1603
|
+
changesetListHasStructural(arr));
|
|
1604
|
+
if (didReload) {
|
|
1605
|
+
/* revertChangesetEntryOnDom already kicked off iframe reload */
|
|
1606
|
+
} else if (needsStructuralReload) {
|
|
1607
|
+
softReloadEditorIframe();
|
|
1608
|
+
} else {
|
|
1492
1609
|
try {
|
|
1610
|
+
appliedStructuralChangesetKeys = {};
|
|
1493
1611
|
applyActiveVariationHtml();
|
|
1494
1612
|
registerPendingGranularChangesets(
|
|
1495
1613
|
arr,
|
|
1496
1614
|
document.getElementById('iframeId').contentDocument,
|
|
1497
1615
|
);
|
|
1616
|
+
saveCurrentVariationHtml();
|
|
1498
1617
|
} catch(_) {}
|
|
1499
|
-
saveCurrentVariationHtml();
|
|
1500
1618
|
}
|
|
1501
1619
|
if (currentMainTab === 'history') renderHistoryTab();
|
|
1502
|
-
|
|
1620
|
+
recomputeEditorDirty();
|
|
1503
1621
|
scheduleDomTreeRefresh();
|
|
1504
1622
|
}
|
|
1505
1623
|
|
|
@@ -1509,15 +1627,82 @@ function clearAllHistoryChangesets() {
|
|
|
1509
1627
|
if (!parseVariationChangesets(v).length) return;
|
|
1510
1628
|
persistActiveVariationChangesets([]);
|
|
1511
1629
|
appliedChangesetSnapshots = {};
|
|
1630
|
+
appliedStructuralChangesetKeys = {};
|
|
1512
1631
|
try {
|
|
1513
1632
|
delete varHtmlCache[activeVarId];
|
|
1514
1633
|
} catch(_) {}
|
|
1515
1634
|
if (currentMainTab === 'history') renderHistoryTab();
|
|
1516
|
-
|
|
1635
|
+
recomputeEditorDirty();
|
|
1517
1636
|
scheduleDomTreeRefresh();
|
|
1518
1637
|
softReloadEditorIframe();
|
|
1519
1638
|
}
|
|
1520
1639
|
|
|
1640
|
+
// \u2500\u2500 Persisted active variation (survives iframe / full page reload) \u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1641
|
+
/** All Visual Editor iframe keys in localStorage use this prefix \u2014 cleared on close. */
|
|
1642
|
+
var VVE_LOCAL_STORAGE_PREFIX = 'vve:';
|
|
1643
|
+
|
|
1644
|
+
function clearVisualEditorLocalStorage() {
|
|
1645
|
+
try {
|
|
1646
|
+
for (var i = localStorage.length - 1; i >= 0; i--) {
|
|
1647
|
+
var k = localStorage.key(i);
|
|
1648
|
+
if (k && k.indexOf(VVE_LOCAL_STORAGE_PREFIX) === 0) {
|
|
1649
|
+
localStorage.removeItem(k);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
} catch(_) {}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
function activeVariationStorageKeyFromPayload(data) {
|
|
1656
|
+
return (
|
|
1657
|
+
VVE_LOCAL_STORAGE_PREFIX +
|
|
1658
|
+
'activeVar:' +
|
|
1659
|
+
String((data && data.experimentId) || '') +
|
|
1660
|
+
':' +
|
|
1661
|
+
String((data && data.pageUrl) || '')
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function readPersistedActiveVariationId(data) {
|
|
1666
|
+
try {
|
|
1667
|
+
var sk = activeVariationStorageKeyFromPayload(data);
|
|
1668
|
+
if (sk === VVE_LOCAL_STORAGE_PREFIX + 'activeVar::') return null;
|
|
1669
|
+
return localStorage.getItem(sk);
|
|
1670
|
+
} catch(_) {
|
|
1671
|
+
return null;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
function writePersistedActiveVariationId(varId) {
|
|
1676
|
+
try {
|
|
1677
|
+
if (!experimentData || !experimentData.experimentId) return;
|
|
1678
|
+
var sk =
|
|
1679
|
+
VVE_LOCAL_STORAGE_PREFIX +
|
|
1680
|
+
'activeVar:' +
|
|
1681
|
+
String(experimentData.experimentId || '') +
|
|
1682
|
+
':' +
|
|
1683
|
+
String(experimentData.pageUrl || '');
|
|
1684
|
+
if (sk === VVE_LOCAL_STORAGE_PREFIX + 'activeVar::') return;
|
|
1685
|
+
if (varId) localStorage.setItem(sk, String(varId));
|
|
1686
|
+
} catch(_) {}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
/**
|
|
1690
|
+
* @param allowPrevMemory when true, keep in-session activeVarId if still valid (skip-reload path).
|
|
1691
|
+
*/
|
|
1692
|
+
function pickActiveVariationIdForLoad(data, variationsArr, prevMemoryId, allowPrevMemory) {
|
|
1693
|
+
var baseline = variationsArr.find(function(v) { return v.baseline; });
|
|
1694
|
+
var fallback = (baseline || variationsArr[0] || {})._id || null;
|
|
1695
|
+
if (!variationsArr.length) return null;
|
|
1696
|
+
if (allowPrevMemory && prevMemoryId && variationsArr.some(function(v) { return v._id === prevMemoryId; })) {
|
|
1697
|
+
return prevMemoryId;
|
|
1698
|
+
}
|
|
1699
|
+
var stored = readPersistedActiveVariationId(data);
|
|
1700
|
+
if (stored && variationsArr.some(function(v) { return v._id === stored; })) {
|
|
1701
|
+
return stored;
|
|
1702
|
+
}
|
|
1703
|
+
return fallback;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1521
1706
|
// \u2500\u2500 Experiment loading \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
|
|
1522
1707
|
function handleLoadExperiment(data) {
|
|
1523
1708
|
clearPendingGranularChangesets();
|
|
@@ -1545,10 +1730,8 @@ function handleLoadExperiment(data) {
|
|
|
1545
1730
|
experimentData = data;
|
|
1546
1731
|
variations = Array.isArray(data.variations) ? data.variations : [];
|
|
1547
1732
|
var prevActive = activeVarId;
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
activeVarId =
|
|
1551
|
-
prevActive && variations.some(function(v) { return v._id === prevActive; }) ? prevActive : fallback;
|
|
1733
|
+
activeVarId = pickActiveVariationIdForLoad(data, variations, prevActive, true);
|
|
1734
|
+
writePersistedActiveVariationId(activeVarId);
|
|
1552
1735
|
renderVariationTabs();
|
|
1553
1736
|
var urlBarSkip = document.getElementById('url-bar');
|
|
1554
1737
|
urlBarSkip.textContent = pageUrl;
|
|
@@ -1562,24 +1745,30 @@ function handleLoadExperiment(data) {
|
|
|
1562
1745
|
if (ifrSkip && ifrSkip.contentDocument) attachIframeLoadingUntilComplete(ifrSkip);
|
|
1563
1746
|
} catch(_) {}
|
|
1564
1747
|
if (currentMainTab === 'history') renderHistoryTab();
|
|
1748
|
+
captureBaselineFromVariations(variations);
|
|
1749
|
+
recomputeEditorDirty();
|
|
1565
1750
|
return;
|
|
1566
1751
|
}
|
|
1567
1752
|
|
|
1568
1753
|
if (!experimentData || prevKey !== nextKey) {
|
|
1569
1754
|
varHtmlCache = {};
|
|
1755
|
+
sessionStructuralChainRowsByVarId = {};
|
|
1570
1756
|
appliedChangesetSnapshots = {};
|
|
1757
|
+
appliedStructuralChangesetKeys = {};
|
|
1571
1758
|
}
|
|
1572
1759
|
experimentData = data;
|
|
1573
1760
|
variations = Array.isArray(data.variations) ? data.variations : [];
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
activeVarId
|
|
1761
|
+
var sameExpPage = prevKey === nextKey;
|
|
1762
|
+
activeVarId = pickActiveVariationIdForLoad(data, variations, activeVarId, sameExpPage);
|
|
1763
|
+
writePersistedActiveVariationId(activeVarId);
|
|
1577
1764
|
renderVariationTabs();
|
|
1578
1765
|
|
|
1579
1766
|
var urlBar = document.getElementById('url-bar');
|
|
1580
1767
|
urlBar.textContent = pageUrl;
|
|
1581
1768
|
urlBar.title = pageUrl;
|
|
1582
1769
|
if (currentMainTab === 'history') renderHistoryTab();
|
|
1770
|
+
captureBaselineFromVariations(variations);
|
|
1771
|
+
recomputeEditorDirty();
|
|
1583
1772
|
loadPage(proxyUrl);
|
|
1584
1773
|
}
|
|
1585
1774
|
|
|
@@ -1679,9 +1868,7 @@ function granularAnySelectorMatches(doc, cs) {
|
|
|
1679
1868
|
if (!doc || !cs || !cs.length) return false;
|
|
1680
1869
|
var g = filterGranularChangesetEntries(cs);
|
|
1681
1870
|
for (var i = 0; i < g.length; i++) {
|
|
1682
|
-
|
|
1683
|
-
if (doc.querySelector(g[i].selector)) return true;
|
|
1684
|
-
} catch(_) {}
|
|
1871
|
+
if (querySelectorResolved(doc, g[i].selector)) return true;
|
|
1685
1872
|
}
|
|
1686
1873
|
return false;
|
|
1687
1874
|
}
|
|
@@ -1702,7 +1889,7 @@ function appendIframeReloadBust(url) {
|
|
|
1702
1889
|
// applying variation changesets there is then wiped when the real navigation commits.
|
|
1703
1890
|
function iframeDocMatchesNavigatedSrc(iframe, doc) {
|
|
1704
1891
|
if (!iframe || !doc) return false;
|
|
1705
|
-
var src = iframe.src || iframe.
|
|
1892
|
+
var src = iframe.getAttribute('src') || iframe.src || '';
|
|
1706
1893
|
if (!src || src === 'about:blank') return false;
|
|
1707
1894
|
var loc = '';
|
|
1708
1895
|
try {
|
|
@@ -1711,12 +1898,28 @@ function iframeDocMatchesNavigatedSrc(iframe, doc) {
|
|
|
1711
1898
|
return false;
|
|
1712
1899
|
}
|
|
1713
1900
|
if (!loc || loc === 'about:blank') return false;
|
|
1714
|
-
var rmSrc = src.match(/__ve_reload=([0-9]+)/);
|
|
1715
|
-
if (rmSrc) return loc.indexOf('__ve_reload=' + rmSrc[1]) !== -1;
|
|
1716
1901
|
try {
|
|
1717
1902
|
var base = window.location.href;
|
|
1718
1903
|
var su = new URL(src, base);
|
|
1904
|
+
if (su.searchParams && su.searchParams.has('__ve_reload')) {
|
|
1905
|
+
su.searchParams.delete('__ve_reload');
|
|
1906
|
+
}
|
|
1719
1907
|
var du = new URL(loc, base);
|
|
1908
|
+
if (du.searchParams && du.searchParams.has('__ve_reload')) {
|
|
1909
|
+
du.searchParams.delete('__ve_reload');
|
|
1910
|
+
}
|
|
1911
|
+
// Same-origin proxy that keeps document address aligned with iframe src
|
|
1912
|
+
if (su.origin === du.origin && su.pathname + su.search === du.pathname + du.search) {
|
|
1913
|
+
return true;
|
|
1914
|
+
}
|
|
1915
|
+
// conversion-proxy: iframe src stays on our app, but doc.URL is usually the target site after redirects
|
|
1916
|
+
var p = su.pathname || '';
|
|
1917
|
+
var isRootProxyPath = p === '/api/conversion-proxy' || p.indexOf('/api/conversion-proxy/') === 0;
|
|
1918
|
+
var isNestedMalformedProxy = !isRootProxyPath && p.indexOf('api/conversion-proxy') !== -1;
|
|
1919
|
+
if (isNestedMalformedProxy) return false;
|
|
1920
|
+
if (isRootProxyPath || String(su.href).indexOf('conversion-proxy') !== -1) {
|
|
1921
|
+
return doc === iframe.contentDocument;
|
|
1922
|
+
}
|
|
1720
1923
|
return su.pathname + su.search === du.pathname + du.search;
|
|
1721
1924
|
} catch(_) {
|
|
1722
1925
|
return false;
|
|
@@ -1749,6 +1952,7 @@ function resetIframeBindings() {
|
|
|
1749
1952
|
detachIframeLoadingListeners();
|
|
1750
1953
|
stopIframeContentApplyWatcher();
|
|
1751
1954
|
appliedChangesetSnapshots = {};
|
|
1955
|
+
appliedStructuralChangesetKeys = {};
|
|
1752
1956
|
clickAttachDoc = null;
|
|
1753
1957
|
dragAttachDoc = null;
|
|
1754
1958
|
changeObserverDoc = null;
|
|
@@ -1819,13 +2023,19 @@ function switchVariation(varId) {
|
|
|
1819
2023
|
saveCurrentVariationHtml();
|
|
1820
2024
|
clearPendingGranularChangesets();
|
|
1821
2025
|
activeVarId = varId;
|
|
2026
|
+
writePersistedActiveVariationId(varId);
|
|
1822
2027
|
renderVariationTabs();
|
|
1823
2028
|
deselectElement();
|
|
1824
2029
|
try {
|
|
1825
2030
|
var iframe = document.getElementById('iframeId');
|
|
1826
2031
|
var saved = varHtmlCache[varId];
|
|
1827
2032
|
if (saved) {
|
|
1828
|
-
|
|
2033
|
+
beginSuppressIframeMutationDirty();
|
|
2034
|
+
try {
|
|
2035
|
+
iframe.contentDocument.body.innerHTML = saved;
|
|
2036
|
+
} finally {
|
|
2037
|
+
endSuppressIframeMutationDirty();
|
|
2038
|
+
}
|
|
1829
2039
|
detachIframeLoadingListeners();
|
|
1830
2040
|
setIframePageLoadingUi(false);
|
|
1831
2041
|
syncIframeInteractions('switch-variation-cache');
|
|
@@ -1848,6 +2058,7 @@ function switchVariation(varId) {
|
|
|
1848
2058
|
}
|
|
1849
2059
|
} catch(_) {}
|
|
1850
2060
|
if (currentMainTab === 'history') renderHistoryTab();
|
|
2061
|
+
recomputeEditorDirty();
|
|
1851
2062
|
}
|
|
1852
2063
|
|
|
1853
2064
|
function saveCurrentVariationHtml() {
|
|
@@ -1886,31 +2097,123 @@ function parseVariationChangesets(variation) {
|
|
|
1886
2097
|
}
|
|
1887
2098
|
}
|
|
1888
2099
|
|
|
2100
|
+
/** Lowercase entry.type so persisted / API rows match switches (e.g. Insert vs insert). */
|
|
2101
|
+
function normalizeChangesetType(entry) {
|
|
2102
|
+
return String(entry && entry.type != null ? entry.type : '').toLowerCase();
|
|
2103
|
+
}
|
|
2104
|
+
|
|
1889
2105
|
function filterGranularChangesetEntries(cs) {
|
|
1890
2106
|
if (!cs || !cs.length) return [];
|
|
1891
2107
|
var out = [];
|
|
1892
2108
|
for (var i = 0; i < cs.length; i++) {
|
|
1893
2109
|
var e = cs[i];
|
|
1894
|
-
if (e
|
|
2110
|
+
if (!e || !e.selector || e.selector === '__vvveb_body__') continue;
|
|
2111
|
+
out.push(e);
|
|
1895
2112
|
}
|
|
1896
2113
|
return out;
|
|
1897
2114
|
}
|
|
1898
2115
|
|
|
2116
|
+
/** Dedup key for merging chain-set rows (overlay wins over base). */
|
|
2117
|
+
function chainSetDedupKey(entry) {
|
|
2118
|
+
if (!entry || !entry.selector) return '';
|
|
2119
|
+
var t = normalizeChangesetType(entry);
|
|
2120
|
+
if (t === 'style') return entry.selector + '|s|' + String(entry.property || '');
|
|
2121
|
+
if (t === 'content') return entry.selector + '|c|' + (entry.html != null ? 'h' : 't');
|
|
2122
|
+
if (t === 'attribute') return entry.selector + '|a|' + String(entry.attribute || '');
|
|
2123
|
+
if (t === 'insert') return entry.selector + '|i|' + String(entry.action || '') + '|' + String(entry.html || '').slice(0, 120);
|
|
2124
|
+
if (t === 'remove') return entry.selector + '|r|';
|
|
2125
|
+
if (t === 'reorder') {
|
|
2126
|
+
return entry.selector + '|ro|' + String(entry.targetSelector || '') + '|' + String(entry.action || '');
|
|
2127
|
+
}
|
|
2128
|
+
try {
|
|
2129
|
+
return entry.selector + '|' + t + '|' + JSON.stringify(entry);
|
|
2130
|
+
} catch(_) {
|
|
2131
|
+
return entry.selector + '|' + t;
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
function mergeGranularChainSets(baseList, overlayList) {
|
|
2136
|
+
var map = {};
|
|
2137
|
+
var order = [];
|
|
2138
|
+
function ingest(arr) {
|
|
2139
|
+
if (!arr || !arr.length) return;
|
|
2140
|
+
for (var i = 0; i < arr.length; i++) {
|
|
2141
|
+
var e = arr[i];
|
|
2142
|
+
if (!e || !e.selector) continue;
|
|
2143
|
+
var k = chainSetDedupKey(e);
|
|
2144
|
+
if (!map[k]) order.push(k);
|
|
2145
|
+
map[k] = e;
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
ingest(baseList);
|
|
2149
|
+
ingest(overlayList);
|
|
2150
|
+
var out = [];
|
|
2151
|
+
for (var j = 0; j < order.length; j++) {
|
|
2152
|
+
out.push(map[order[j]]);
|
|
2153
|
+
}
|
|
2154
|
+
return out;
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
function appendSessionStructuralChainRow(varId, row) {
|
|
2158
|
+
if (!varId || !row) return;
|
|
2159
|
+
if (!sessionStructuralChainRowsByVarId[varId]) sessionStructuralChainRowsByVarId[varId] = [];
|
|
2160
|
+
sessionStructuralChainRowsByVarId[varId].push(row);
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
/** One States-tab row -> Conversion.io chain-set shape (matches applyChangesetEntry). */
|
|
2164
|
+
function stateChangeToChainSet(c) {
|
|
2165
|
+
if (!c || !c.selector) return null;
|
|
2166
|
+
if (c.cssProp) {
|
|
2167
|
+
return { selector: c.selector, type: 'style', property: c.cssProp, value: c.value };
|
|
2168
|
+
}
|
|
2169
|
+
switch (c.inputId) {
|
|
2170
|
+
case 'pp-text':
|
|
2171
|
+
return { selector: c.selector, type: 'content', value: c.value };
|
|
2172
|
+
case 'pp-html':
|
|
2173
|
+
return { selector: c.selector, type: 'content', html: c.value };
|
|
2174
|
+
case 'pp-cls':
|
|
2175
|
+
return { selector: c.selector, type: 'attribute', attribute: 'class', value: c.value };
|
|
2176
|
+
case 'pp-id':
|
|
2177
|
+
return { selector: c.selector, type: 'attribute', attribute: 'id', value: c.value };
|
|
2178
|
+
case 'pp-href':
|
|
2179
|
+
return { selector: c.selector, type: 'attribute', attribute: 'href', value: c.value };
|
|
2180
|
+
case 'pp-target':
|
|
2181
|
+
return { selector: c.selector, type: 'attribute', attribute: 'target', value: c.value };
|
|
2182
|
+
case 'pp-src':
|
|
2183
|
+
return { selector: c.selector, type: 'attribute', attribute: 'src', value: c.value };
|
|
2184
|
+
case 'pp-alt':
|
|
2185
|
+
return { selector: c.selector, type: 'attribute', attribute: 'alt', value: c.value };
|
|
2186
|
+
case 'pp-ph':
|
|
2187
|
+
return { selector: c.selector, type: 'attribute', attribute: 'placeholder', value: c.value };
|
|
2188
|
+
case 'pp-css':
|
|
2189
|
+
return { selector: c.selector, type: 'attribute', attribute: 'style', value: c.value };
|
|
2190
|
+
case 'pp-mob-css':
|
|
2191
|
+
return { selector: c.selector, type: 'attribute', attribute: 'data-mobile-css', value: c.value };
|
|
2192
|
+
case 'pp-tab-css':
|
|
2193
|
+
return { selector: c.selector, type: 'attribute', attribute: 'data-tablet-css', value: c.value };
|
|
2194
|
+
default:
|
|
2195
|
+
return null;
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
|
|
1899
2199
|
function countUnresolvedGranularSelectors(iframeDoc, entries) {
|
|
1900
2200
|
if (!iframeDoc || !entries || !entries.length) return 0;
|
|
1901
2201
|
var n = 0;
|
|
1902
2202
|
for (var i = 0; i < entries.length; i++) {
|
|
1903
|
-
|
|
1904
|
-
try { el = iframeDoc.querySelector(entries[i].selector); } catch(_) {}
|
|
1905
|
-
if (!el) n++;
|
|
2203
|
+
if (!querySelectorResolved(iframeDoc, entries[i].selector)) n++;
|
|
1906
2204
|
}
|
|
1907
2205
|
return n;
|
|
1908
2206
|
}
|
|
1909
2207
|
|
|
1910
2208
|
function applyGranularChangesetEntries(iframeDoc, entries) {
|
|
1911
2209
|
if (!iframeDoc || !entries || !entries.length) return;
|
|
1912
|
-
|
|
1913
|
-
|
|
2210
|
+
beginSuppressIframeMutationDirty();
|
|
2211
|
+
try {
|
|
2212
|
+
for (var i = 0; i < entries.length; i++) {
|
|
2213
|
+
applyChangesetEntry(entries[i], iframeDoc);
|
|
2214
|
+
}
|
|
2215
|
+
} finally {
|
|
2216
|
+
endSuppressIframeMutationDirty();
|
|
1914
2217
|
}
|
|
1915
2218
|
}
|
|
1916
2219
|
|
|
@@ -1965,6 +2268,30 @@ function flushPendingGranularChangesets() {
|
|
|
1965
2268
|
scheduleGranularChangesetReapply();
|
|
1966
2269
|
}
|
|
1967
2270
|
|
|
2271
|
+
/** Stable key for structural changesets (insert/reorder) to avoid double-apply across early + full paint. */
|
|
2272
|
+
function structuralChangesetDedupKey(entry) {
|
|
2273
|
+
var nt = normalizeChangesetType(entry);
|
|
2274
|
+
if (!entry || (nt !== 'insert' && nt !== 'reorder')) return '';
|
|
2275
|
+
var vid = activeVarId || '';
|
|
2276
|
+
try {
|
|
2277
|
+
return (
|
|
2278
|
+
vid +
|
|
2279
|
+
'\0' +
|
|
2280
|
+
nt +
|
|
2281
|
+
'\0' +
|
|
2282
|
+
entry.selector +
|
|
2283
|
+
'\0' +
|
|
2284
|
+
String(entry.action || '') +
|
|
2285
|
+
'\0' +
|
|
2286
|
+
String(entry.html != null ? entry.html : '').slice(0, 240) +
|
|
2287
|
+
'\0' +
|
|
2288
|
+
String(entry.targetSelector || '')
|
|
2289
|
+
);
|
|
2290
|
+
} catch(_) {
|
|
2291
|
+
return vid + '\0' + nt + '\0' + entry.selector;
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
|
|
1968
2295
|
/**
|
|
1969
2296
|
* Apply a single changeset entry inside the editor iframe.
|
|
1970
2297
|
* Backend format: { selector, type: 'content'|'style'|'attribute'|'insert'|'remove', ... }
|
|
@@ -1984,21 +2311,34 @@ function applyChangesetEntry(entry, iframeDoc) {
|
|
|
1984
2311
|
return;
|
|
1985
2312
|
}
|
|
1986
2313
|
|
|
2314
|
+
var structuralDedupeKey = structuralChangesetDedupKey(entry);
|
|
2315
|
+
if (structuralDedupeKey && appliedStructuralChangesetKeys[structuralDedupeKey]) return;
|
|
2316
|
+
|
|
1987
2317
|
// \u2500\u2500 Standard granular changeset \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
|
|
1988
|
-
var el =
|
|
1989
|
-
try { el = iframeDoc.querySelector(entry.selector); } catch(_) {}
|
|
2318
|
+
var el = querySelectorResolved(iframeDoc, entry.selector);
|
|
1990
2319
|
if (!el) return;
|
|
1991
2320
|
|
|
1992
2321
|
captureChangesetSnapshotBeforeApply(entry, el, iframeDoc);
|
|
1993
2322
|
|
|
1994
|
-
switch (entry
|
|
2323
|
+
switch (normalizeChangesetType(entry)) {
|
|
1995
2324
|
case 'content':
|
|
1996
2325
|
if (entry.html != null) el.innerHTML = entry.html;
|
|
1997
2326
|
else if (entry.value != null) el.textContent = entry.value;
|
|
1998
2327
|
break;
|
|
1999
2328
|
case 'style':
|
|
2000
|
-
if (entry.property
|
|
2001
|
-
|
|
2329
|
+
if (entry.property) {
|
|
2330
|
+
var propKebab = entry.property;
|
|
2331
|
+
var cam = camelize(propKebab);
|
|
2332
|
+
if (entry.value == null || entry.value === '') {
|
|
2333
|
+
try { el.style.removeProperty(propKebab); } catch(_) {}
|
|
2334
|
+
try { if (cam in el.style) el.style[cam] = ''; } catch(__) {}
|
|
2335
|
+
} else {
|
|
2336
|
+
try {
|
|
2337
|
+
el.style.setProperty(propKebab, entry.value, 'important');
|
|
2338
|
+
} catch(_) {
|
|
2339
|
+
el.style[cam] = entry.value;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2002
2342
|
}
|
|
2003
2343
|
break;
|
|
2004
2344
|
case 'attribute':
|
|
@@ -2009,11 +2349,23 @@ function applyChangesetEntry(entry, iframeDoc) {
|
|
|
2009
2349
|
case 'insert': {
|
|
2010
2350
|
var pos = entry.action === 'before' ? 'beforebegin' : 'afterend';
|
|
2011
2351
|
if (entry.html) el.insertAdjacentHTML(pos, entry.html);
|
|
2352
|
+
if (structuralDedupeKey) appliedStructuralChangesetKeys[structuralDedupeKey] = true;
|
|
2012
2353
|
break;
|
|
2013
2354
|
}
|
|
2014
2355
|
case 'remove':
|
|
2015
2356
|
el.style.display = 'none';
|
|
2016
2357
|
break;
|
|
2358
|
+
case 'reorder': {
|
|
2359
|
+
var target = entry.targetSelector ? querySelectorResolved(iframeDoc, entry.targetSelector) : null;
|
|
2360
|
+
if (!target || !el.parentNode || !target.parentNode) break;
|
|
2361
|
+
if (entry.action === 'before') {
|
|
2362
|
+
target.parentNode.insertBefore(el, target);
|
|
2363
|
+
} else {
|
|
2364
|
+
target.parentNode.insertBefore(el, target.nextSibling);
|
|
2365
|
+
}
|
|
2366
|
+
if (structuralDedupeKey) appliedStructuralChangesetKeys[structuralDedupeKey] = true;
|
|
2367
|
+
break;
|
|
2368
|
+
}
|
|
2017
2369
|
default: break;
|
|
2018
2370
|
}
|
|
2019
2371
|
}
|
|
@@ -2028,19 +2380,24 @@ function applyActiveVariationHtml() {
|
|
|
2028
2380
|
var variation = variations.find(function(v) { return v._id === activeVarId; });
|
|
2029
2381
|
var cs = parseVariationChangesets(variation);
|
|
2030
2382
|
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2383
|
+
beginSuppressIframeMutationDirty();
|
|
2384
|
+
try {
|
|
2385
|
+
// If we have an in-session HTML snapshot, use it (user edited in this session)
|
|
2386
|
+
var saved = varHtmlCache[activeVarId];
|
|
2387
|
+
if (saved) {
|
|
2388
|
+
try { iframeDoc.body.innerHTML = saved; } catch(_) {}
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2037
2391
|
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2392
|
+
if (!cs.length) return;
|
|
2393
|
+
for (var i = 0; i < cs.length; i++) {
|
|
2394
|
+
applyChangesetEntry(cs[i], iframeDoc);
|
|
2395
|
+
}
|
|
2396
|
+
// Selectors often miss on first paint (client-rendered sections). Retry via timer + mutations.
|
|
2397
|
+
registerPendingGranularChangesets(cs, iframeDoc);
|
|
2398
|
+
} finally {
|
|
2399
|
+
endSuppressIframeMutationDirty();
|
|
2041
2400
|
}
|
|
2042
|
-
// Selectors often miss on first paint (client-rendered sections). Retry via timer + mutations.
|
|
2043
|
-
registerPendingGranularChangesets(cs, iframeDoc);
|
|
2044
2401
|
}
|
|
2045
2402
|
|
|
2046
2403
|
function changesetsHaveBodySnapshot(cs) {
|
|
@@ -2051,6 +2408,23 @@ function changesetsHaveBodySnapshot(cs) {
|
|
|
2051
2408
|
return false;
|
|
2052
2409
|
}
|
|
2053
2410
|
|
|
2411
|
+
/** Rows to persist for this variation on Finalize (same chain-set model as EditorShell \u2014 never __vvveb_body__). */
|
|
2412
|
+
function buildPersistedChainSetsForVariation(v) {
|
|
2413
|
+
if (!v || !v._id) return [];
|
|
2414
|
+
var parsed = parseVariationChangesets(v);
|
|
2415
|
+
var base = filterGranularChangesetEntries(parsed);
|
|
2416
|
+
var sessionExtra = sessionStructuralChainRowsByVarId[v._id] || [];
|
|
2417
|
+
if (v._id !== activeVarId) {
|
|
2418
|
+
return mergeGranularChainSets(base, sessionExtra);
|
|
2419
|
+
}
|
|
2420
|
+
var overlay = [];
|
|
2421
|
+
for (var si = 0; si < stateChanges.length; si++) {
|
|
2422
|
+
var row = stateChangeToChainSet(stateChanges[si]);
|
|
2423
|
+
if (row) overlay.push(row);
|
|
2424
|
+
}
|
|
2425
|
+
return mergeGranularChainSets(mergeGranularChainSets(base, sessionExtra), overlay);
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2054
2428
|
/**
|
|
2055
2429
|
* While document.readyState === 'loading', apply only granular changesets (no __vvveb_body__
|
|
2056
2430
|
* replacement) so the first painted nodes can receive edits before iframe/window load completes.
|
|
@@ -2061,10 +2435,15 @@ function applyVariationGranularOnly(iframeDoc) {
|
|
|
2061
2435
|
var variation = variations.find(function(v) { return v._id === activeVarId; });
|
|
2062
2436
|
var cs = parseVariationChangesets(variation);
|
|
2063
2437
|
if (!cs.length || changesetsHaveBodySnapshot(cs)) return;
|
|
2064
|
-
|
|
2065
|
-
|
|
2438
|
+
beginSuppressIframeMutationDirty();
|
|
2439
|
+
try {
|
|
2440
|
+
for (var i = 0; i < cs.length; i++) {
|
|
2441
|
+
applyChangesetEntry(cs[i], iframeDoc);
|
|
2442
|
+
}
|
|
2443
|
+
registerPendingGranularChangesets(cs, iframeDoc);
|
|
2444
|
+
} finally {
|
|
2445
|
+
endSuppressIframeMutationDirty();
|
|
2066
2446
|
}
|
|
2067
|
-
registerPendingGranularChangesets(cs, iframeDoc);
|
|
2068
2447
|
}
|
|
2069
2448
|
|
|
2070
2449
|
/** Re-try granular entries without resetting pending registration (use between poll ticks). */
|
|
@@ -2074,8 +2453,13 @@ function reapplyActiveVariationGranular(iframeDoc) {
|
|
|
2074
2453
|
var variation = variations.find(function(v) { return v._id === activeVarId; });
|
|
2075
2454
|
var cs = parseVariationChangesets(variation);
|
|
2076
2455
|
if (!cs.length || changesetsHaveBodySnapshot(cs)) return;
|
|
2077
|
-
|
|
2078
|
-
|
|
2456
|
+
beginSuppressIframeMutationDirty();
|
|
2457
|
+
try {
|
|
2458
|
+
for (var i = 0; i < cs.length; i++) {
|
|
2459
|
+
applyChangesetEntry(cs[i], iframeDoc);
|
|
2460
|
+
}
|
|
2461
|
+
} finally {
|
|
2462
|
+
endSuppressIframeMutationDirty();
|
|
2079
2463
|
}
|
|
2080
2464
|
}
|
|
2081
2465
|
|
|
@@ -2137,9 +2521,14 @@ function startIframeContentApplyWatcher(navGen) {
|
|
|
2137
2521
|
|
|
2138
2522
|
// \u2500\u2500 Element selection \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
|
|
2139
2523
|
function selectElement(el) {
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2524
|
+
beginSuppressIframeMutationDirty();
|
|
2525
|
+
try {
|
|
2526
|
+
if (selectedEl) { try { selectedEl.classList.remove('vve-selected'); } catch(_) {} }
|
|
2527
|
+
selectedEl = el;
|
|
2528
|
+
try { el.classList.add('vve-selected'); } catch(_) {}
|
|
2529
|
+
} finally {
|
|
2530
|
+
endSuppressIframeMutationDirty();
|
|
2531
|
+
}
|
|
2143
2532
|
document.getElementById('bc-path').textContent = buildSelector(el);
|
|
2144
2533
|
document.getElementById('bc-path').style.color = 'var(--accent-txt)';
|
|
2145
2534
|
document.getElementById('no-sel').style.display = 'none';
|
|
@@ -2154,7 +2543,12 @@ function selectElement(el) {
|
|
|
2154
2543
|
|
|
2155
2544
|
function deselectElement() {
|
|
2156
2545
|
setDragHandleActive(false);
|
|
2157
|
-
|
|
2546
|
+
beginSuppressIframeMutationDirty();
|
|
2547
|
+
try {
|
|
2548
|
+
if (selectedEl) { try { selectedEl.classList.remove('vve-selected'); } catch(_) {} selectedEl = null; }
|
|
2549
|
+
} finally {
|
|
2550
|
+
endSuppressIframeMutationDirty();
|
|
2551
|
+
}
|
|
2158
2552
|
document.getElementById('no-sel').style.display = '';
|
|
2159
2553
|
document.getElementById('el-info').style.display = 'none';
|
|
2160
2554
|
document.getElementById('rp-accordion').style.display = 'none';
|
|
@@ -2178,18 +2572,27 @@ function injectIframeSelectionStyles(doc) {
|
|
|
2178
2572
|
'html.vve-drag-armed .vve-selected{cursor:grab!important;}' +
|
|
2179
2573
|
'.vve-dragging{opacity:0.92!important;outline:2px dashed #f59e0b!important;' +
|
|
2180
2574
|
'outline-offset:2px!important;cursor:grabbing!important;box-shadow:none!important;}';
|
|
2181
|
-
|
|
2575
|
+
beginSuppressIframeMutationDirty();
|
|
2576
|
+
try {
|
|
2577
|
+
doc.head.appendChild(st);
|
|
2578
|
+
} finally {
|
|
2579
|
+
endSuppressIframeMutationDirty();
|
|
2580
|
+
}
|
|
2182
2581
|
}
|
|
2183
2582
|
|
|
2184
2583
|
function setDragHandleActive(on) {
|
|
2185
2584
|
dragHandleActive = !!on;
|
|
2186
2585
|
var b = document.getElementById('sf-drag');
|
|
2187
2586
|
if (b) b.classList.toggle('active', dragHandleActive);
|
|
2587
|
+
beginSuppressIframeMutationDirty();
|
|
2188
2588
|
try {
|
|
2189
2589
|
var iframe = document.getElementById('iframeId');
|
|
2190
2590
|
var d = iframe && iframe.contentDocument && iframe.contentDocument.documentElement;
|
|
2191
2591
|
if (d) d.classList.toggle('vve-drag-armed', dragHandleActive);
|
|
2192
|
-
} catch(_) {
|
|
2592
|
+
} catch(_) {
|
|
2593
|
+
} finally {
|
|
2594
|
+
endSuppressIframeMutationDirty();
|
|
2595
|
+
}
|
|
2193
2596
|
}
|
|
2194
2597
|
|
|
2195
2598
|
function positionSelectionToolbar() {
|
|
@@ -2259,34 +2662,72 @@ function selectElementFromTree(el) {
|
|
|
2259
2662
|
|
|
2260
2663
|
function duplicateSelectedEl() {
|
|
2261
2664
|
if (!selectedEl || !selectedEl.parentNode) return;
|
|
2665
|
+
var anchorSel = buildSelector(selectedEl);
|
|
2262
2666
|
var clone = selectedEl.cloneNode(true);
|
|
2263
2667
|
clone.classList.remove('vve-selected');
|
|
2264
2668
|
var all = clone.querySelectorAll ? clone.querySelectorAll('.vve-selected') : [];
|
|
2265
2669
|
for (var i = 0; i < all.length; i++) all[i].classList.remove('vve-selected');
|
|
2266
2670
|
clone.style.visibility = '';
|
|
2267
2671
|
if (clone.removeAttribute) clone.removeAttribute('data-vve-hidden');
|
|
2672
|
+
stripDataVveInstanceSubtree(clone);
|
|
2673
|
+
try {
|
|
2674
|
+
if (clone.id) clone.removeAttribute('id');
|
|
2675
|
+
} catch(_) {}
|
|
2676
|
+
try {
|
|
2677
|
+
clone.setAttribute('data-vve-instance', generateVveInstanceId());
|
|
2678
|
+
} catch(_) {}
|
|
2679
|
+
if (activeVarId) {
|
|
2680
|
+
appendSessionStructuralChainRow(activeVarId, {
|
|
2681
|
+
selector: anchorSel,
|
|
2682
|
+
type: 'insert',
|
|
2683
|
+
action: 'after',
|
|
2684
|
+
html: clone.outerHTML,
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2268
2687
|
selectedEl.parentNode.insertBefore(clone, selectedEl.nextSibling);
|
|
2269
|
-
|
|
2688
|
+
saveCurrentVariationHtml();
|
|
2689
|
+
recomputeEditorDirty();
|
|
2270
2690
|
scheduleDomTreeRefresh();
|
|
2271
2691
|
selectElement(clone);
|
|
2272
2692
|
}
|
|
2273
2693
|
|
|
2274
2694
|
function toggleHideSelectedEl() {
|
|
2275
2695
|
if (!selectedEl) return;
|
|
2696
|
+
var hidSel = buildSelector(selectedEl);
|
|
2276
2697
|
if (selectedEl.getAttribute('data-vve-hidden') === '1') {
|
|
2277
2698
|
selectedEl.style.visibility = '';
|
|
2278
2699
|
selectedEl.removeAttribute('data-vve-hidden');
|
|
2700
|
+
if (activeVarId) {
|
|
2701
|
+
appendSessionStructuralChainRow(activeVarId, {
|
|
2702
|
+
selector: hidSel,
|
|
2703
|
+
type: 'style',
|
|
2704
|
+
property: 'visibility',
|
|
2705
|
+
value: '',
|
|
2706
|
+
});
|
|
2707
|
+
}
|
|
2279
2708
|
} else {
|
|
2280
2709
|
selectedEl.style.visibility = 'hidden';
|
|
2281
2710
|
selectedEl.setAttribute('data-vve-hidden', '1');
|
|
2711
|
+
if (activeVarId) {
|
|
2712
|
+
appendSessionStructuralChainRow(activeVarId, {
|
|
2713
|
+
selector: hidSel,
|
|
2714
|
+
type: 'style',
|
|
2715
|
+
property: 'visibility',
|
|
2716
|
+
value: 'hidden',
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2282
2719
|
}
|
|
2283
|
-
|
|
2720
|
+
saveCurrentVariationHtml();
|
|
2721
|
+
recomputeEditorDirty();
|
|
2284
2722
|
}
|
|
2285
2723
|
|
|
2286
2724
|
function deleteSelectedEl() {
|
|
2287
2725
|
if (!selectedEl || !selectedEl.parentNode) return;
|
|
2726
|
+
var delSel = buildSelector(selectedEl);
|
|
2288
2727
|
selectedEl.remove();
|
|
2289
|
-
|
|
2728
|
+
if (activeVarId) appendSessionStructuralChainRow(activeVarId, { selector: delSel, type: 'remove' });
|
|
2729
|
+
saveCurrentVariationHtml();
|
|
2730
|
+
recomputeEditorDirty();
|
|
2290
2731
|
deselectElement();
|
|
2291
2732
|
scheduleDomTreeRefresh();
|
|
2292
2733
|
}
|
|
@@ -2578,14 +3019,12 @@ function renderImageSection(el) {
|
|
|
2578
3019
|
var prev = document.querySelector('.img-preview');
|
|
2579
3020
|
if (prev) prev.src = srcInp.value;
|
|
2580
3021
|
logChange(sel, 'pp-src', srcInp.value, el, orig);
|
|
2581
|
-
markDirty();
|
|
2582
3022
|
});
|
|
2583
3023
|
var altInp = document.getElementById('pp-img-alt');
|
|
2584
3024
|
if (altInp) altInp.addEventListener('input', function() {
|
|
2585
3025
|
var orig = getOriginalValue('pp-alt', el);
|
|
2586
3026
|
el.setAttribute('alt', altInp.value);
|
|
2587
3027
|
logChange(sel, 'pp-alt', altInp.value, el, orig);
|
|
2588
|
-
markDirty();
|
|
2589
3028
|
});
|
|
2590
3029
|
|
|
2591
3030
|
// Wire srcset entry inputs
|
|
@@ -2594,7 +3033,8 @@ function renderImageSection(el) {
|
|
|
2594
3033
|
var dInp = document.getElementById('pp-se-desc-'+i);
|
|
2595
3034
|
function flushSrcset() {
|
|
2596
3035
|
el.setAttribute('srcset', buildSrcset(_srcsetEntries));
|
|
2597
|
-
|
|
3036
|
+
saveCurrentVariationHtml();
|
|
3037
|
+
recomputeEditorDirty();
|
|
2598
3038
|
}
|
|
2599
3039
|
if (uInp) uInp.addEventListener('input', function(){ _srcsetEntries[i].url = uInp.value; flushSrcset(); });
|
|
2600
3040
|
if (dInp) dInp.addEventListener('input', function(){ _srcsetEntries[i].descriptor = dInp.value; flushSrcset(); });
|
|
@@ -2612,7 +3052,8 @@ function renderImageSection(el) {
|
|
|
2612
3052
|
}
|
|
2613
3053
|
function flushSizes() {
|
|
2614
3054
|
el.setAttribute('sizes', buildSizes(_sizesEntries));
|
|
2615
|
-
|
|
3055
|
+
saveCurrentVariationHtml();
|
|
3056
|
+
recomputeEditorDirty();
|
|
2616
3057
|
}
|
|
2617
3058
|
if (opInp) opInp.addEventListener('change', function(){ buildCondition(); flushSizes(); });
|
|
2618
3059
|
if (valInp) valInp.addEventListener('input', function(){ buildCondition(); flushSizes(); });
|
|
@@ -2626,7 +3067,12 @@ function addSrcsetEntry() {
|
|
|
2626
3067
|
}
|
|
2627
3068
|
function removeSrcsetEntry(i) {
|
|
2628
3069
|
_srcsetEntries.splice(i, 1);
|
|
2629
|
-
if (_imageEl) {
|
|
3070
|
+
if (_imageEl) {
|
|
3071
|
+
_imageEl.setAttribute('srcset', buildSrcset(_srcsetEntries));
|
|
3072
|
+
renderImageSection(_imageEl);
|
|
3073
|
+
saveCurrentVariationHtml();
|
|
3074
|
+
recomputeEditorDirty();
|
|
3075
|
+
}
|
|
2630
3076
|
}
|
|
2631
3077
|
function addSizesEntry() {
|
|
2632
3078
|
_sizesEntries.push({condition:'max-width: 760px', value:'760px'});
|
|
@@ -2634,7 +3080,12 @@ function addSizesEntry() {
|
|
|
2634
3080
|
}
|
|
2635
3081
|
function removeSizesEntry(i) {
|
|
2636
3082
|
_sizesEntries.splice(i, 1);
|
|
2637
|
-
if (_imageEl) {
|
|
3083
|
+
if (_imageEl) {
|
|
3084
|
+
_imageEl.setAttribute('sizes', buildSizes(_sizesEntries));
|
|
3085
|
+
renderImageSection(_imageEl);
|
|
3086
|
+
saveCurrentVariationHtml();
|
|
3087
|
+
recomputeEditorDirty();
|
|
3088
|
+
}
|
|
2638
3089
|
}
|
|
2639
3090
|
|
|
2640
3091
|
// \u2500\u2500 Right panel rendering (accordion) \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
|
|
@@ -2839,20 +3290,51 @@ function renderRightPanel(el) {
|
|
|
2839
3290
|
var orig = getOriginalValue(b[0], el);
|
|
2840
3291
|
b[1](inp.value);
|
|
2841
3292
|
logChange(sel, b[0], inp.value, el, orig);
|
|
2842
|
-
markDirty();
|
|
2843
3293
|
});
|
|
2844
3294
|
});
|
|
2845
3295
|
}
|
|
2846
3296
|
|
|
2847
3297
|
// \u2500\u2500 Selector helper \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
|
|
3298
|
+
function generateVveInstanceId() {
|
|
3299
|
+
return 'v' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
/** Editor-assigned clone marker so duplicated subtrees do not share the same CSS path as the original. */
|
|
3303
|
+
function stripDataVveInstanceSubtree(root) {
|
|
3304
|
+
if (!root || root.nodeType !== 1) return;
|
|
3305
|
+
try {
|
|
3306
|
+
root.removeAttribute('data-vve-instance');
|
|
3307
|
+
} catch(_) {}
|
|
3308
|
+
var sub = root.querySelectorAll ? root.querySelectorAll('[data-vve-instance]') : [];
|
|
3309
|
+
for (var i = 0; i < sub.length; i++) {
|
|
3310
|
+
try {
|
|
3311
|
+
sub[i].removeAttribute('data-vve-instance');
|
|
3312
|
+
} catch(_) {}
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
|
|
2848
3316
|
function buildSelector(el) {
|
|
2849
3317
|
if (!el) return '';
|
|
3318
|
+
var doc = el.ownerDocument || document;
|
|
3319
|
+
var inst = el.getAttribute && el.getAttribute('data-vve-instance');
|
|
3320
|
+
if (inst && String(inst).trim()) {
|
|
3321
|
+
var safe = String(inst).split('"').join('\\"');
|
|
3322
|
+
var attrSel = '[data-vve-instance="' + safe + '"]';
|
|
3323
|
+
try {
|
|
3324
|
+
if (doc.querySelectorAll(attrSel).length === 1) return attrSel;
|
|
3325
|
+
} catch(_) {}
|
|
3326
|
+
}
|
|
2850
3327
|
if (el.id) return '#' + el.id;
|
|
2851
3328
|
var parts = [], node = el, depth = 0;
|
|
2852
3329
|
while (node && node.nodeType === 1 && depth < 5) {
|
|
2853
3330
|
if (node.id) { parts.unshift('#' + node.id); break; }
|
|
2854
3331
|
var p = node.tagName.toLowerCase();
|
|
2855
|
-
if (node.classList && node.classList.length)
|
|
3332
|
+
if (node.classList && node.classList.length) {
|
|
3333
|
+
var clsArr = Array.from(node.classList).filter(function(c) {
|
|
3334
|
+
return c.indexOf('vve-') !== 0;
|
|
3335
|
+
});
|
|
3336
|
+
if (clsArr.length) p += '.' + clsArr.slice(0, 2).join('.');
|
|
3337
|
+
}
|
|
2856
3338
|
var idx = 1, sib = node.previousElementSibling;
|
|
2857
3339
|
while (sib) { if (sib.tagName === node.tagName) idx++; sib = sib.previousElementSibling; }
|
|
2858
3340
|
if (idx > 1) p += ':nth-of-type(' + idx + ')';
|
|
@@ -2863,6 +3345,71 @@ function buildSelector(el) {
|
|
|
2863
3345
|
return parts.join(' > ');
|
|
2864
3346
|
}
|
|
2865
3347
|
|
|
3348
|
+
/**
|
|
3349
|
+
* Strip editor-only .vve-* class tokens from a selector string (fixes DB rows saved while an element was selected).
|
|
3350
|
+
*/
|
|
3351
|
+
function sanitizeSelectorForMatch(sel) {
|
|
3352
|
+
if (!sel || typeof sel !== 'string') return '';
|
|
3353
|
+
var s0 = sel.replace(/.vve-[a-zA-Z0-9_-]+/gi, '');
|
|
3354
|
+
var parts = s0.split(/s*>s*/).map(function(seg) {
|
|
3355
|
+
var t = seg.replace(/.+/g, '.').replace(/.$/, '');
|
|
3356
|
+
return t.trim();
|
|
3357
|
+
});
|
|
3358
|
+
return parts.filter(Boolean).join(' > ');
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
/** Drop the rightmost :nth-of-type(n) (hydration / layout often shifts sibling indices). */
|
|
3362
|
+
function stripRightmostNthOfType(sel) {
|
|
3363
|
+
if (!sel || typeof sel !== 'string') return null;
|
|
3364
|
+
var idx = sel.lastIndexOf(':nth-of-type(');
|
|
3365
|
+
if (idx === -1) return null;
|
|
3366
|
+
var j = idx + ':nth-of-type('.length;
|
|
3367
|
+
while (j < sel.length && sel.charCodeAt(j) >= 48 && sel.charCodeAt(j) <= 57) j++;
|
|
3368
|
+
if (j >= sel.length || sel.charAt(j) !== ')') return null;
|
|
3369
|
+
return (sel.slice(0, idx) + sel.slice(j + 1))
|
|
3370
|
+
.replace(/s{2,}/g, ' ')
|
|
3371
|
+
.replace(/s*>s*>/g, ' >')
|
|
3372
|
+
.trim();
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
function querySelectorResolved(iframeDoc, selector) {
|
|
3376
|
+
if (!iframeDoc || !selector) return null;
|
|
3377
|
+
var seen = {};
|
|
3378
|
+
function tryOne(s) {
|
|
3379
|
+
if (!s || seen[s]) return null;
|
|
3380
|
+
seen[s] = true;
|
|
3381
|
+
try {
|
|
3382
|
+
return iframeDoc.querySelector(s) || null;
|
|
3383
|
+
} catch(_) {
|
|
3384
|
+
return null;
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
function walkRelax(base) {
|
|
3388
|
+
if (!base) return null;
|
|
3389
|
+
var el = tryOne(base);
|
|
3390
|
+
if (el) return el;
|
|
3391
|
+
var cur = base;
|
|
3392
|
+
for (var g = 0; g < 28; g++) {
|
|
3393
|
+
var nxt = stripRightmostNthOfType(cur);
|
|
3394
|
+
if (!nxt || nxt === cur) break;
|
|
3395
|
+
cur = nxt;
|
|
3396
|
+
el = tryOne(cur);
|
|
3397
|
+
if (el) return el;
|
|
3398
|
+
}
|
|
3399
|
+
return null;
|
|
3400
|
+
}
|
|
3401
|
+
var alt = sanitizeSelectorForMatch(selector);
|
|
3402
|
+
// Prefer sanitized + nth relax FIRST: raw selectors often still contain .vve-* from
|
|
3403
|
+
// save-time selection; those only match after clicking (we re-add vve-selected).
|
|
3404
|
+
var el = walkRelax(alt || selector);
|
|
3405
|
+
if (el) return el;
|
|
3406
|
+
if (alt !== selector) {
|
|
3407
|
+
el = walkRelax(selector);
|
|
3408
|
+
if (el) return el;
|
|
3409
|
+
}
|
|
3410
|
+
return null;
|
|
3411
|
+
}
|
|
3412
|
+
|
|
2866
3413
|
// \u2500\u2500 Iframe interaction \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
|
|
2867
3414
|
function repositionDragSibling(dragEl, clientY) {
|
|
2868
3415
|
var p = dragEl.parentElement;
|
|
@@ -2888,6 +3435,27 @@ function repositionDragSibling(dragEl, clientY) {
|
|
|
2888
3435
|
}
|
|
2889
3436
|
}
|
|
2890
3437
|
|
|
3438
|
+
function recordReorderAfterDrag(movedEl) {
|
|
3439
|
+
if (!activeVarId || !movedEl || !movedEl.parentElement) return;
|
|
3440
|
+
var prev = movedEl.previousElementSibling;
|
|
3441
|
+
var next = movedEl.nextElementSibling;
|
|
3442
|
+
if (prev) {
|
|
3443
|
+
appendSessionStructuralChainRow(activeVarId, {
|
|
3444
|
+
selector: buildSelector(movedEl),
|
|
3445
|
+
type: 'reorder',
|
|
3446
|
+
targetSelector: buildSelector(prev),
|
|
3447
|
+
action: 'after',
|
|
3448
|
+
});
|
|
3449
|
+
} else if (next) {
|
|
3450
|
+
appendSessionStructuralChainRow(activeVarId, {
|
|
3451
|
+
selector: buildSelector(movedEl),
|
|
3452
|
+
type: 'reorder',
|
|
3453
|
+
targetSelector: buildSelector(next),
|
|
3454
|
+
action: 'before',
|
|
3455
|
+
});
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
|
|
2891
3459
|
function attachDragReposition() {
|
|
2892
3460
|
try {
|
|
2893
3461
|
var iframe = document.getElementById('iframeId');
|
|
@@ -2941,7 +3509,9 @@ function attachDragReposition() {
|
|
|
2941
3509
|
} catch(_) {}
|
|
2942
3510
|
suppressClickUntil = Date.now() + 200;
|
|
2943
3511
|
setDragHandleActive(false);
|
|
2944
|
-
|
|
3512
|
+
if (activeVarId) recordReorderAfterDrag(selectedEl);
|
|
3513
|
+
saveCurrentVariationHtml();
|
|
3514
|
+
recomputeEditorDirty();
|
|
2945
3515
|
updateSelectionToolbar();
|
|
2946
3516
|
scheduleDomTreeRefresh();
|
|
2947
3517
|
}
|
|
@@ -2995,8 +3565,7 @@ function attachChangeObserver() {
|
|
|
2995
3565
|
changeObserverDoc = null;
|
|
2996
3566
|
}
|
|
2997
3567
|
changeObserver = new MutationObserver(function() {
|
|
2998
|
-
|
|
2999
|
-
// Debounced full rebuild of Elements panel as the live DOM grows / changes
|
|
3568
|
+
// Dirty state is derived from changesets baseline + stateChanges (not raw DOM mutations).
|
|
3000
3569
|
scheduleDomTreeRefresh();
|
|
3001
3570
|
scheduleGranularChangesetReapply();
|
|
3002
3571
|
});
|
|
@@ -3032,6 +3601,7 @@ function syncIframeInteractions(reason) {
|
|
|
3032
3601
|
var inp = document.getElementById('comp-search');
|
|
3033
3602
|
renderDomTree(inp ? inp.value : '');
|
|
3034
3603
|
updateSelectionToolbar();
|
|
3604
|
+
recomputeEditorDirty();
|
|
3035
3605
|
} catch(_) {}
|
|
3036
3606
|
}
|
|
3037
3607
|
|
|
@@ -3083,8 +3653,11 @@ function insertHtml(html) {
|
|
|
3083
3653
|
console.warn('[V2] insertHtml: iframe document not ready');
|
|
3084
3654
|
return;
|
|
3085
3655
|
}
|
|
3656
|
+
var htmlStr = String(html).trim();
|
|
3657
|
+
var anchorSel =
|
|
3658
|
+
selectedEl && selectedEl !== doc.body && selectedEl.parentNode ? buildSelector(selectedEl) : 'body';
|
|
3086
3659
|
var t = doc.createElement('template');
|
|
3087
|
-
t.innerHTML =
|
|
3660
|
+
t.innerHTML = htmlStr;
|
|
3088
3661
|
var frag = doc.createDocumentFragment();
|
|
3089
3662
|
var firstEl = null;
|
|
3090
3663
|
while (t.content.firstChild) {
|
|
@@ -3100,7 +3673,16 @@ function insertHtml(html) {
|
|
|
3100
3673
|
doc.body.appendChild(frag);
|
|
3101
3674
|
}
|
|
3102
3675
|
if (firstEl) selectElement(firstEl);
|
|
3103
|
-
|
|
3676
|
+
if (activeVarId) {
|
|
3677
|
+
appendSessionStructuralChainRow(activeVarId, {
|
|
3678
|
+
selector: anchorSel,
|
|
3679
|
+
type: 'insert',
|
|
3680
|
+
action: 'after',
|
|
3681
|
+
html: htmlStr,
|
|
3682
|
+
});
|
|
3683
|
+
}
|
|
3684
|
+
saveCurrentVariationHtml();
|
|
3685
|
+
recomputeEditorDirty();
|
|
3104
3686
|
scheduleDomTreeRefresh();
|
|
3105
3687
|
} catch(err) { console.warn('[V2] insertHtml:', err); }
|
|
3106
3688
|
}
|
|
@@ -3192,15 +3774,37 @@ document.getElementById('btn-close').addEventListener('click', handleClose);
|
|
|
3192
3774
|
function handleSave() {
|
|
3193
3775
|
saveCurrentVariationHtml();
|
|
3194
3776
|
var updatedVariations = variations.map(function(v) {
|
|
3195
|
-
var
|
|
3196
|
-
|
|
3197
|
-
|
|
3777
|
+
var prevParsed = parseVariationChangesets(v);
|
|
3778
|
+
var granularPrev = filterGranularChangesetEntries(prevParsed);
|
|
3779
|
+
var bodyOnlyLegacy = changesetsHaveBodySnapshot(prevParsed) && granularPrev.length === 0;
|
|
3780
|
+
|
|
3781
|
+
var rows = buildPersistedChainSetsForVariation(v);
|
|
3782
|
+
if (bodyOnlyLegacy && rows.length === 0) {
|
|
3783
|
+
return Object.assign({}, v);
|
|
3784
|
+
}
|
|
3785
|
+
var json = '[]';
|
|
3786
|
+
try {
|
|
3787
|
+
json = JSON.stringify(rows || []);
|
|
3788
|
+
} catch(_) {
|
|
3789
|
+
json = '[]';
|
|
3790
|
+
}
|
|
3791
|
+
return Object.assign({}, v, { changesets: json });
|
|
3198
3792
|
});
|
|
3793
|
+
variations = updatedVariations;
|
|
3794
|
+
varHtmlCache = {};
|
|
3795
|
+
sessionStructuralChainRowsByVarId = {};
|
|
3796
|
+
stateChanges = [];
|
|
3797
|
+
if (currentMainTab === 'states') renderStatesTab();
|
|
3798
|
+
captureBaselineFromVariations(variations);
|
|
3799
|
+
if (experimentData && Array.isArray(experimentData.variations)) {
|
|
3800
|
+
experimentData.variations = updatedVariations;
|
|
3801
|
+
}
|
|
3199
3802
|
send('save-experiment', { experimentId: experimentData ? experimentData.experimentId : null, variations: updatedVariations });
|
|
3200
|
-
|
|
3803
|
+
setEditorDirty(false);
|
|
3201
3804
|
}
|
|
3202
3805
|
|
|
3203
3806
|
function handleClose() {
|
|
3807
|
+
clearVisualEditorLocalStorage();
|
|
3204
3808
|
// Unsaved-changes UX lives in the parent (PlatformVisualEditorV2); avoid double confirm here.
|
|
3205
3809
|
send('close-editor', {});
|
|
3206
3810
|
}
|
|
@@ -3208,8 +3812,22 @@ function handleClose() {
|
|
|
3208
3812
|
// \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
|
|
3209
3813
|
document.addEventListener('keydown', function(e) {
|
|
3210
3814
|
var meta = e.metaKey || e.ctrlKey;
|
|
3211
|
-
if (meta && !e.shiftKey && e.key === 'z') {
|
|
3212
|
-
|
|
3815
|
+
if (meta && !e.shiftKey && e.key === 'z') {
|
|
3816
|
+
e.preventDefault();
|
|
3817
|
+
if (typeof Vvveb !== 'undefined' && Vvveb.Undo) {
|
|
3818
|
+
Vvveb.Undo.undo();
|
|
3819
|
+
saveCurrentVariationHtml();
|
|
3820
|
+
recomputeEditorDirty();
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
if (meta && e.shiftKey && e.key === 'z') {
|
|
3824
|
+
e.preventDefault();
|
|
3825
|
+
if (typeof Vvveb !== 'undefined' && Vvveb.Undo) {
|
|
3826
|
+
Vvveb.Undo.redo();
|
|
3827
|
+
saveCurrentVariationHtml();
|
|
3828
|
+
recomputeEditorDirty();
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3213
3831
|
if (meta && e.key === 's') { e.preventDefault(); handleSave(); }
|
|
3214
3832
|
if (e.key === 'Escape') {
|
|
3215
3833
|
var openTips = document.querySelectorAll('.ve-pl-tip.is-tip-open');
|