ngx-xtroedge-cms 1.4.0 → 1.4.3

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.mjs CHANGED
@@ -397,6 +397,12 @@ body.lcms-editing [data-cms] {
397
397
  -webkit-user-select: text !important;
398
398
  cursor: text !important;
399
399
  }
400
+
401
+ /* Ensure editable elements appear above other content */
402
+ body.lcms-editing [contenteditable="true"] {
403
+ position: relative !important;
404
+ z-index: 9999 !important;
405
+ }
400
406
  `;
401
407
 
402
408
  // src/xtroedge-cms.ts
@@ -483,6 +489,7 @@ var _XtroedgeCMS = class _XtroedgeCMS {
483
489
  this.scanTimeout = null;
484
490
  this.activeImageEl = null;
485
491
  this.imageCtxMenu = null;
492
+ this.floatingEditDialog = null;
486
493
  // ===== Rich Text Toolbar =====
487
494
  this.richToolbarEl = null;
488
495
  this.activeEditableEl = null;
@@ -520,6 +527,7 @@ var _XtroedgeCMS = class _XtroedgeCMS {
520
527
  // ===== Edit-mode visibility tracking =====
521
528
  this.editScrollHandler = null;
522
529
  this.editScrollRAF = null;
530
+ this.editClickCaptureHandler = null;
523
531
  // ===== FAB Drag =====
524
532
  this.posX = 20;
525
533
  this.posY = 20;
@@ -646,6 +654,26 @@ var _XtroedgeCMS = class _XtroedgeCMS {
646
654
  this.observer.observe(document.body, { childList: true, subtree: true });
647
655
  window.addEventListener("popstate", this.boundPopState);
648
656
  window.addEventListener("hashchange", this.boundHashChange);
657
+ document.addEventListener("click", (e) => {
658
+ if (!document.body.classList.contains("lcms-editing")) return;
659
+ const target = e.target;
660
+ const anchor = target.closest("a");
661
+ if (anchor) {
662
+ let editableEl = null;
663
+ if (target.getAttribute("contenteditable") === "true") {
664
+ editableEl = target;
665
+ } else {
666
+ editableEl = target.closest('[contenteditable="true"]') || anchor.querySelector('[contenteditable="true"]');
667
+ }
668
+ if (editableEl) {
669
+ e.preventDefault();
670
+ e.stopPropagation();
671
+ setTimeout(() => {
672
+ editableEl.focus();
673
+ }, 0);
674
+ }
675
+ }
676
+ }, true);
649
677
  this.handleNavigation();
650
678
  }
651
679
  destroy() {
@@ -666,6 +694,14 @@ var _XtroedgeCMS = class _XtroedgeCMS {
666
694
  if (this.toastTimer) clearTimeout(this.toastTimer);
667
695
  this.db?.close();
668
696
  }
697
+ /**
698
+ * Manually trigger a rescan of the DOM to detect new editable elements.
699
+ * Useful when elements are dynamically shown/hidden or added to the page.
700
+ */
701
+ rescan() {
702
+ if (this.scanTimeout) clearTimeout(this.scanTimeout);
703
+ this.autoDetectAndScan();
704
+ }
669
705
  // 24 hours
670
706
  async validateLicense() {
671
707
  const licenseKey = this.config.licenseKey || _XtroedgeCMS.secureGet("builder_token") || "";
@@ -1165,13 +1201,20 @@ var _XtroedgeCMS = class _XtroedgeCMS {
1165
1201
  return this.currentSlug;
1166
1202
  };
1167
1203
  const tagCounters = {};
1204
+ const hasCmsKey = (candidate) => {
1205
+ return Array.from(document.querySelectorAll("[data-cms]")).some((node) => node.getAttribute("data-cms") === candidate);
1206
+ };
1168
1207
  const assignKey = (el, sectionSlug) => {
1169
1208
  const sectionKey = sectionSlug === "/header" ? "header" : sectionSlug === "/footer" ? "footer" : "page";
1170
1209
  if (!tagCounters[sectionKey]) tagCounters[sectionKey] = {};
1171
1210
  const tag = el.tagName.toLowerCase();
1172
1211
  if (!tagCounters[sectionKey][tag]) tagCounters[sectionKey][tag] = 0;
1173
1212
  const prefix = sectionKey !== "page" ? `${sectionKey}_` : "";
1174
- const key = `${prefix}xcms_${tag}_${tagCounters[sectionKey][tag]}`;
1213
+ let key = `${prefix}xcms_${tag}_${tagCounters[sectionKey][tag]}`;
1214
+ while (hasCmsKey(key)) {
1215
+ tagCounters[sectionKey][tag]++;
1216
+ key = `${prefix}xcms_${tag}_${tagCounters[sectionKey][tag]}`;
1217
+ }
1175
1218
  tagCounters[sectionKey][tag]++;
1176
1219
  el.setAttribute("data-cms", key);
1177
1220
  el.setAttribute("data-cms-section", sectionSlug);
@@ -1198,9 +1241,21 @@ var _XtroedgeCMS = class _XtroedgeCMS {
1198
1241
  }
1199
1242
  scanDOM() {
1200
1243
  const elements = document.querySelectorAll("[data-cms]");
1244
+ const seenKeys = /* @__PURE__ */ new Set();
1201
1245
  elements.forEach((el) => {
1202
1246
  if (el.closest("#xtroedge-cms-root, #lcms-login-modal")) return;
1203
- const key = el.getAttribute("data-cms");
1247
+ let key = el.getAttribute("data-cms");
1248
+ if (seenKeys.has(key)) {
1249
+ let idx = 1;
1250
+ let repaired = `${key}__${idx}`;
1251
+ while (document.querySelector(`[data-cms="${repaired}"]`)) {
1252
+ idx++;
1253
+ repaired = `${key}__${idx}`;
1254
+ }
1255
+ el.setAttribute("data-cms", repaired);
1256
+ key = repaired;
1257
+ }
1258
+ seenKeys.add(key);
1204
1259
  this.registeredKeys.add(key);
1205
1260
  if (!this.managedElements.has(el)) {
1206
1261
  this.attachElement(el, key);
@@ -1248,21 +1303,20 @@ var _XtroedgeCMS = class _XtroedgeCMS {
1248
1303
  return !!el.querySelector("[data-cms]");
1249
1304
  }
1250
1305
  getElementContent(el) {
1251
- if (this.hasEditableChildren(el)) {
1252
- return this.getDirectTextContent(el).trim();
1253
- }
1254
- return this.richTextEnabled ? el.innerHTML?.trim() || "" : el.textContent?.trim() || "";
1306
+ if (this.richTextEnabled) return el.innerHTML?.trim() || "";
1307
+ if (this.hasEditableChildren(el)) return this.getDirectTextContent(el).trim();
1308
+ return el.textContent?.trim() || "";
1255
1309
  }
1256
1310
  setElementContent(el, val) {
1257
- if (this.hasEditableChildren(el)) {
1311
+ if (this.richTextEnabled) {
1312
+ el.innerHTML = this.sanitizeHTML(val);
1313
+ } else if (this.hasEditableChildren(el)) {
1258
1314
  const textNodes = [];
1259
1315
  for (let i = 0; i < el.childNodes.length; i++) {
1260
1316
  if (el.childNodes[i].nodeType === Node.TEXT_NODE) textNodes.push(el.childNodes[i]);
1261
1317
  }
1262
1318
  if (textNodes.length > 0) textNodes[0].textContent = val;
1263
1319
  else el.prepend(document.createTextNode(val));
1264
- } else if (this.richTextEnabled) {
1265
- el.innerHTML = this.sanitizeHTML(val);
1266
1320
  } else {
1267
1321
  el.textContent = val;
1268
1322
  }
@@ -1296,12 +1350,21 @@ var _XtroedgeCMS = class _XtroedgeCMS {
1296
1350
  const currentVal = this.getPageText(key);
1297
1351
  if (text !== currentVal) this.onTextChanged(key, text);
1298
1352
  };
1353
+ const focusEditableEnd = () => {
1354
+ if (document.activeElement !== el) el.focus();
1355
+ const selection = window.getSelection();
1356
+ if (!selection) return;
1357
+ const range = document.createRange();
1358
+ range.selectNodeContents(el);
1359
+ range.collapse(false);
1360
+ selection.removeAllRanges();
1361
+ selection.addRange(range);
1362
+ };
1299
1363
  const clickHandler = (e) => {
1300
- const isLink = el.tagName === "A" || e.target.closest("a");
1301
- if (isLink) {
1364
+ e.stopPropagation();
1365
+ const inAnchor = el.tagName === "A" || !!e.target.closest("a");
1366
+ if (inAnchor) {
1302
1367
  e.preventDefault();
1303
- e.stopPropagation();
1304
- el.focus();
1305
1368
  }
1306
1369
  };
1307
1370
  const sectionSlug = el.getAttribute("data-cms-section") || this.currentSlug;
@@ -1323,6 +1386,16 @@ var _XtroedgeCMS = class _XtroedgeCMS {
1323
1386
  static stopProp(e) {
1324
1387
  e.stopPropagation();
1325
1388
  }
1389
+ focusEditableEnd(el) {
1390
+ if (document.activeElement !== el) el.focus();
1391
+ const selection = window.getSelection();
1392
+ if (!selection) return;
1393
+ const range = document.createRange();
1394
+ range.selectNodeContents(el);
1395
+ range.collapse(false);
1396
+ selection.removeAllRanges();
1397
+ selection.addRange(range);
1398
+ }
1326
1399
  enableElementEdit(el, _key, blurH, keyH, inputH, clickH) {
1327
1400
  const val = this.getPageText(_key);
1328
1401
  if (val) this.setElementContent(el, val);
@@ -1359,6 +1432,24 @@ var _XtroedgeCMS = class _XtroedgeCMS {
1359
1432
  applyEditMode(editMode) {
1360
1433
  document.body.classList.toggle("lcms-editing", editMode);
1361
1434
  if (editMode) {
1435
+ this.editClickCaptureHandler = (e) => {
1436
+ const target = e.target;
1437
+ if (!target) return;
1438
+ const editable = target.closest('[contenteditable="true"]');
1439
+ const imageTarget = target.closest("[data-cms-image-key], img[data-cms-key], [data-cms-key]");
1440
+ const isManagedEditable = !!editable && this.managedElements.has(editable);
1441
+ const isManagedImage = !!imageTarget && this.managedImages.has(imageTarget);
1442
+ const isManagedTarget = isManagedEditable || isManagedImage || !!editable || !!imageTarget;
1443
+ if (isManagedTarget) {
1444
+ e.stopPropagation();
1445
+ const anchor = target.closest("a");
1446
+ if (anchor) e.preventDefault();
1447
+ }
1448
+ };
1449
+ document.addEventListener("click", this.editClickCaptureHandler, true);
1450
+ document.addEventListener("mousedown", this.editClickCaptureHandler, true);
1451
+ document.addEventListener("pointerdown", this.editClickCaptureHandler, true);
1452
+ document.addEventListener("touchstart", this.editClickCaptureHandler, true);
1362
1453
  this.editScrollHandler = () => {
1363
1454
  if (this.editScrollRAF) return;
1364
1455
  this.editScrollRAF = requestAnimationFrame(() => {
@@ -1369,6 +1460,13 @@ var _XtroedgeCMS = class _XtroedgeCMS {
1369
1460
  window.addEventListener("scroll", this.editScrollHandler, { passive: true });
1370
1461
  this.syncEditablePointerEvents();
1371
1462
  } else {
1463
+ if (this.editClickCaptureHandler) {
1464
+ document.removeEventListener("click", this.editClickCaptureHandler, true);
1465
+ document.removeEventListener("mousedown", this.editClickCaptureHandler, true);
1466
+ document.removeEventListener("pointerdown", this.editClickCaptureHandler, true);
1467
+ document.removeEventListener("touchstart", this.editClickCaptureHandler, true);
1468
+ this.editClickCaptureHandler = null;
1469
+ }
1372
1470
  if (this.editScrollHandler) {
1373
1471
  window.removeEventListener("scroll", this.editScrollHandler);
1374
1472
  this.editScrollHandler = null;
@@ -1531,6 +1629,11 @@ var _XtroedgeCMS = class _XtroedgeCMS {
1531
1629
  }
1532
1630
  }
1533
1631
  this.observer?.observe(document.body, { childList: true, subtree: true });
1632
+ setTimeout(() => {
1633
+ if (this.editMode) {
1634
+ this.applyEditMode(true);
1635
+ }
1636
+ }, 200);
1534
1637
  }
1535
1638
  cleanupManagedElements() {
1536
1639
  for (const [el] of this.managedElements) this.detachElement(el);
@@ -1572,7 +1675,10 @@ var _XtroedgeCMS = class _XtroedgeCMS {
1572
1675
  if (editMode) this.enableImageEdit(img, info.ctxHandler);
1573
1676
  else this.disableImageEdit(img, info.ctxHandler);
1574
1677
  }
1575
- if (!editMode) this.dismissImageCtxMenu();
1678
+ if (!editMode) {
1679
+ this.dismissImageCtxMenu();
1680
+ this.dismissFloatingEditDialog();
1681
+ }
1576
1682
  }
1577
1683
  enableImageEdit(img, ctxHandler) {
1578
1684
  if (!img.dataset.origTitle) img.dataset.origTitle = img.title || "";
@@ -1649,6 +1755,123 @@ var _XtroedgeCMS = class _XtroedgeCMS {
1649
1755
  this.imageCtxMenu = null;
1650
1756
  }
1651
1757
  }
1758
+ showFloatingEditDialog(el, key) {
1759
+ this.dismissFloatingEditDialog();
1760
+ const currentText = el.textContent || "";
1761
+ const rect = el.getBoundingClientRect();
1762
+ const dialog = document.createElement("div");
1763
+ Object.assign(dialog.style, {
1764
+ position: "fixed",
1765
+ zIndex: "99999",
1766
+ left: `${Math.max(20, rect.left)}px`,
1767
+ top: `${Math.max(20, rect.top - 120)}px`,
1768
+ background: "rgba(15, 10, 40, 0.95)",
1769
+ backdropFilter: "blur(20px)",
1770
+ webkitBackdropFilter: "blur(20px)",
1771
+ border: "2px solid rgba(139, 92, 246, 0.5)",
1772
+ borderRadius: "12px",
1773
+ padding: "16px",
1774
+ boxShadow: "0 12px 48px rgba(0, 0, 0, 0.6)",
1775
+ minWidth: "320px",
1776
+ maxWidth: "500px"
1777
+ });
1778
+ const title = document.createElement("div");
1779
+ title.textContent = "Edit Text";
1780
+ Object.assign(title.style, {
1781
+ color: "rgba(255, 255, 255, 0.9)",
1782
+ fontSize: "13px",
1783
+ fontWeight: "600",
1784
+ marginBottom: "12px",
1785
+ fontFamily: "system-ui, sans-serif"
1786
+ });
1787
+ const textarea = document.createElement("textarea");
1788
+ textarea.value = currentText;
1789
+ Object.assign(textarea.style, {
1790
+ width: "100%",
1791
+ minHeight: "80px",
1792
+ background: "rgba(255, 255, 255, 0.08)",
1793
+ border: "1px solid rgba(139, 92, 246, 0.3)",
1794
+ borderRadius: "8px",
1795
+ color: "white",
1796
+ padding: "10px",
1797
+ fontSize: "14px",
1798
+ fontFamily: "system-ui, sans-serif",
1799
+ resize: "vertical",
1800
+ outline: "none"
1801
+ });
1802
+ textarea.addEventListener("focus", () => {
1803
+ textarea.style.borderColor = this.highlightColor;
1804
+ });
1805
+ textarea.addEventListener("blur", () => {
1806
+ textarea.style.borderColor = "rgba(139, 92, 246, 0.3)";
1807
+ });
1808
+ const buttons = document.createElement("div");
1809
+ Object.assign(buttons.style, {
1810
+ display: "flex",
1811
+ gap: "8px",
1812
+ marginTop: "12px",
1813
+ justifyContent: "flex-end"
1814
+ });
1815
+ const cancelBtn = document.createElement("button");
1816
+ cancelBtn.textContent = "Cancel";
1817
+ Object.assign(cancelBtn.style, {
1818
+ padding: "8px 16px",
1819
+ background: "rgba(255, 255, 255, 0.1)",
1820
+ border: "none",
1821
+ borderRadius: "6px",
1822
+ color: "rgba(255, 255, 255, 0.7)",
1823
+ fontSize: "13px",
1824
+ fontWeight: "500",
1825
+ cursor: "pointer",
1826
+ fontFamily: "system-ui, sans-serif"
1827
+ });
1828
+ cancelBtn.addEventListener("click", () => this.dismissFloatingEditDialog());
1829
+ cancelBtn.addEventListener("mouseenter", () => cancelBtn.style.background = "rgba(255, 255, 255, 0.15)");
1830
+ cancelBtn.addEventListener("mouseleave", () => cancelBtn.style.background = "rgba(255, 255, 255, 0.1)");
1831
+ const saveBtn = document.createElement("button");
1832
+ saveBtn.textContent = "Save";
1833
+ Object.assign(saveBtn.style, {
1834
+ padding: "8px 16px",
1835
+ background: this.highlightColor,
1836
+ border: "none",
1837
+ borderRadius: "6px",
1838
+ color: "white",
1839
+ fontSize: "13px",
1840
+ fontWeight: "600",
1841
+ cursor: "pointer",
1842
+ fontFamily: "system-ui, sans-serif"
1843
+ });
1844
+ saveBtn.addEventListener("click", () => {
1845
+ const newText = textarea.value;
1846
+ this.setElementContent(el, newText);
1847
+ this.onTextChanged(key, newText);
1848
+ this.dismissFloatingEditDialog();
1849
+ });
1850
+ saveBtn.addEventListener("mouseenter", () => saveBtn.style.opacity = "0.9");
1851
+ saveBtn.addEventListener("mouseleave", () => saveBtn.style.opacity = "1");
1852
+ buttons.appendChild(cancelBtn);
1853
+ buttons.appendChild(saveBtn);
1854
+ dialog.appendChild(title);
1855
+ dialog.appendChild(textarea);
1856
+ dialog.appendChild(buttons);
1857
+ (this.rootEl || document.body).appendChild(dialog);
1858
+ this.floatingEditDialog = dialog;
1859
+ textarea.focus();
1860
+ textarea.select();
1861
+ const closeHandler = (e) => {
1862
+ if (!dialog.contains(e.target)) {
1863
+ this.dismissFloatingEditDialog();
1864
+ document.removeEventListener("mousedown", closeHandler);
1865
+ }
1866
+ };
1867
+ setTimeout(() => document.addEventListener("mousedown", closeHandler), 0);
1868
+ }
1869
+ dismissFloatingEditDialog() {
1870
+ if (this.floatingEditDialog) {
1871
+ this.floatingEditDialog.remove();
1872
+ this.floatingEditDialog = null;
1873
+ }
1874
+ }
1652
1875
  onImageFileSelected(event) {
1653
1876
  const input = event.target;
1654
1877
  const file = input.files?.[0];