@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.js
CHANGED
|
@@ -842,6 +842,7 @@ select.pr-inp{cursor:pointer;background:#fff}
|
|
|
842
842
|
<script src="https://cdn.jsdelivr.net/gh/givanz/VvvebJs@master/libs/builder/inputs.js"></script>
|
|
843
843
|
<!-- components.js defines shared colour-class arrays used by bootstrap5/widgets components -->
|
|
844
844
|
<script src="https://cdn.jsdelivr.net/gh/givanz/VvvebJs@master/libs/builder/components.js"></script>
|
|
845
|
+
<script src="https://cdn.jsdelivr.net/gh/givanz/VvvebJs@master/libs/builder/components-html.js"></script>
|
|
845
846
|
<script>
|
|
846
847
|
/* Safety stub: if components.js didn't define these, create empty arrays so
|
|
847
848
|
components-bootstrap5.js doesn't throw ReferenceError on load. */
|
|
@@ -855,6 +856,18 @@ if (typeof colorSelectOptions === 'undefined') window.colorSelectOptions =
|
|
|
855
856
|
if (typeof textColorSelectOptions=== 'undefined') window.textColorSelectOptions= [];
|
|
856
857
|
if (typeof borderSelectOptions === 'undefined') window.borderSelectOptions = [];
|
|
857
858
|
if (typeof sizeSelectOptions === 'undefined') window.sizeSelectOptions = [];
|
|
859
|
+
if (window.Vvveb && window.Vvveb.Components) {
|
|
860
|
+
if (!window.Vvveb.ComponentsGroup) window.Vvveb.ComponentsGroup = {};
|
|
861
|
+
if (!window.Vvveb.ComponentsGroup['Bootstrap 5']) window.Vvveb.ComponentsGroup['Bootstrap 5'] = [];
|
|
862
|
+
try {
|
|
863
|
+
var baseExists =
|
|
864
|
+
window.Vvveb.Components._components &&
|
|
865
|
+
Object.prototype.hasOwnProperty.call(window.Vvveb.Components._components, '_base');
|
|
866
|
+
if (!baseExists && typeof window.Vvveb.Components.add === 'function') {
|
|
867
|
+
window.Vvveb.Components.add('_base', { name: 'Base', properties: [] });
|
|
868
|
+
}
|
|
869
|
+
} catch(_) {}
|
|
870
|
+
}
|
|
858
871
|
</script>
|
|
859
872
|
<script src="https://cdn.jsdelivr.net/gh/givanz/VvvebJs@master/libs/builder/components-bootstrap5.js"></script>
|
|
860
873
|
<script src="https://cdn.jsdelivr.net/gh/givanz/VvvebJs@master/libs/builder/components-widgets.js"></script>
|
|
@@ -906,6 +919,8 @@ var experimentData = null;
|
|
|
906
919
|
var variations = [];
|
|
907
920
|
var activeVarId = null;
|
|
908
921
|
var varHtmlCache = {};
|
|
922
|
+
/** Per-variation chain rows from structural actions (insert/duplicate/delete/reorder/hide), merged on Finalize. */
|
|
923
|
+
var sessionStructuralChainRowsByVarId = {};
|
|
909
924
|
/** Last iframe proxy URL we navigated to \u2014 used to skip redundant reloads when parent re-sends load-experiment */
|
|
910
925
|
var lastLoadedProxyUrl = '';
|
|
911
926
|
/** API changeset rows (excluding __vvveb_body__) reapplied until selectors match late-hydrated DOM */
|
|
@@ -919,6 +934,8 @@ var iframeContentNavGen = 0;
|
|
|
919
934
|
var iframeContentApplyTimer = null;
|
|
920
935
|
var iframeEarlyGranularPrimedForGen = null;
|
|
921
936
|
var iframeEarlySyncPrimedForGen = null;
|
|
937
|
+
/** insert/reorder entries are applied from early granular + full apply \u2014 skip exact duplicates per iframe nav */
|
|
938
|
+
var appliedStructuralChangesetKeys = {};
|
|
922
939
|
var isDirty = false;
|
|
923
940
|
var vvvebReady = false;
|
|
924
941
|
var currentMode = 'editor';
|
|
@@ -938,6 +955,8 @@ var selectionResizeBound = false;
|
|
|
938
955
|
var clickAttachDoc = null;
|
|
939
956
|
var changeObserver = null;
|
|
940
957
|
var changeObserverDoc = null;
|
|
958
|
+
/** Incremented while applying changesets / selection chrome so MutationObserver does not mark dirty */
|
|
959
|
+
var suppressIframeMutationDirty = 0;
|
|
941
960
|
/** { doc, onRS } \u2014 iframe document readystate until complete */
|
|
942
961
|
var iframeDocLoadingListeners = null;
|
|
943
962
|
// Each entry: {selector, label, cssProp, value, targetEl}
|
|
@@ -945,12 +964,89 @@ var iframeDocLoadingListeners = null;
|
|
|
945
964
|
var stateChanges = [];
|
|
946
965
|
/** Pre-apply DOM snapshots for granular + body changesets (used when removing History rows) */
|
|
947
966
|
var appliedChangesetSnapshots = {};
|
|
967
|
+
/** Canonical JSON fingerprints of persisted changesets per variation (last load / finalize) */
|
|
968
|
+
var baselineChangesetsByVarId = {};
|
|
969
|
+
|
|
970
|
+
// \u2500\u2500 Dirty tracking (compare DB baseline + session stateChanges vs current export) \u2500\u2500
|
|
971
|
+
function beginSuppressIframeMutationDirty() {
|
|
972
|
+
suppressIframeMutationDirty += 1;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function endSuppressIframeMutationDirty() {
|
|
976
|
+
suppressIframeMutationDirty = Math.max(0, suppressIframeMutationDirty - 1);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/** Stable stringify of a variation's changesets field (string or array from API). */
|
|
980
|
+
function fingerprintChangesetsField(raw) {
|
|
981
|
+
if (raw == null) return '[]';
|
|
982
|
+
if (Array.isArray(raw)) {
|
|
983
|
+
try {
|
|
984
|
+
return JSON.stringify(raw);
|
|
985
|
+
} catch(_) {
|
|
986
|
+
return '[]';
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
if (typeof raw !== 'string') return '[]';
|
|
990
|
+
var s = raw.trim();
|
|
991
|
+
if (!s) return '[]';
|
|
992
|
+
try {
|
|
993
|
+
var p = JSON.parse(s);
|
|
994
|
+
return JSON.stringify(Array.isArray(p) ? p : []);
|
|
995
|
+
} catch(_) {
|
|
996
|
+
return '[]';
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function captureBaselineFromVariations(list) {
|
|
1001
|
+
baselineChangesetsByVarId = {};
|
|
1002
|
+
if (!list || !list.length) return;
|
|
1003
|
+
for (var i = 0; i < list.length; i++) {
|
|
1004
|
+
var v = list[i];
|
|
1005
|
+
if (!v || !v._id) continue;
|
|
1006
|
+
baselineChangesetsByVarId[v._id] = fingerprintChangesetsField(v.changesets);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/** Fingerprint of what Finalize would send for this variation (matches buildPersistedChainSetsForVariation). */
|
|
1011
|
+
function persistedExportFingerprintForVariation(v) {
|
|
1012
|
+
if (!v || !v._id) return '[]';
|
|
1013
|
+
try {
|
|
1014
|
+
var rows = buildPersistedChainSetsForVariation(v);
|
|
1015
|
+
return JSON.stringify(rows || []);
|
|
1016
|
+
} catch(_) {
|
|
1017
|
+
return '[]';
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function setEditorDirty(dirty) {
|
|
1022
|
+
var was = isDirty;
|
|
1023
|
+
isDirty = !!dirty;
|
|
1024
|
+
var dot = document.getElementById('dirty-dot');
|
|
1025
|
+
if (dot) dot.classList.toggle('on', isDirty);
|
|
1026
|
+
if (isDirty && !was) send('mutations-changed', {});
|
|
1027
|
+
if (!isDirty && was) send('editor-dirty', { dirty: false });
|
|
1028
|
+
if (!isDirty) {
|
|
1029
|
+
savedAt = Date.now();
|
|
1030
|
+
updateSaveTime();
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
948
1033
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1034
|
+
function recomputeEditorDirty() {
|
|
1035
|
+
var d = stateChanges.length > 0;
|
|
1036
|
+
if (!d && variations && variations.length) {
|
|
1037
|
+
for (var i = 0; i < variations.length; i++) {
|
|
1038
|
+
var v = variations[i];
|
|
1039
|
+
var vid = v._id;
|
|
1040
|
+
var cur = persistedExportFingerprintForVariation(v);
|
|
1041
|
+
var base = baselineChangesetsByVarId[vid];
|
|
1042
|
+
if (base == null) base = '[]';
|
|
1043
|
+
if (cur !== base) {
|
|
1044
|
+
d = true;
|
|
1045
|
+
break;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
setEditorDirty(d);
|
|
954
1050
|
}
|
|
955
1051
|
var savedAt = null;
|
|
956
1052
|
function updateSaveTime() {
|
|
@@ -960,12 +1056,6 @@ function updateSaveTime() {
|
|
|
960
1056
|
el.textContent = s < 60 ? s + 's ago' : Math.floor(s / 60) + 'm ago';
|
|
961
1057
|
}
|
|
962
1058
|
setInterval(updateSaveTime, 10000);
|
|
963
|
-
function markClean() {
|
|
964
|
-
isDirty = false;
|
|
965
|
-
savedAt = Date.now();
|
|
966
|
-
document.getElementById('dirty-dot').classList.remove('on');
|
|
967
|
-
updateSaveTime();
|
|
968
|
-
}
|
|
969
1059
|
|
|
970
1060
|
// \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
|
|
971
1061
|
function setMode(mode) {
|
|
@@ -1142,6 +1232,7 @@ function logChange(selector, inputId, value, targetEl, originalValue) {
|
|
|
1142
1232
|
if (idx >= 0) { stateChanges[idx] = entry; } else { stateChanges.push(entry); }
|
|
1143
1233
|
}
|
|
1144
1234
|
if (currentMainTab === 'states') renderStatesTab();
|
|
1235
|
+
recomputeEditorDirty();
|
|
1145
1236
|
}
|
|
1146
1237
|
|
|
1147
1238
|
function renderStatesTab() {
|
|
@@ -1175,7 +1266,7 @@ function renderStatesTab() {
|
|
|
1175
1266
|
|
|
1176
1267
|
// Resolve a live DOM element for a state-change entry.
|
|
1177
1268
|
// Tries the stored direct reference first; if it's detached or missing,
|
|
1178
|
-
// falls back to querySelector(
|
|
1269
|
+
// falls back to querySelector (with .vve-* class stripped) inside the iframe document.
|
|
1179
1270
|
function resolveChangeEl(change) {
|
|
1180
1271
|
try {
|
|
1181
1272
|
var iframeDoc = document.getElementById('iframeId').contentDocument;
|
|
@@ -1185,7 +1276,7 @@ function resolveChangeEl(change) {
|
|
|
1185
1276
|
return change.targetEl;
|
|
1186
1277
|
}
|
|
1187
1278
|
// Fallback: re-query by the stored CSS selector
|
|
1188
|
-
return iframeDoc
|
|
1279
|
+
return querySelectorResolved(iframeDoc, change.selector);
|
|
1189
1280
|
} catch (e) {
|
|
1190
1281
|
console.warn('[V2] resolveChangeEl:', e);
|
|
1191
1282
|
return null;
|
|
@@ -1248,7 +1339,7 @@ function removeStateChange(idx) {
|
|
|
1248
1339
|
syncDesignInput(change);
|
|
1249
1340
|
stateChanges.splice(idx, 1);
|
|
1250
1341
|
renderStatesTab();
|
|
1251
|
-
|
|
1342
|
+
recomputeEditorDirty();
|
|
1252
1343
|
}
|
|
1253
1344
|
|
|
1254
1345
|
function clearAllStates() {
|
|
@@ -1258,7 +1349,7 @@ function clearAllStates() {
|
|
|
1258
1349
|
});
|
|
1259
1350
|
stateChanges = [];
|
|
1260
1351
|
renderStatesTab();
|
|
1261
|
-
|
|
1352
|
+
recomputeEditorDirty();
|
|
1262
1353
|
}
|
|
1263
1354
|
|
|
1264
1355
|
// \u2500\u2500 History tab (saved changesets from DB for active variation) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
@@ -1283,10 +1374,11 @@ function persistActiveVariationChangesets(arr) {
|
|
|
1283
1374
|
|
|
1284
1375
|
function entrySnapshotKey(entry) {
|
|
1285
1376
|
if (!entry || !entry.selector) return '';
|
|
1377
|
+
var selKey = sanitizeSelectorForMatch(entry.selector) || entry.selector;
|
|
1286
1378
|
return (
|
|
1287
|
-
|
|
1379
|
+
selKey +
|
|
1288
1380
|
'\0' +
|
|
1289
|
-
(entry
|
|
1381
|
+
normalizeChangesetType(entry) +
|
|
1290
1382
|
'\0' +
|
|
1291
1383
|
String(entry.property || '') +
|
|
1292
1384
|
'\0' +
|
|
@@ -1304,7 +1396,7 @@ function captureChangesetSnapshotBeforeApply(entry, el, iframeDoc) {
|
|
|
1304
1396
|
if (!entry || !el || entry.selector === '__vvveb_body__') return;
|
|
1305
1397
|
var k = entrySnapshotKey(entry);
|
|
1306
1398
|
if (appliedChangesetSnapshots[k]) return;
|
|
1307
|
-
switch (entry
|
|
1399
|
+
switch (normalizeChangesetType(entry)) {
|
|
1308
1400
|
case 'content':
|
|
1309
1401
|
if (entry.html != null) {
|
|
1310
1402
|
appliedChangesetSnapshots[k] = { kind: 'innerHTML', v: el.innerHTML };
|
|
@@ -1372,10 +1464,7 @@ function revertChangesetEntryOnDom(entry) {
|
|
|
1372
1464
|
if (!iframeDoc) return false;
|
|
1373
1465
|
var k = entrySnapshotKey(entry);
|
|
1374
1466
|
var snap = appliedChangesetSnapshots[k];
|
|
1375
|
-
var el =
|
|
1376
|
-
try {
|
|
1377
|
-
el = iframeDoc.querySelector(entry.selector);
|
|
1378
|
-
} catch(_) {}
|
|
1467
|
+
var el = querySelectorResolved(iframeDoc, entry.selector);
|
|
1379
1468
|
if (!snap || !el) {
|
|
1380
1469
|
softReloadEditorIframe();
|
|
1381
1470
|
delete appliedChangesetSnapshots[k];
|
|
@@ -1402,7 +1491,7 @@ function revertChangesetEntryOnDom(entry) {
|
|
|
1402
1491
|
function historyEntryTypeLabel(entry) {
|
|
1403
1492
|
if (!entry) return 'Change';
|
|
1404
1493
|
if (entry.selector === '__vvveb_body__') return 'Full page HTML';
|
|
1405
|
-
var t = (entry
|
|
1494
|
+
var t = normalizeChangesetType(entry);
|
|
1406
1495
|
if (t === 'content') return entry.html != null ? 'Inner HTML' : 'Text / content';
|
|
1407
1496
|
if (t === 'style') return 'Style: ' + (entry.property || '');
|
|
1408
1497
|
if (t === 'attribute') return 'Attribute: ' + (entry.attribute || '');
|
|
@@ -1416,7 +1505,8 @@ function historyEntryValuePreview(entry) {
|
|
|
1416
1505
|
if (entry.selector === '__vvveb_body__') return '(body snapshot)';
|
|
1417
1506
|
if (entry.html != null) return String(entry.html).slice(0, 120);
|
|
1418
1507
|
if (entry.value != null) return String(entry.value).slice(0, 120);
|
|
1419
|
-
|
|
1508
|
+
var nt = normalizeChangesetType(entry);
|
|
1509
|
+
if (nt === 'style' || nt === 'attribute') return String(entry.value != null ? entry.value : '').slice(0, 120);
|
|
1420
1510
|
return '';
|
|
1421
1511
|
}
|
|
1422
1512
|
|
|
@@ -1450,6 +1540,9 @@ function renderHistoryTab() {
|
|
|
1450
1540
|
var val = historyEntryValuePreview(item.entry);
|
|
1451
1541
|
html +=
|
|
1452
1542
|
'<div class="state-item">' +
|
|
1543
|
+
'<span class="state-item-idx" style="opacity:0.55;font-size:10px;min-width:2.2em;display:inline-block" title="Order in saved changesets">#' +
|
|
1544
|
+
item.idx +
|
|
1545
|
+
'</span>' +
|
|
1453
1546
|
'<span class="state-item-label">' +
|
|
1454
1547
|
esc(lab) +
|
|
1455
1548
|
'</span>' +
|
|
@@ -1458,7 +1551,9 @@ function renderHistoryTab() {
|
|
|
1458
1551
|
'">' +
|
|
1459
1552
|
esc(val) +
|
|
1460
1553
|
'</span>' +
|
|
1461
|
-
'<button type="button" class="state-remove" title="Remove
|
|
1554
|
+
'<button type="button" class="state-remove" title="Remove this saved row (#' +
|
|
1555
|
+
item.idx +
|
|
1556
|
+
')" onclick="removeHistoryChangeset(' +
|
|
1462
1557
|
item.idx +
|
|
1463
1558
|
')">✕</button>' +
|
|
1464
1559
|
'</div>';
|
|
@@ -1468,6 +1563,16 @@ function renderHistoryTab() {
|
|
|
1468
1563
|
container.innerHTML = html;
|
|
1469
1564
|
}
|
|
1470
1565
|
|
|
1566
|
+
function changesetListHasStructural(arr) {
|
|
1567
|
+
if (!arr || !arr.length) return false;
|
|
1568
|
+
for (var i = 0; i < arr.length; i++) {
|
|
1569
|
+
var e = arr[i];
|
|
1570
|
+
var t = normalizeChangesetType(e);
|
|
1571
|
+
if (e && (t === 'insert' || t === 'reorder')) return true;
|
|
1572
|
+
}
|
|
1573
|
+
return false;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1471
1576
|
function removeHistoryChangeset(idx) {
|
|
1472
1577
|
var v = getActiveVariationForHistory();
|
|
1473
1578
|
if (!v) return;
|
|
@@ -1480,18 +1585,31 @@ function removeHistoryChangeset(idx) {
|
|
|
1480
1585
|
try {
|
|
1481
1586
|
delete varHtmlCache[activeVarId];
|
|
1482
1587
|
} catch(_) {}
|
|
1483
|
-
|
|
1588
|
+
// Re-applying remaining rows on top of current DOM duplicates insert/reorder nodes; reload when any
|
|
1589
|
+
// structural row remains or was removed (revert may already have started a reload for insert/body).
|
|
1590
|
+
var removedType = normalizeChangesetType(removed);
|
|
1591
|
+
var needsStructuralReload =
|
|
1592
|
+
!didReload &&
|
|
1593
|
+
(removedType === 'insert' ||
|
|
1594
|
+
removedType === 'reorder' ||
|
|
1595
|
+
changesetListHasStructural(arr));
|
|
1596
|
+
if (didReload) {
|
|
1597
|
+
/* revertChangesetEntryOnDom already kicked off iframe reload */
|
|
1598
|
+
} else if (needsStructuralReload) {
|
|
1599
|
+
softReloadEditorIframe();
|
|
1600
|
+
} else {
|
|
1484
1601
|
try {
|
|
1602
|
+
appliedStructuralChangesetKeys = {};
|
|
1485
1603
|
applyActiveVariationHtml();
|
|
1486
1604
|
registerPendingGranularChangesets(
|
|
1487
1605
|
arr,
|
|
1488
1606
|
document.getElementById('iframeId').contentDocument,
|
|
1489
1607
|
);
|
|
1608
|
+
saveCurrentVariationHtml();
|
|
1490
1609
|
} catch(_) {}
|
|
1491
|
-
saveCurrentVariationHtml();
|
|
1492
1610
|
}
|
|
1493
1611
|
if (currentMainTab === 'history') renderHistoryTab();
|
|
1494
|
-
|
|
1612
|
+
recomputeEditorDirty();
|
|
1495
1613
|
scheduleDomTreeRefresh();
|
|
1496
1614
|
}
|
|
1497
1615
|
|
|
@@ -1501,15 +1619,82 @@ function clearAllHistoryChangesets() {
|
|
|
1501
1619
|
if (!parseVariationChangesets(v).length) return;
|
|
1502
1620
|
persistActiveVariationChangesets([]);
|
|
1503
1621
|
appliedChangesetSnapshots = {};
|
|
1622
|
+
appliedStructuralChangesetKeys = {};
|
|
1504
1623
|
try {
|
|
1505
1624
|
delete varHtmlCache[activeVarId];
|
|
1506
1625
|
} catch(_) {}
|
|
1507
1626
|
if (currentMainTab === 'history') renderHistoryTab();
|
|
1508
|
-
|
|
1627
|
+
recomputeEditorDirty();
|
|
1509
1628
|
scheduleDomTreeRefresh();
|
|
1510
1629
|
softReloadEditorIframe();
|
|
1511
1630
|
}
|
|
1512
1631
|
|
|
1632
|
+
// \u2500\u2500 Persisted active variation (survives iframe / full page reload) \u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1633
|
+
/** All Visual Editor iframe keys in localStorage use this prefix \u2014 cleared on close. */
|
|
1634
|
+
var VVE_LOCAL_STORAGE_PREFIX = 'vve:';
|
|
1635
|
+
|
|
1636
|
+
function clearVisualEditorLocalStorage() {
|
|
1637
|
+
try {
|
|
1638
|
+
for (var i = localStorage.length - 1; i >= 0; i--) {
|
|
1639
|
+
var k = localStorage.key(i);
|
|
1640
|
+
if (k && k.indexOf(VVE_LOCAL_STORAGE_PREFIX) === 0) {
|
|
1641
|
+
localStorage.removeItem(k);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
} catch(_) {}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
function activeVariationStorageKeyFromPayload(data) {
|
|
1648
|
+
return (
|
|
1649
|
+
VVE_LOCAL_STORAGE_PREFIX +
|
|
1650
|
+
'activeVar:' +
|
|
1651
|
+
String((data && data.experimentId) || '') +
|
|
1652
|
+
':' +
|
|
1653
|
+
String((data && data.pageUrl) || '')
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
function readPersistedActiveVariationId(data) {
|
|
1658
|
+
try {
|
|
1659
|
+
var sk = activeVariationStorageKeyFromPayload(data);
|
|
1660
|
+
if (sk === VVE_LOCAL_STORAGE_PREFIX + 'activeVar::') return null;
|
|
1661
|
+
return localStorage.getItem(sk);
|
|
1662
|
+
} catch(_) {
|
|
1663
|
+
return null;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
function writePersistedActiveVariationId(varId) {
|
|
1668
|
+
try {
|
|
1669
|
+
if (!experimentData || !experimentData.experimentId) return;
|
|
1670
|
+
var sk =
|
|
1671
|
+
VVE_LOCAL_STORAGE_PREFIX +
|
|
1672
|
+
'activeVar:' +
|
|
1673
|
+
String(experimentData.experimentId || '') +
|
|
1674
|
+
':' +
|
|
1675
|
+
String(experimentData.pageUrl || '');
|
|
1676
|
+
if (sk === VVE_LOCAL_STORAGE_PREFIX + 'activeVar::') return;
|
|
1677
|
+
if (varId) localStorage.setItem(sk, String(varId));
|
|
1678
|
+
} catch(_) {}
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
/**
|
|
1682
|
+
* @param allowPrevMemory when true, keep in-session activeVarId if still valid (skip-reload path).
|
|
1683
|
+
*/
|
|
1684
|
+
function pickActiveVariationIdForLoad(data, variationsArr, prevMemoryId, allowPrevMemory) {
|
|
1685
|
+
var baseline = variationsArr.find(function(v) { return v.baseline; });
|
|
1686
|
+
var fallback = (baseline || variationsArr[0] || {})._id || null;
|
|
1687
|
+
if (!variationsArr.length) return null;
|
|
1688
|
+
if (allowPrevMemory && prevMemoryId && variationsArr.some(function(v) { return v._id === prevMemoryId; })) {
|
|
1689
|
+
return prevMemoryId;
|
|
1690
|
+
}
|
|
1691
|
+
var stored = readPersistedActiveVariationId(data);
|
|
1692
|
+
if (stored && variationsArr.some(function(v) { return v._id === stored; })) {
|
|
1693
|
+
return stored;
|
|
1694
|
+
}
|
|
1695
|
+
return fallback;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1513
1698
|
// \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
|
|
1514
1699
|
function handleLoadExperiment(data) {
|
|
1515
1700
|
clearPendingGranularChangesets();
|
|
@@ -1537,10 +1722,8 @@ function handleLoadExperiment(data) {
|
|
|
1537
1722
|
experimentData = data;
|
|
1538
1723
|
variations = Array.isArray(data.variations) ? data.variations : [];
|
|
1539
1724
|
var prevActive = activeVarId;
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
activeVarId =
|
|
1543
|
-
prevActive && variations.some(function(v) { return v._id === prevActive; }) ? prevActive : fallback;
|
|
1725
|
+
activeVarId = pickActiveVariationIdForLoad(data, variations, prevActive, true);
|
|
1726
|
+
writePersistedActiveVariationId(activeVarId);
|
|
1544
1727
|
renderVariationTabs();
|
|
1545
1728
|
var urlBarSkip = document.getElementById('url-bar');
|
|
1546
1729
|
urlBarSkip.textContent = pageUrl;
|
|
@@ -1554,24 +1737,30 @@ function handleLoadExperiment(data) {
|
|
|
1554
1737
|
if (ifrSkip && ifrSkip.contentDocument) attachIframeLoadingUntilComplete(ifrSkip);
|
|
1555
1738
|
} catch(_) {}
|
|
1556
1739
|
if (currentMainTab === 'history') renderHistoryTab();
|
|
1740
|
+
captureBaselineFromVariations(variations);
|
|
1741
|
+
recomputeEditorDirty();
|
|
1557
1742
|
return;
|
|
1558
1743
|
}
|
|
1559
1744
|
|
|
1560
1745
|
if (!experimentData || prevKey !== nextKey) {
|
|
1561
1746
|
varHtmlCache = {};
|
|
1747
|
+
sessionStructuralChainRowsByVarId = {};
|
|
1562
1748
|
appliedChangesetSnapshots = {};
|
|
1749
|
+
appliedStructuralChangesetKeys = {};
|
|
1563
1750
|
}
|
|
1564
1751
|
experimentData = data;
|
|
1565
1752
|
variations = Array.isArray(data.variations) ? data.variations : [];
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
activeVarId
|
|
1753
|
+
var sameExpPage = prevKey === nextKey;
|
|
1754
|
+
activeVarId = pickActiveVariationIdForLoad(data, variations, activeVarId, sameExpPage);
|
|
1755
|
+
writePersistedActiveVariationId(activeVarId);
|
|
1569
1756
|
renderVariationTabs();
|
|
1570
1757
|
|
|
1571
1758
|
var urlBar = document.getElementById('url-bar');
|
|
1572
1759
|
urlBar.textContent = pageUrl;
|
|
1573
1760
|
urlBar.title = pageUrl;
|
|
1574
1761
|
if (currentMainTab === 'history') renderHistoryTab();
|
|
1762
|
+
captureBaselineFromVariations(variations);
|
|
1763
|
+
recomputeEditorDirty();
|
|
1575
1764
|
loadPage(proxyUrl);
|
|
1576
1765
|
}
|
|
1577
1766
|
|
|
@@ -1671,9 +1860,7 @@ function granularAnySelectorMatches(doc, cs) {
|
|
|
1671
1860
|
if (!doc || !cs || !cs.length) return false;
|
|
1672
1861
|
var g = filterGranularChangesetEntries(cs);
|
|
1673
1862
|
for (var i = 0; i < g.length; i++) {
|
|
1674
|
-
|
|
1675
|
-
if (doc.querySelector(g[i].selector)) return true;
|
|
1676
|
-
} catch(_) {}
|
|
1863
|
+
if (querySelectorResolved(doc, g[i].selector)) return true;
|
|
1677
1864
|
}
|
|
1678
1865
|
return false;
|
|
1679
1866
|
}
|
|
@@ -1694,7 +1881,7 @@ function appendIframeReloadBust(url) {
|
|
|
1694
1881
|
// applying variation changesets there is then wiped when the real navigation commits.
|
|
1695
1882
|
function iframeDocMatchesNavigatedSrc(iframe, doc) {
|
|
1696
1883
|
if (!iframe || !doc) return false;
|
|
1697
|
-
var src = iframe.src || iframe.
|
|
1884
|
+
var src = iframe.getAttribute('src') || iframe.src || '';
|
|
1698
1885
|
if (!src || src === 'about:blank') return false;
|
|
1699
1886
|
var loc = '';
|
|
1700
1887
|
try {
|
|
@@ -1703,12 +1890,28 @@ function iframeDocMatchesNavigatedSrc(iframe, doc) {
|
|
|
1703
1890
|
return false;
|
|
1704
1891
|
}
|
|
1705
1892
|
if (!loc || loc === 'about:blank') return false;
|
|
1706
|
-
var rmSrc = src.match(/__ve_reload=([0-9]+)/);
|
|
1707
|
-
if (rmSrc) return loc.indexOf('__ve_reload=' + rmSrc[1]) !== -1;
|
|
1708
1893
|
try {
|
|
1709
1894
|
var base = window.location.href;
|
|
1710
1895
|
var su = new URL(src, base);
|
|
1896
|
+
if (su.searchParams && su.searchParams.has('__ve_reload')) {
|
|
1897
|
+
su.searchParams.delete('__ve_reload');
|
|
1898
|
+
}
|
|
1711
1899
|
var du = new URL(loc, base);
|
|
1900
|
+
if (du.searchParams && du.searchParams.has('__ve_reload')) {
|
|
1901
|
+
du.searchParams.delete('__ve_reload');
|
|
1902
|
+
}
|
|
1903
|
+
// Same-origin proxy that keeps document address aligned with iframe src
|
|
1904
|
+
if (su.origin === du.origin && su.pathname + su.search === du.pathname + du.search) {
|
|
1905
|
+
return true;
|
|
1906
|
+
}
|
|
1907
|
+
// conversion-proxy: iframe src stays on our app, but doc.URL is usually the target site after redirects
|
|
1908
|
+
var p = su.pathname || '';
|
|
1909
|
+
var isRootProxyPath = p === '/api/conversion-proxy' || p.indexOf('/api/conversion-proxy/') === 0;
|
|
1910
|
+
var isNestedMalformedProxy = !isRootProxyPath && p.indexOf('api/conversion-proxy') !== -1;
|
|
1911
|
+
if (isNestedMalformedProxy) return false;
|
|
1912
|
+
if (isRootProxyPath || String(su.href).indexOf('conversion-proxy') !== -1) {
|
|
1913
|
+
return doc === iframe.contentDocument;
|
|
1914
|
+
}
|
|
1712
1915
|
return su.pathname + su.search === du.pathname + du.search;
|
|
1713
1916
|
} catch(_) {
|
|
1714
1917
|
return false;
|
|
@@ -1741,6 +1944,7 @@ function resetIframeBindings() {
|
|
|
1741
1944
|
detachIframeLoadingListeners();
|
|
1742
1945
|
stopIframeContentApplyWatcher();
|
|
1743
1946
|
appliedChangesetSnapshots = {};
|
|
1947
|
+
appliedStructuralChangesetKeys = {};
|
|
1744
1948
|
clickAttachDoc = null;
|
|
1745
1949
|
dragAttachDoc = null;
|
|
1746
1950
|
changeObserverDoc = null;
|
|
@@ -1811,13 +2015,19 @@ function switchVariation(varId) {
|
|
|
1811
2015
|
saveCurrentVariationHtml();
|
|
1812
2016
|
clearPendingGranularChangesets();
|
|
1813
2017
|
activeVarId = varId;
|
|
2018
|
+
writePersistedActiveVariationId(varId);
|
|
1814
2019
|
renderVariationTabs();
|
|
1815
2020
|
deselectElement();
|
|
1816
2021
|
try {
|
|
1817
2022
|
var iframe = document.getElementById('iframeId');
|
|
1818
2023
|
var saved = varHtmlCache[varId];
|
|
1819
2024
|
if (saved) {
|
|
1820
|
-
|
|
2025
|
+
beginSuppressIframeMutationDirty();
|
|
2026
|
+
try {
|
|
2027
|
+
iframe.contentDocument.body.innerHTML = saved;
|
|
2028
|
+
} finally {
|
|
2029
|
+
endSuppressIframeMutationDirty();
|
|
2030
|
+
}
|
|
1821
2031
|
detachIframeLoadingListeners();
|
|
1822
2032
|
setIframePageLoadingUi(false);
|
|
1823
2033
|
syncIframeInteractions('switch-variation-cache');
|
|
@@ -1840,6 +2050,7 @@ function switchVariation(varId) {
|
|
|
1840
2050
|
}
|
|
1841
2051
|
} catch(_) {}
|
|
1842
2052
|
if (currentMainTab === 'history') renderHistoryTab();
|
|
2053
|
+
recomputeEditorDirty();
|
|
1843
2054
|
}
|
|
1844
2055
|
|
|
1845
2056
|
function saveCurrentVariationHtml() {
|
|
@@ -1878,31 +2089,123 @@ function parseVariationChangesets(variation) {
|
|
|
1878
2089
|
}
|
|
1879
2090
|
}
|
|
1880
2091
|
|
|
2092
|
+
/** Lowercase entry.type so persisted / API rows match switches (e.g. Insert vs insert). */
|
|
2093
|
+
function normalizeChangesetType(entry) {
|
|
2094
|
+
return String(entry && entry.type != null ? entry.type : '').toLowerCase();
|
|
2095
|
+
}
|
|
2096
|
+
|
|
1881
2097
|
function filterGranularChangesetEntries(cs) {
|
|
1882
2098
|
if (!cs || !cs.length) return [];
|
|
1883
2099
|
var out = [];
|
|
1884
2100
|
for (var i = 0; i < cs.length; i++) {
|
|
1885
2101
|
var e = cs[i];
|
|
1886
|
-
if (e
|
|
2102
|
+
if (!e || !e.selector || e.selector === '__vvveb_body__') continue;
|
|
2103
|
+
out.push(e);
|
|
1887
2104
|
}
|
|
1888
2105
|
return out;
|
|
1889
2106
|
}
|
|
1890
2107
|
|
|
2108
|
+
/** Dedup key for merging chain-set rows (overlay wins over base). */
|
|
2109
|
+
function chainSetDedupKey(entry) {
|
|
2110
|
+
if (!entry || !entry.selector) return '';
|
|
2111
|
+
var t = normalizeChangesetType(entry);
|
|
2112
|
+
if (t === 'style') return entry.selector + '|s|' + String(entry.property || '');
|
|
2113
|
+
if (t === 'content') return entry.selector + '|c|' + (entry.html != null ? 'h' : 't');
|
|
2114
|
+
if (t === 'attribute') return entry.selector + '|a|' + String(entry.attribute || '');
|
|
2115
|
+
if (t === 'insert') return entry.selector + '|i|' + String(entry.action || '') + '|' + String(entry.html || '').slice(0, 120);
|
|
2116
|
+
if (t === 'remove') return entry.selector + '|r|';
|
|
2117
|
+
if (t === 'reorder') {
|
|
2118
|
+
return entry.selector + '|ro|' + String(entry.targetSelector || '') + '|' + String(entry.action || '');
|
|
2119
|
+
}
|
|
2120
|
+
try {
|
|
2121
|
+
return entry.selector + '|' + t + '|' + JSON.stringify(entry);
|
|
2122
|
+
} catch(_) {
|
|
2123
|
+
return entry.selector + '|' + t;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
function mergeGranularChainSets(baseList, overlayList) {
|
|
2128
|
+
var map = {};
|
|
2129
|
+
var order = [];
|
|
2130
|
+
function ingest(arr) {
|
|
2131
|
+
if (!arr || !arr.length) return;
|
|
2132
|
+
for (var i = 0; i < arr.length; i++) {
|
|
2133
|
+
var e = arr[i];
|
|
2134
|
+
if (!e || !e.selector) continue;
|
|
2135
|
+
var k = chainSetDedupKey(e);
|
|
2136
|
+
if (!map[k]) order.push(k);
|
|
2137
|
+
map[k] = e;
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
ingest(baseList);
|
|
2141
|
+
ingest(overlayList);
|
|
2142
|
+
var out = [];
|
|
2143
|
+
for (var j = 0; j < order.length; j++) {
|
|
2144
|
+
out.push(map[order[j]]);
|
|
2145
|
+
}
|
|
2146
|
+
return out;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
function appendSessionStructuralChainRow(varId, row) {
|
|
2150
|
+
if (!varId || !row) return;
|
|
2151
|
+
if (!sessionStructuralChainRowsByVarId[varId]) sessionStructuralChainRowsByVarId[varId] = [];
|
|
2152
|
+
sessionStructuralChainRowsByVarId[varId].push(row);
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
/** One States-tab row -> Conversion.io chain-set shape (matches applyChangesetEntry). */
|
|
2156
|
+
function stateChangeToChainSet(c) {
|
|
2157
|
+
if (!c || !c.selector) return null;
|
|
2158
|
+
if (c.cssProp) {
|
|
2159
|
+
return { selector: c.selector, type: 'style', property: c.cssProp, value: c.value };
|
|
2160
|
+
}
|
|
2161
|
+
switch (c.inputId) {
|
|
2162
|
+
case 'pp-text':
|
|
2163
|
+
return { selector: c.selector, type: 'content', value: c.value };
|
|
2164
|
+
case 'pp-html':
|
|
2165
|
+
return { selector: c.selector, type: 'content', html: c.value };
|
|
2166
|
+
case 'pp-cls':
|
|
2167
|
+
return { selector: c.selector, type: 'attribute', attribute: 'class', value: c.value };
|
|
2168
|
+
case 'pp-id':
|
|
2169
|
+
return { selector: c.selector, type: 'attribute', attribute: 'id', value: c.value };
|
|
2170
|
+
case 'pp-href':
|
|
2171
|
+
return { selector: c.selector, type: 'attribute', attribute: 'href', value: c.value };
|
|
2172
|
+
case 'pp-target':
|
|
2173
|
+
return { selector: c.selector, type: 'attribute', attribute: 'target', value: c.value };
|
|
2174
|
+
case 'pp-src':
|
|
2175
|
+
return { selector: c.selector, type: 'attribute', attribute: 'src', value: c.value };
|
|
2176
|
+
case 'pp-alt':
|
|
2177
|
+
return { selector: c.selector, type: 'attribute', attribute: 'alt', value: c.value };
|
|
2178
|
+
case 'pp-ph':
|
|
2179
|
+
return { selector: c.selector, type: 'attribute', attribute: 'placeholder', value: c.value };
|
|
2180
|
+
case 'pp-css':
|
|
2181
|
+
return { selector: c.selector, type: 'attribute', attribute: 'style', value: c.value };
|
|
2182
|
+
case 'pp-mob-css':
|
|
2183
|
+
return { selector: c.selector, type: 'attribute', attribute: 'data-mobile-css', value: c.value };
|
|
2184
|
+
case 'pp-tab-css':
|
|
2185
|
+
return { selector: c.selector, type: 'attribute', attribute: 'data-tablet-css', value: c.value };
|
|
2186
|
+
default:
|
|
2187
|
+
return null;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
|
|
1891
2191
|
function countUnresolvedGranularSelectors(iframeDoc, entries) {
|
|
1892
2192
|
if (!iframeDoc || !entries || !entries.length) return 0;
|
|
1893
2193
|
var n = 0;
|
|
1894
2194
|
for (var i = 0; i < entries.length; i++) {
|
|
1895
|
-
|
|
1896
|
-
try { el = iframeDoc.querySelector(entries[i].selector); } catch(_) {}
|
|
1897
|
-
if (!el) n++;
|
|
2195
|
+
if (!querySelectorResolved(iframeDoc, entries[i].selector)) n++;
|
|
1898
2196
|
}
|
|
1899
2197
|
return n;
|
|
1900
2198
|
}
|
|
1901
2199
|
|
|
1902
2200
|
function applyGranularChangesetEntries(iframeDoc, entries) {
|
|
1903
2201
|
if (!iframeDoc || !entries || !entries.length) return;
|
|
1904
|
-
|
|
1905
|
-
|
|
2202
|
+
beginSuppressIframeMutationDirty();
|
|
2203
|
+
try {
|
|
2204
|
+
for (var i = 0; i < entries.length; i++) {
|
|
2205
|
+
applyChangesetEntry(entries[i], iframeDoc);
|
|
2206
|
+
}
|
|
2207
|
+
} finally {
|
|
2208
|
+
endSuppressIframeMutationDirty();
|
|
1906
2209
|
}
|
|
1907
2210
|
}
|
|
1908
2211
|
|
|
@@ -1957,6 +2260,30 @@ function flushPendingGranularChangesets() {
|
|
|
1957
2260
|
scheduleGranularChangesetReapply();
|
|
1958
2261
|
}
|
|
1959
2262
|
|
|
2263
|
+
/** Stable key for structural changesets (insert/reorder) to avoid double-apply across early + full paint. */
|
|
2264
|
+
function structuralChangesetDedupKey(entry) {
|
|
2265
|
+
var nt = normalizeChangesetType(entry);
|
|
2266
|
+
if (!entry || (nt !== 'insert' && nt !== 'reorder')) return '';
|
|
2267
|
+
var vid = activeVarId || '';
|
|
2268
|
+
try {
|
|
2269
|
+
return (
|
|
2270
|
+
vid +
|
|
2271
|
+
'\0' +
|
|
2272
|
+
nt +
|
|
2273
|
+
'\0' +
|
|
2274
|
+
entry.selector +
|
|
2275
|
+
'\0' +
|
|
2276
|
+
String(entry.action || '') +
|
|
2277
|
+
'\0' +
|
|
2278
|
+
String(entry.html != null ? entry.html : '').slice(0, 240) +
|
|
2279
|
+
'\0' +
|
|
2280
|
+
String(entry.targetSelector || '')
|
|
2281
|
+
);
|
|
2282
|
+
} catch(_) {
|
|
2283
|
+
return vid + '\0' + nt + '\0' + entry.selector;
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
|
|
1960
2287
|
/**
|
|
1961
2288
|
* Apply a single changeset entry inside the editor iframe.
|
|
1962
2289
|
* Backend format: { selector, type: 'content'|'style'|'attribute'|'insert'|'remove', ... }
|
|
@@ -1976,21 +2303,34 @@ function applyChangesetEntry(entry, iframeDoc) {
|
|
|
1976
2303
|
return;
|
|
1977
2304
|
}
|
|
1978
2305
|
|
|
2306
|
+
var structuralDedupeKey = structuralChangesetDedupKey(entry);
|
|
2307
|
+
if (structuralDedupeKey && appliedStructuralChangesetKeys[structuralDedupeKey]) return;
|
|
2308
|
+
|
|
1979
2309
|
// \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
|
|
1980
|
-
var el =
|
|
1981
|
-
try { el = iframeDoc.querySelector(entry.selector); } catch(_) {}
|
|
2310
|
+
var el = querySelectorResolved(iframeDoc, entry.selector);
|
|
1982
2311
|
if (!el) return;
|
|
1983
2312
|
|
|
1984
2313
|
captureChangesetSnapshotBeforeApply(entry, el, iframeDoc);
|
|
1985
2314
|
|
|
1986
|
-
switch (entry
|
|
2315
|
+
switch (normalizeChangesetType(entry)) {
|
|
1987
2316
|
case 'content':
|
|
1988
2317
|
if (entry.html != null) el.innerHTML = entry.html;
|
|
1989
2318
|
else if (entry.value != null) el.textContent = entry.value;
|
|
1990
2319
|
break;
|
|
1991
2320
|
case 'style':
|
|
1992
|
-
if (entry.property
|
|
1993
|
-
|
|
2321
|
+
if (entry.property) {
|
|
2322
|
+
var propKebab = entry.property;
|
|
2323
|
+
var cam = camelize(propKebab);
|
|
2324
|
+
if (entry.value == null || entry.value === '') {
|
|
2325
|
+
try { el.style.removeProperty(propKebab); } catch(_) {}
|
|
2326
|
+
try { if (cam in el.style) el.style[cam] = ''; } catch(__) {}
|
|
2327
|
+
} else {
|
|
2328
|
+
try {
|
|
2329
|
+
el.style.setProperty(propKebab, entry.value, 'important');
|
|
2330
|
+
} catch(_) {
|
|
2331
|
+
el.style[cam] = entry.value;
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
1994
2334
|
}
|
|
1995
2335
|
break;
|
|
1996
2336
|
case 'attribute':
|
|
@@ -2001,11 +2341,23 @@ function applyChangesetEntry(entry, iframeDoc) {
|
|
|
2001
2341
|
case 'insert': {
|
|
2002
2342
|
var pos = entry.action === 'before' ? 'beforebegin' : 'afterend';
|
|
2003
2343
|
if (entry.html) el.insertAdjacentHTML(pos, entry.html);
|
|
2344
|
+
if (structuralDedupeKey) appliedStructuralChangesetKeys[structuralDedupeKey] = true;
|
|
2004
2345
|
break;
|
|
2005
2346
|
}
|
|
2006
2347
|
case 'remove':
|
|
2007
2348
|
el.style.display = 'none';
|
|
2008
2349
|
break;
|
|
2350
|
+
case 'reorder': {
|
|
2351
|
+
var target = entry.targetSelector ? querySelectorResolved(iframeDoc, entry.targetSelector) : null;
|
|
2352
|
+
if (!target || !el.parentNode || !target.parentNode) break;
|
|
2353
|
+
if (entry.action === 'before') {
|
|
2354
|
+
target.parentNode.insertBefore(el, target);
|
|
2355
|
+
} else {
|
|
2356
|
+
target.parentNode.insertBefore(el, target.nextSibling);
|
|
2357
|
+
}
|
|
2358
|
+
if (structuralDedupeKey) appliedStructuralChangesetKeys[structuralDedupeKey] = true;
|
|
2359
|
+
break;
|
|
2360
|
+
}
|
|
2009
2361
|
default: break;
|
|
2010
2362
|
}
|
|
2011
2363
|
}
|
|
@@ -2020,19 +2372,24 @@ function applyActiveVariationHtml() {
|
|
|
2020
2372
|
var variation = variations.find(function(v) { return v._id === activeVarId; });
|
|
2021
2373
|
var cs = parseVariationChangesets(variation);
|
|
2022
2374
|
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2375
|
+
beginSuppressIframeMutationDirty();
|
|
2376
|
+
try {
|
|
2377
|
+
// If we have an in-session HTML snapshot, use it (user edited in this session)
|
|
2378
|
+
var saved = varHtmlCache[activeVarId];
|
|
2379
|
+
if (saved) {
|
|
2380
|
+
try { iframeDoc.body.innerHTML = saved; } catch(_) {}
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2029
2383
|
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2384
|
+
if (!cs.length) return;
|
|
2385
|
+
for (var i = 0; i < cs.length; i++) {
|
|
2386
|
+
applyChangesetEntry(cs[i], iframeDoc);
|
|
2387
|
+
}
|
|
2388
|
+
// Selectors often miss on first paint (client-rendered sections). Retry via timer + mutations.
|
|
2389
|
+
registerPendingGranularChangesets(cs, iframeDoc);
|
|
2390
|
+
} finally {
|
|
2391
|
+
endSuppressIframeMutationDirty();
|
|
2033
2392
|
}
|
|
2034
|
-
// Selectors often miss on first paint (client-rendered sections). Retry via timer + mutations.
|
|
2035
|
-
registerPendingGranularChangesets(cs, iframeDoc);
|
|
2036
2393
|
}
|
|
2037
2394
|
|
|
2038
2395
|
function changesetsHaveBodySnapshot(cs) {
|
|
@@ -2043,6 +2400,23 @@ function changesetsHaveBodySnapshot(cs) {
|
|
|
2043
2400
|
return false;
|
|
2044
2401
|
}
|
|
2045
2402
|
|
|
2403
|
+
/** Rows to persist for this variation on Finalize (same chain-set model as EditorShell \u2014 never __vvveb_body__). */
|
|
2404
|
+
function buildPersistedChainSetsForVariation(v) {
|
|
2405
|
+
if (!v || !v._id) return [];
|
|
2406
|
+
var parsed = parseVariationChangesets(v);
|
|
2407
|
+
var base = filterGranularChangesetEntries(parsed);
|
|
2408
|
+
var sessionExtra = sessionStructuralChainRowsByVarId[v._id] || [];
|
|
2409
|
+
if (v._id !== activeVarId) {
|
|
2410
|
+
return mergeGranularChainSets(base, sessionExtra);
|
|
2411
|
+
}
|
|
2412
|
+
var overlay = [];
|
|
2413
|
+
for (var si = 0; si < stateChanges.length; si++) {
|
|
2414
|
+
var row = stateChangeToChainSet(stateChanges[si]);
|
|
2415
|
+
if (row) overlay.push(row);
|
|
2416
|
+
}
|
|
2417
|
+
return mergeGranularChainSets(mergeGranularChainSets(base, sessionExtra), overlay);
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2046
2420
|
/**
|
|
2047
2421
|
* While document.readyState === 'loading', apply only granular changesets (no __vvveb_body__
|
|
2048
2422
|
* replacement) so the first painted nodes can receive edits before iframe/window load completes.
|
|
@@ -2053,10 +2427,15 @@ function applyVariationGranularOnly(iframeDoc) {
|
|
|
2053
2427
|
var variation = variations.find(function(v) { return v._id === activeVarId; });
|
|
2054
2428
|
var cs = parseVariationChangesets(variation);
|
|
2055
2429
|
if (!cs.length || changesetsHaveBodySnapshot(cs)) return;
|
|
2056
|
-
|
|
2057
|
-
|
|
2430
|
+
beginSuppressIframeMutationDirty();
|
|
2431
|
+
try {
|
|
2432
|
+
for (var i = 0; i < cs.length; i++) {
|
|
2433
|
+
applyChangesetEntry(cs[i], iframeDoc);
|
|
2434
|
+
}
|
|
2435
|
+
registerPendingGranularChangesets(cs, iframeDoc);
|
|
2436
|
+
} finally {
|
|
2437
|
+
endSuppressIframeMutationDirty();
|
|
2058
2438
|
}
|
|
2059
|
-
registerPendingGranularChangesets(cs, iframeDoc);
|
|
2060
2439
|
}
|
|
2061
2440
|
|
|
2062
2441
|
/** Re-try granular entries without resetting pending registration (use between poll ticks). */
|
|
@@ -2066,8 +2445,13 @@ function reapplyActiveVariationGranular(iframeDoc) {
|
|
|
2066
2445
|
var variation = variations.find(function(v) { return v._id === activeVarId; });
|
|
2067
2446
|
var cs = parseVariationChangesets(variation);
|
|
2068
2447
|
if (!cs.length || changesetsHaveBodySnapshot(cs)) return;
|
|
2069
|
-
|
|
2070
|
-
|
|
2448
|
+
beginSuppressIframeMutationDirty();
|
|
2449
|
+
try {
|
|
2450
|
+
for (var i = 0; i < cs.length; i++) {
|
|
2451
|
+
applyChangesetEntry(cs[i], iframeDoc);
|
|
2452
|
+
}
|
|
2453
|
+
} finally {
|
|
2454
|
+
endSuppressIframeMutationDirty();
|
|
2071
2455
|
}
|
|
2072
2456
|
}
|
|
2073
2457
|
|
|
@@ -2129,9 +2513,14 @@ function startIframeContentApplyWatcher(navGen) {
|
|
|
2129
2513
|
|
|
2130
2514
|
// \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
|
|
2131
2515
|
function selectElement(el) {
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2516
|
+
beginSuppressIframeMutationDirty();
|
|
2517
|
+
try {
|
|
2518
|
+
if (selectedEl) { try { selectedEl.classList.remove('vve-selected'); } catch(_) {} }
|
|
2519
|
+
selectedEl = el;
|
|
2520
|
+
try { el.classList.add('vve-selected'); } catch(_) {}
|
|
2521
|
+
} finally {
|
|
2522
|
+
endSuppressIframeMutationDirty();
|
|
2523
|
+
}
|
|
2135
2524
|
document.getElementById('bc-path').textContent = buildSelector(el);
|
|
2136
2525
|
document.getElementById('bc-path').style.color = 'var(--accent-txt)';
|
|
2137
2526
|
document.getElementById('no-sel').style.display = 'none';
|
|
@@ -2146,7 +2535,12 @@ function selectElement(el) {
|
|
|
2146
2535
|
|
|
2147
2536
|
function deselectElement() {
|
|
2148
2537
|
setDragHandleActive(false);
|
|
2149
|
-
|
|
2538
|
+
beginSuppressIframeMutationDirty();
|
|
2539
|
+
try {
|
|
2540
|
+
if (selectedEl) { try { selectedEl.classList.remove('vve-selected'); } catch(_) {} selectedEl = null; }
|
|
2541
|
+
} finally {
|
|
2542
|
+
endSuppressIframeMutationDirty();
|
|
2543
|
+
}
|
|
2150
2544
|
document.getElementById('no-sel').style.display = '';
|
|
2151
2545
|
document.getElementById('el-info').style.display = 'none';
|
|
2152
2546
|
document.getElementById('rp-accordion').style.display = 'none';
|
|
@@ -2170,18 +2564,27 @@ function injectIframeSelectionStyles(doc) {
|
|
|
2170
2564
|
'html.vve-drag-armed .vve-selected{cursor:grab!important;}' +
|
|
2171
2565
|
'.vve-dragging{opacity:0.92!important;outline:2px dashed #f59e0b!important;' +
|
|
2172
2566
|
'outline-offset:2px!important;cursor:grabbing!important;box-shadow:none!important;}';
|
|
2173
|
-
|
|
2567
|
+
beginSuppressIframeMutationDirty();
|
|
2568
|
+
try {
|
|
2569
|
+
doc.head.appendChild(st);
|
|
2570
|
+
} finally {
|
|
2571
|
+
endSuppressIframeMutationDirty();
|
|
2572
|
+
}
|
|
2174
2573
|
}
|
|
2175
2574
|
|
|
2176
2575
|
function setDragHandleActive(on) {
|
|
2177
2576
|
dragHandleActive = !!on;
|
|
2178
2577
|
var b = document.getElementById('sf-drag');
|
|
2179
2578
|
if (b) b.classList.toggle('active', dragHandleActive);
|
|
2579
|
+
beginSuppressIframeMutationDirty();
|
|
2180
2580
|
try {
|
|
2181
2581
|
var iframe = document.getElementById('iframeId');
|
|
2182
2582
|
var d = iframe && iframe.contentDocument && iframe.contentDocument.documentElement;
|
|
2183
2583
|
if (d) d.classList.toggle('vve-drag-armed', dragHandleActive);
|
|
2184
|
-
} catch(_) {
|
|
2584
|
+
} catch(_) {
|
|
2585
|
+
} finally {
|
|
2586
|
+
endSuppressIframeMutationDirty();
|
|
2587
|
+
}
|
|
2185
2588
|
}
|
|
2186
2589
|
|
|
2187
2590
|
function positionSelectionToolbar() {
|
|
@@ -2251,34 +2654,72 @@ function selectElementFromTree(el) {
|
|
|
2251
2654
|
|
|
2252
2655
|
function duplicateSelectedEl() {
|
|
2253
2656
|
if (!selectedEl || !selectedEl.parentNode) return;
|
|
2657
|
+
var anchorSel = buildSelector(selectedEl);
|
|
2254
2658
|
var clone = selectedEl.cloneNode(true);
|
|
2255
2659
|
clone.classList.remove('vve-selected');
|
|
2256
2660
|
var all = clone.querySelectorAll ? clone.querySelectorAll('.vve-selected') : [];
|
|
2257
2661
|
for (var i = 0; i < all.length; i++) all[i].classList.remove('vve-selected');
|
|
2258
2662
|
clone.style.visibility = '';
|
|
2259
2663
|
if (clone.removeAttribute) clone.removeAttribute('data-vve-hidden');
|
|
2664
|
+
stripDataVveInstanceSubtree(clone);
|
|
2665
|
+
try {
|
|
2666
|
+
if (clone.id) clone.removeAttribute('id');
|
|
2667
|
+
} catch(_) {}
|
|
2668
|
+
try {
|
|
2669
|
+
clone.setAttribute('data-vve-instance', generateVveInstanceId());
|
|
2670
|
+
} catch(_) {}
|
|
2671
|
+
if (activeVarId) {
|
|
2672
|
+
appendSessionStructuralChainRow(activeVarId, {
|
|
2673
|
+
selector: anchorSel,
|
|
2674
|
+
type: 'insert',
|
|
2675
|
+
action: 'after',
|
|
2676
|
+
html: clone.outerHTML,
|
|
2677
|
+
});
|
|
2678
|
+
}
|
|
2260
2679
|
selectedEl.parentNode.insertBefore(clone, selectedEl.nextSibling);
|
|
2261
|
-
|
|
2680
|
+
saveCurrentVariationHtml();
|
|
2681
|
+
recomputeEditorDirty();
|
|
2262
2682
|
scheduleDomTreeRefresh();
|
|
2263
2683
|
selectElement(clone);
|
|
2264
2684
|
}
|
|
2265
2685
|
|
|
2266
2686
|
function toggleHideSelectedEl() {
|
|
2267
2687
|
if (!selectedEl) return;
|
|
2688
|
+
var hidSel = buildSelector(selectedEl);
|
|
2268
2689
|
if (selectedEl.getAttribute('data-vve-hidden') === '1') {
|
|
2269
2690
|
selectedEl.style.visibility = '';
|
|
2270
2691
|
selectedEl.removeAttribute('data-vve-hidden');
|
|
2692
|
+
if (activeVarId) {
|
|
2693
|
+
appendSessionStructuralChainRow(activeVarId, {
|
|
2694
|
+
selector: hidSel,
|
|
2695
|
+
type: 'style',
|
|
2696
|
+
property: 'visibility',
|
|
2697
|
+
value: '',
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2271
2700
|
} else {
|
|
2272
2701
|
selectedEl.style.visibility = 'hidden';
|
|
2273
2702
|
selectedEl.setAttribute('data-vve-hidden', '1');
|
|
2703
|
+
if (activeVarId) {
|
|
2704
|
+
appendSessionStructuralChainRow(activeVarId, {
|
|
2705
|
+
selector: hidSel,
|
|
2706
|
+
type: 'style',
|
|
2707
|
+
property: 'visibility',
|
|
2708
|
+
value: 'hidden',
|
|
2709
|
+
});
|
|
2710
|
+
}
|
|
2274
2711
|
}
|
|
2275
|
-
|
|
2712
|
+
saveCurrentVariationHtml();
|
|
2713
|
+
recomputeEditorDirty();
|
|
2276
2714
|
}
|
|
2277
2715
|
|
|
2278
2716
|
function deleteSelectedEl() {
|
|
2279
2717
|
if (!selectedEl || !selectedEl.parentNode) return;
|
|
2718
|
+
var delSel = buildSelector(selectedEl);
|
|
2280
2719
|
selectedEl.remove();
|
|
2281
|
-
|
|
2720
|
+
if (activeVarId) appendSessionStructuralChainRow(activeVarId, { selector: delSel, type: 'remove' });
|
|
2721
|
+
saveCurrentVariationHtml();
|
|
2722
|
+
recomputeEditorDirty();
|
|
2282
2723
|
deselectElement();
|
|
2283
2724
|
scheduleDomTreeRefresh();
|
|
2284
2725
|
}
|
|
@@ -2570,14 +3011,12 @@ function renderImageSection(el) {
|
|
|
2570
3011
|
var prev = document.querySelector('.img-preview');
|
|
2571
3012
|
if (prev) prev.src = srcInp.value;
|
|
2572
3013
|
logChange(sel, 'pp-src', srcInp.value, el, orig);
|
|
2573
|
-
markDirty();
|
|
2574
3014
|
});
|
|
2575
3015
|
var altInp = document.getElementById('pp-img-alt');
|
|
2576
3016
|
if (altInp) altInp.addEventListener('input', function() {
|
|
2577
3017
|
var orig = getOriginalValue('pp-alt', el);
|
|
2578
3018
|
el.setAttribute('alt', altInp.value);
|
|
2579
3019
|
logChange(sel, 'pp-alt', altInp.value, el, orig);
|
|
2580
|
-
markDirty();
|
|
2581
3020
|
});
|
|
2582
3021
|
|
|
2583
3022
|
// Wire srcset entry inputs
|
|
@@ -2586,7 +3025,8 @@ function renderImageSection(el) {
|
|
|
2586
3025
|
var dInp = document.getElementById('pp-se-desc-'+i);
|
|
2587
3026
|
function flushSrcset() {
|
|
2588
3027
|
el.setAttribute('srcset', buildSrcset(_srcsetEntries));
|
|
2589
|
-
|
|
3028
|
+
saveCurrentVariationHtml();
|
|
3029
|
+
recomputeEditorDirty();
|
|
2590
3030
|
}
|
|
2591
3031
|
if (uInp) uInp.addEventListener('input', function(){ _srcsetEntries[i].url = uInp.value; flushSrcset(); });
|
|
2592
3032
|
if (dInp) dInp.addEventListener('input', function(){ _srcsetEntries[i].descriptor = dInp.value; flushSrcset(); });
|
|
@@ -2604,7 +3044,8 @@ function renderImageSection(el) {
|
|
|
2604
3044
|
}
|
|
2605
3045
|
function flushSizes() {
|
|
2606
3046
|
el.setAttribute('sizes', buildSizes(_sizesEntries));
|
|
2607
|
-
|
|
3047
|
+
saveCurrentVariationHtml();
|
|
3048
|
+
recomputeEditorDirty();
|
|
2608
3049
|
}
|
|
2609
3050
|
if (opInp) opInp.addEventListener('change', function(){ buildCondition(); flushSizes(); });
|
|
2610
3051
|
if (valInp) valInp.addEventListener('input', function(){ buildCondition(); flushSizes(); });
|
|
@@ -2618,7 +3059,12 @@ function addSrcsetEntry() {
|
|
|
2618
3059
|
}
|
|
2619
3060
|
function removeSrcsetEntry(i) {
|
|
2620
3061
|
_srcsetEntries.splice(i, 1);
|
|
2621
|
-
if (_imageEl) {
|
|
3062
|
+
if (_imageEl) {
|
|
3063
|
+
_imageEl.setAttribute('srcset', buildSrcset(_srcsetEntries));
|
|
3064
|
+
renderImageSection(_imageEl);
|
|
3065
|
+
saveCurrentVariationHtml();
|
|
3066
|
+
recomputeEditorDirty();
|
|
3067
|
+
}
|
|
2622
3068
|
}
|
|
2623
3069
|
function addSizesEntry() {
|
|
2624
3070
|
_sizesEntries.push({condition:'max-width: 760px', value:'760px'});
|
|
@@ -2626,7 +3072,12 @@ function addSizesEntry() {
|
|
|
2626
3072
|
}
|
|
2627
3073
|
function removeSizesEntry(i) {
|
|
2628
3074
|
_sizesEntries.splice(i, 1);
|
|
2629
|
-
if (_imageEl) {
|
|
3075
|
+
if (_imageEl) {
|
|
3076
|
+
_imageEl.setAttribute('sizes', buildSizes(_sizesEntries));
|
|
3077
|
+
renderImageSection(_imageEl);
|
|
3078
|
+
saveCurrentVariationHtml();
|
|
3079
|
+
recomputeEditorDirty();
|
|
3080
|
+
}
|
|
2630
3081
|
}
|
|
2631
3082
|
|
|
2632
3083
|
// \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
|
|
@@ -2831,20 +3282,51 @@ function renderRightPanel(el) {
|
|
|
2831
3282
|
var orig = getOriginalValue(b[0], el);
|
|
2832
3283
|
b[1](inp.value);
|
|
2833
3284
|
logChange(sel, b[0], inp.value, el, orig);
|
|
2834
|
-
markDirty();
|
|
2835
3285
|
});
|
|
2836
3286
|
});
|
|
2837
3287
|
}
|
|
2838
3288
|
|
|
2839
3289
|
// \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
|
|
3290
|
+
function generateVveInstanceId() {
|
|
3291
|
+
return 'v' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
/** Editor-assigned clone marker so duplicated subtrees do not share the same CSS path as the original. */
|
|
3295
|
+
function stripDataVveInstanceSubtree(root) {
|
|
3296
|
+
if (!root || root.nodeType !== 1) return;
|
|
3297
|
+
try {
|
|
3298
|
+
root.removeAttribute('data-vve-instance');
|
|
3299
|
+
} catch(_) {}
|
|
3300
|
+
var sub = root.querySelectorAll ? root.querySelectorAll('[data-vve-instance]') : [];
|
|
3301
|
+
for (var i = 0; i < sub.length; i++) {
|
|
3302
|
+
try {
|
|
3303
|
+
sub[i].removeAttribute('data-vve-instance');
|
|
3304
|
+
} catch(_) {}
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
|
|
2840
3308
|
function buildSelector(el) {
|
|
2841
3309
|
if (!el) return '';
|
|
3310
|
+
var doc = el.ownerDocument || document;
|
|
3311
|
+
var inst = el.getAttribute && el.getAttribute('data-vve-instance');
|
|
3312
|
+
if (inst && String(inst).trim()) {
|
|
3313
|
+
var safe = String(inst).split('"').join('\\"');
|
|
3314
|
+
var attrSel = '[data-vve-instance="' + safe + '"]';
|
|
3315
|
+
try {
|
|
3316
|
+
if (doc.querySelectorAll(attrSel).length === 1) return attrSel;
|
|
3317
|
+
} catch(_) {}
|
|
3318
|
+
}
|
|
2842
3319
|
if (el.id) return '#' + el.id;
|
|
2843
3320
|
var parts = [], node = el, depth = 0;
|
|
2844
3321
|
while (node && node.nodeType === 1 && depth < 5) {
|
|
2845
3322
|
if (node.id) { parts.unshift('#' + node.id); break; }
|
|
2846
3323
|
var p = node.tagName.toLowerCase();
|
|
2847
|
-
if (node.classList && node.classList.length)
|
|
3324
|
+
if (node.classList && node.classList.length) {
|
|
3325
|
+
var clsArr = Array.from(node.classList).filter(function(c) {
|
|
3326
|
+
return c.indexOf('vve-') !== 0;
|
|
3327
|
+
});
|
|
3328
|
+
if (clsArr.length) p += '.' + clsArr.slice(0, 2).join('.');
|
|
3329
|
+
}
|
|
2848
3330
|
var idx = 1, sib = node.previousElementSibling;
|
|
2849
3331
|
while (sib) { if (sib.tagName === node.tagName) idx++; sib = sib.previousElementSibling; }
|
|
2850
3332
|
if (idx > 1) p += ':nth-of-type(' + idx + ')';
|
|
@@ -2855,6 +3337,71 @@ function buildSelector(el) {
|
|
|
2855
3337
|
return parts.join(' > ');
|
|
2856
3338
|
}
|
|
2857
3339
|
|
|
3340
|
+
/**
|
|
3341
|
+
* Strip editor-only .vve-* class tokens from a selector string (fixes DB rows saved while an element was selected).
|
|
3342
|
+
*/
|
|
3343
|
+
function sanitizeSelectorForMatch(sel) {
|
|
3344
|
+
if (!sel || typeof sel !== 'string') return '';
|
|
3345
|
+
var s0 = sel.replace(/.vve-[a-zA-Z0-9_-]+/gi, '');
|
|
3346
|
+
var parts = s0.split(/s*>s*/).map(function(seg) {
|
|
3347
|
+
var t = seg.replace(/.+/g, '.').replace(/.$/, '');
|
|
3348
|
+
return t.trim();
|
|
3349
|
+
});
|
|
3350
|
+
return parts.filter(Boolean).join(' > ');
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
/** Drop the rightmost :nth-of-type(n) (hydration / layout often shifts sibling indices). */
|
|
3354
|
+
function stripRightmostNthOfType(sel) {
|
|
3355
|
+
if (!sel || typeof sel !== 'string') return null;
|
|
3356
|
+
var idx = sel.lastIndexOf(':nth-of-type(');
|
|
3357
|
+
if (idx === -1) return null;
|
|
3358
|
+
var j = idx + ':nth-of-type('.length;
|
|
3359
|
+
while (j < sel.length && sel.charCodeAt(j) >= 48 && sel.charCodeAt(j) <= 57) j++;
|
|
3360
|
+
if (j >= sel.length || sel.charAt(j) !== ')') return null;
|
|
3361
|
+
return (sel.slice(0, idx) + sel.slice(j + 1))
|
|
3362
|
+
.replace(/s{2,}/g, ' ')
|
|
3363
|
+
.replace(/s*>s*>/g, ' >')
|
|
3364
|
+
.trim();
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
function querySelectorResolved(iframeDoc, selector) {
|
|
3368
|
+
if (!iframeDoc || !selector) return null;
|
|
3369
|
+
var seen = {};
|
|
3370
|
+
function tryOne(s) {
|
|
3371
|
+
if (!s || seen[s]) return null;
|
|
3372
|
+
seen[s] = true;
|
|
3373
|
+
try {
|
|
3374
|
+
return iframeDoc.querySelector(s) || null;
|
|
3375
|
+
} catch(_) {
|
|
3376
|
+
return null;
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
function walkRelax(base) {
|
|
3380
|
+
if (!base) return null;
|
|
3381
|
+
var el = tryOne(base);
|
|
3382
|
+
if (el) return el;
|
|
3383
|
+
var cur = base;
|
|
3384
|
+
for (var g = 0; g < 28; g++) {
|
|
3385
|
+
var nxt = stripRightmostNthOfType(cur);
|
|
3386
|
+
if (!nxt || nxt === cur) break;
|
|
3387
|
+
cur = nxt;
|
|
3388
|
+
el = tryOne(cur);
|
|
3389
|
+
if (el) return el;
|
|
3390
|
+
}
|
|
3391
|
+
return null;
|
|
3392
|
+
}
|
|
3393
|
+
var alt = sanitizeSelectorForMatch(selector);
|
|
3394
|
+
// Prefer sanitized + nth relax FIRST: raw selectors often still contain .vve-* from
|
|
3395
|
+
// save-time selection; those only match after clicking (we re-add vve-selected).
|
|
3396
|
+
var el = walkRelax(alt || selector);
|
|
3397
|
+
if (el) return el;
|
|
3398
|
+
if (alt !== selector) {
|
|
3399
|
+
el = walkRelax(selector);
|
|
3400
|
+
if (el) return el;
|
|
3401
|
+
}
|
|
3402
|
+
return null;
|
|
3403
|
+
}
|
|
3404
|
+
|
|
2858
3405
|
// \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
|
|
2859
3406
|
function repositionDragSibling(dragEl, clientY) {
|
|
2860
3407
|
var p = dragEl.parentElement;
|
|
@@ -2880,6 +3427,27 @@ function repositionDragSibling(dragEl, clientY) {
|
|
|
2880
3427
|
}
|
|
2881
3428
|
}
|
|
2882
3429
|
|
|
3430
|
+
function recordReorderAfterDrag(movedEl) {
|
|
3431
|
+
if (!activeVarId || !movedEl || !movedEl.parentElement) return;
|
|
3432
|
+
var prev = movedEl.previousElementSibling;
|
|
3433
|
+
var next = movedEl.nextElementSibling;
|
|
3434
|
+
if (prev) {
|
|
3435
|
+
appendSessionStructuralChainRow(activeVarId, {
|
|
3436
|
+
selector: buildSelector(movedEl),
|
|
3437
|
+
type: 'reorder',
|
|
3438
|
+
targetSelector: buildSelector(prev),
|
|
3439
|
+
action: 'after',
|
|
3440
|
+
});
|
|
3441
|
+
} else if (next) {
|
|
3442
|
+
appendSessionStructuralChainRow(activeVarId, {
|
|
3443
|
+
selector: buildSelector(movedEl),
|
|
3444
|
+
type: 'reorder',
|
|
3445
|
+
targetSelector: buildSelector(next),
|
|
3446
|
+
action: 'before',
|
|
3447
|
+
});
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
|
|
2883
3451
|
function attachDragReposition() {
|
|
2884
3452
|
try {
|
|
2885
3453
|
var iframe = document.getElementById('iframeId');
|
|
@@ -2933,7 +3501,9 @@ function attachDragReposition() {
|
|
|
2933
3501
|
} catch(_) {}
|
|
2934
3502
|
suppressClickUntil = Date.now() + 200;
|
|
2935
3503
|
setDragHandleActive(false);
|
|
2936
|
-
|
|
3504
|
+
if (activeVarId) recordReorderAfterDrag(selectedEl);
|
|
3505
|
+
saveCurrentVariationHtml();
|
|
3506
|
+
recomputeEditorDirty();
|
|
2937
3507
|
updateSelectionToolbar();
|
|
2938
3508
|
scheduleDomTreeRefresh();
|
|
2939
3509
|
}
|
|
@@ -2987,8 +3557,7 @@ function attachChangeObserver() {
|
|
|
2987
3557
|
changeObserverDoc = null;
|
|
2988
3558
|
}
|
|
2989
3559
|
changeObserver = new MutationObserver(function() {
|
|
2990
|
-
|
|
2991
|
-
// Debounced full rebuild of Elements panel as the live DOM grows / changes
|
|
3560
|
+
// Dirty state is derived from changesets baseline + stateChanges (not raw DOM mutations).
|
|
2992
3561
|
scheduleDomTreeRefresh();
|
|
2993
3562
|
scheduleGranularChangesetReapply();
|
|
2994
3563
|
});
|
|
@@ -3024,6 +3593,7 @@ function syncIframeInteractions(reason) {
|
|
|
3024
3593
|
var inp = document.getElementById('comp-search');
|
|
3025
3594
|
renderDomTree(inp ? inp.value : '');
|
|
3026
3595
|
updateSelectionToolbar();
|
|
3596
|
+
recomputeEditorDirty();
|
|
3027
3597
|
} catch(_) {}
|
|
3028
3598
|
}
|
|
3029
3599
|
|
|
@@ -3075,8 +3645,11 @@ function insertHtml(html) {
|
|
|
3075
3645
|
console.warn('[V2] insertHtml: iframe document not ready');
|
|
3076
3646
|
return;
|
|
3077
3647
|
}
|
|
3648
|
+
var htmlStr = String(html).trim();
|
|
3649
|
+
var anchorSel =
|
|
3650
|
+
selectedEl && selectedEl !== doc.body && selectedEl.parentNode ? buildSelector(selectedEl) : 'body';
|
|
3078
3651
|
var t = doc.createElement('template');
|
|
3079
|
-
t.innerHTML =
|
|
3652
|
+
t.innerHTML = htmlStr;
|
|
3080
3653
|
var frag = doc.createDocumentFragment();
|
|
3081
3654
|
var firstEl = null;
|
|
3082
3655
|
while (t.content.firstChild) {
|
|
@@ -3092,7 +3665,16 @@ function insertHtml(html) {
|
|
|
3092
3665
|
doc.body.appendChild(frag);
|
|
3093
3666
|
}
|
|
3094
3667
|
if (firstEl) selectElement(firstEl);
|
|
3095
|
-
|
|
3668
|
+
if (activeVarId) {
|
|
3669
|
+
appendSessionStructuralChainRow(activeVarId, {
|
|
3670
|
+
selector: anchorSel,
|
|
3671
|
+
type: 'insert',
|
|
3672
|
+
action: 'after',
|
|
3673
|
+
html: htmlStr,
|
|
3674
|
+
});
|
|
3675
|
+
}
|
|
3676
|
+
saveCurrentVariationHtml();
|
|
3677
|
+
recomputeEditorDirty();
|
|
3096
3678
|
scheduleDomTreeRefresh();
|
|
3097
3679
|
} catch(err) { console.warn('[V2] insertHtml:', err); }
|
|
3098
3680
|
}
|
|
@@ -3184,15 +3766,37 @@ document.getElementById('btn-close').addEventListener('click', handleClose);
|
|
|
3184
3766
|
function handleSave() {
|
|
3185
3767
|
saveCurrentVariationHtml();
|
|
3186
3768
|
var updatedVariations = variations.map(function(v) {
|
|
3187
|
-
var
|
|
3188
|
-
|
|
3189
|
-
|
|
3769
|
+
var prevParsed = parseVariationChangesets(v);
|
|
3770
|
+
var granularPrev = filterGranularChangesetEntries(prevParsed);
|
|
3771
|
+
var bodyOnlyLegacy = changesetsHaveBodySnapshot(prevParsed) && granularPrev.length === 0;
|
|
3772
|
+
|
|
3773
|
+
var rows = buildPersistedChainSetsForVariation(v);
|
|
3774
|
+
if (bodyOnlyLegacy && rows.length === 0) {
|
|
3775
|
+
return Object.assign({}, v);
|
|
3776
|
+
}
|
|
3777
|
+
var json = '[]';
|
|
3778
|
+
try {
|
|
3779
|
+
json = JSON.stringify(rows || []);
|
|
3780
|
+
} catch(_) {
|
|
3781
|
+
json = '[]';
|
|
3782
|
+
}
|
|
3783
|
+
return Object.assign({}, v, { changesets: json });
|
|
3190
3784
|
});
|
|
3785
|
+
variations = updatedVariations;
|
|
3786
|
+
varHtmlCache = {};
|
|
3787
|
+
sessionStructuralChainRowsByVarId = {};
|
|
3788
|
+
stateChanges = [];
|
|
3789
|
+
if (currentMainTab === 'states') renderStatesTab();
|
|
3790
|
+
captureBaselineFromVariations(variations);
|
|
3791
|
+
if (experimentData && Array.isArray(experimentData.variations)) {
|
|
3792
|
+
experimentData.variations = updatedVariations;
|
|
3793
|
+
}
|
|
3191
3794
|
send('save-experiment', { experimentId: experimentData ? experimentData.experimentId : null, variations: updatedVariations });
|
|
3192
|
-
|
|
3795
|
+
setEditorDirty(false);
|
|
3193
3796
|
}
|
|
3194
3797
|
|
|
3195
3798
|
function handleClose() {
|
|
3799
|
+
clearVisualEditorLocalStorage();
|
|
3196
3800
|
// Unsaved-changes UX lives in the parent (PlatformVisualEditorV2); avoid double confirm here.
|
|
3197
3801
|
send('close-editor', {});
|
|
3198
3802
|
}
|
|
@@ -3200,8 +3804,22 @@ function handleClose() {
|
|
|
3200
3804
|
// \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
|
|
3201
3805
|
document.addEventListener('keydown', function(e) {
|
|
3202
3806
|
var meta = e.metaKey || e.ctrlKey;
|
|
3203
|
-
if (meta && !e.shiftKey && e.key === 'z') {
|
|
3204
|
-
|
|
3807
|
+
if (meta && !e.shiftKey && e.key === 'z') {
|
|
3808
|
+
e.preventDefault();
|
|
3809
|
+
if (typeof Vvveb !== 'undefined' && Vvveb.Undo) {
|
|
3810
|
+
Vvveb.Undo.undo();
|
|
3811
|
+
saveCurrentVariationHtml();
|
|
3812
|
+
recomputeEditorDirty();
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
if (meta && e.shiftKey && e.key === 'z') {
|
|
3816
|
+
e.preventDefault();
|
|
3817
|
+
if (typeof Vvveb !== 'undefined' && Vvveb.Undo) {
|
|
3818
|
+
Vvveb.Undo.redo();
|
|
3819
|
+
saveCurrentVariationHtml();
|
|
3820
|
+
recomputeEditorDirty();
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3205
3823
|
if (meta && e.key === 's') { e.preventDefault(); handleSave(); }
|
|
3206
3824
|
if (e.key === 'Escape') {
|
|
3207
3825
|
var openTips = document.querySelectorAll('.ve-pl-tip.is-tip-open');
|