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.js CHANGED
@@ -423,6 +423,12 @@ body.lcms-editing [data-cms] {
423
423
  -webkit-user-select: text !important;
424
424
  cursor: text !important;
425
425
  }
426
+
427
+ /* Ensure editable elements appear above other content */
428
+ body.lcms-editing [contenteditable="true"] {
429
+ position: relative !important;
430
+ z-index: 9999 !important;
431
+ }
426
432
  `;
427
433
 
428
434
  // src/xtroedge-cms.ts
@@ -509,6 +515,7 @@ var _XtroedgeCMS = class _XtroedgeCMS {
509
515
  this.scanTimeout = null;
510
516
  this.activeImageEl = null;
511
517
  this.imageCtxMenu = null;
518
+ this.floatingEditDialog = null;
512
519
  // ===== Rich Text Toolbar =====
513
520
  this.richToolbarEl = null;
514
521
  this.activeEditableEl = null;
@@ -546,6 +553,7 @@ var _XtroedgeCMS = class _XtroedgeCMS {
546
553
  // ===== Edit-mode visibility tracking =====
547
554
  this.editScrollHandler = null;
548
555
  this.editScrollRAF = null;
556
+ this.editClickCaptureHandler = null;
549
557
  // ===== FAB Drag =====
550
558
  this.posX = 20;
551
559
  this.posY = 20;
@@ -672,6 +680,26 @@ var _XtroedgeCMS = class _XtroedgeCMS {
672
680
  this.observer.observe(document.body, { childList: true, subtree: true });
673
681
  window.addEventListener("popstate", this.boundPopState);
674
682
  window.addEventListener("hashchange", this.boundHashChange);
683
+ document.addEventListener("click", (e) => {
684
+ if (!document.body.classList.contains("lcms-editing")) return;
685
+ const target = e.target;
686
+ const anchor = target.closest("a");
687
+ if (anchor) {
688
+ let editableEl = null;
689
+ if (target.getAttribute("contenteditable") === "true") {
690
+ editableEl = target;
691
+ } else {
692
+ editableEl = target.closest('[contenteditable="true"]') || anchor.querySelector('[contenteditable="true"]');
693
+ }
694
+ if (editableEl) {
695
+ e.preventDefault();
696
+ e.stopPropagation();
697
+ setTimeout(() => {
698
+ editableEl.focus();
699
+ }, 0);
700
+ }
701
+ }
702
+ }, true);
675
703
  this.handleNavigation();
676
704
  }
677
705
  destroy() {
@@ -692,6 +720,14 @@ var _XtroedgeCMS = class _XtroedgeCMS {
692
720
  if (this.toastTimer) clearTimeout(this.toastTimer);
693
721
  this.db?.close();
694
722
  }
723
+ /**
724
+ * Manually trigger a rescan of the DOM to detect new editable elements.
725
+ * Useful when elements are dynamically shown/hidden or added to the page.
726
+ */
727
+ rescan() {
728
+ if (this.scanTimeout) clearTimeout(this.scanTimeout);
729
+ this.autoDetectAndScan();
730
+ }
695
731
  // 24 hours
696
732
  async validateLicense() {
697
733
  const licenseKey = this.config.licenseKey || _XtroedgeCMS.secureGet("builder_token") || "";
@@ -1191,13 +1227,20 @@ var _XtroedgeCMS = class _XtroedgeCMS {
1191
1227
  return this.currentSlug;
1192
1228
  };
1193
1229
  const tagCounters = {};
1230
+ const hasCmsKey = (candidate) => {
1231
+ return Array.from(document.querySelectorAll("[data-cms]")).some((node) => node.getAttribute("data-cms") === candidate);
1232
+ };
1194
1233
  const assignKey = (el, sectionSlug) => {
1195
1234
  const sectionKey = sectionSlug === "/header" ? "header" : sectionSlug === "/footer" ? "footer" : "page";
1196
1235
  if (!tagCounters[sectionKey]) tagCounters[sectionKey] = {};
1197
1236
  const tag = el.tagName.toLowerCase();
1198
1237
  if (!tagCounters[sectionKey][tag]) tagCounters[sectionKey][tag] = 0;
1199
1238
  const prefix = sectionKey !== "page" ? `${sectionKey}_` : "";
1200
- const key = `${prefix}xcms_${tag}_${tagCounters[sectionKey][tag]}`;
1239
+ let key = `${prefix}xcms_${tag}_${tagCounters[sectionKey][tag]}`;
1240
+ while (hasCmsKey(key)) {
1241
+ tagCounters[sectionKey][tag]++;
1242
+ key = `${prefix}xcms_${tag}_${tagCounters[sectionKey][tag]}`;
1243
+ }
1201
1244
  tagCounters[sectionKey][tag]++;
1202
1245
  el.setAttribute("data-cms", key);
1203
1246
  el.setAttribute("data-cms-section", sectionSlug);
@@ -1224,9 +1267,21 @@ var _XtroedgeCMS = class _XtroedgeCMS {
1224
1267
  }
1225
1268
  scanDOM() {
1226
1269
  const elements = document.querySelectorAll("[data-cms]");
1270
+ const seenKeys = /* @__PURE__ */ new Set();
1227
1271
  elements.forEach((el) => {
1228
1272
  if (el.closest("#xtroedge-cms-root, #lcms-login-modal")) return;
1229
- const key = el.getAttribute("data-cms");
1273
+ let key = el.getAttribute("data-cms");
1274
+ if (seenKeys.has(key)) {
1275
+ let idx = 1;
1276
+ let repaired = `${key}__${idx}`;
1277
+ while (document.querySelector(`[data-cms="${repaired}"]`)) {
1278
+ idx++;
1279
+ repaired = `${key}__${idx}`;
1280
+ }
1281
+ el.setAttribute("data-cms", repaired);
1282
+ key = repaired;
1283
+ }
1284
+ seenKeys.add(key);
1230
1285
  this.registeredKeys.add(key);
1231
1286
  if (!this.managedElements.has(el)) {
1232
1287
  this.attachElement(el, key);
@@ -1274,21 +1329,20 @@ var _XtroedgeCMS = class _XtroedgeCMS {
1274
1329
  return !!el.querySelector("[data-cms]");
1275
1330
  }
1276
1331
  getElementContent(el) {
1277
- if (this.hasEditableChildren(el)) {
1278
- return this.getDirectTextContent(el).trim();
1279
- }
1280
- return this.richTextEnabled ? el.innerHTML?.trim() || "" : el.textContent?.trim() || "";
1332
+ if (this.richTextEnabled) return el.innerHTML?.trim() || "";
1333
+ if (this.hasEditableChildren(el)) return this.getDirectTextContent(el).trim();
1334
+ return el.textContent?.trim() || "";
1281
1335
  }
1282
1336
  setElementContent(el, val) {
1283
- if (this.hasEditableChildren(el)) {
1337
+ if (this.richTextEnabled) {
1338
+ el.innerHTML = this.sanitizeHTML(val);
1339
+ } else if (this.hasEditableChildren(el)) {
1284
1340
  const textNodes = [];
1285
1341
  for (let i = 0; i < el.childNodes.length; i++) {
1286
1342
  if (el.childNodes[i].nodeType === Node.TEXT_NODE) textNodes.push(el.childNodes[i]);
1287
1343
  }
1288
1344
  if (textNodes.length > 0) textNodes[0].textContent = val;
1289
1345
  else el.prepend(document.createTextNode(val));
1290
- } else if (this.richTextEnabled) {
1291
- el.innerHTML = this.sanitizeHTML(val);
1292
1346
  } else {
1293
1347
  el.textContent = val;
1294
1348
  }
@@ -1322,12 +1376,21 @@ var _XtroedgeCMS = class _XtroedgeCMS {
1322
1376
  const currentVal = this.getPageText(key);
1323
1377
  if (text !== currentVal) this.onTextChanged(key, text);
1324
1378
  };
1379
+ const focusEditableEnd = () => {
1380
+ if (document.activeElement !== el) el.focus();
1381
+ const selection = window.getSelection();
1382
+ if (!selection) return;
1383
+ const range = document.createRange();
1384
+ range.selectNodeContents(el);
1385
+ range.collapse(false);
1386
+ selection.removeAllRanges();
1387
+ selection.addRange(range);
1388
+ };
1325
1389
  const clickHandler = (e) => {
1326
- const isLink = el.tagName === "A" || e.target.closest("a");
1327
- if (isLink) {
1390
+ e.stopPropagation();
1391
+ const inAnchor = el.tagName === "A" || !!e.target.closest("a");
1392
+ if (inAnchor) {
1328
1393
  e.preventDefault();
1329
- e.stopPropagation();
1330
- el.focus();
1331
1394
  }
1332
1395
  };
1333
1396
  const sectionSlug = el.getAttribute("data-cms-section") || this.currentSlug;
@@ -1349,6 +1412,16 @@ var _XtroedgeCMS = class _XtroedgeCMS {
1349
1412
  static stopProp(e) {
1350
1413
  e.stopPropagation();
1351
1414
  }
1415
+ focusEditableEnd(el) {
1416
+ if (document.activeElement !== el) el.focus();
1417
+ const selection = window.getSelection();
1418
+ if (!selection) return;
1419
+ const range = document.createRange();
1420
+ range.selectNodeContents(el);
1421
+ range.collapse(false);
1422
+ selection.removeAllRanges();
1423
+ selection.addRange(range);
1424
+ }
1352
1425
  enableElementEdit(el, _key, blurH, keyH, inputH, clickH) {
1353
1426
  const val = this.getPageText(_key);
1354
1427
  if (val) this.setElementContent(el, val);
@@ -1385,6 +1458,24 @@ var _XtroedgeCMS = class _XtroedgeCMS {
1385
1458
  applyEditMode(editMode) {
1386
1459
  document.body.classList.toggle("lcms-editing", editMode);
1387
1460
  if (editMode) {
1461
+ this.editClickCaptureHandler = (e) => {
1462
+ const target = e.target;
1463
+ if (!target) return;
1464
+ const editable = target.closest('[contenteditable="true"]');
1465
+ const imageTarget = target.closest("[data-cms-image-key], img[data-cms-key], [data-cms-key]");
1466
+ const isManagedEditable = !!editable && this.managedElements.has(editable);
1467
+ const isManagedImage = !!imageTarget && this.managedImages.has(imageTarget);
1468
+ const isManagedTarget = isManagedEditable || isManagedImage || !!editable || !!imageTarget;
1469
+ if (isManagedTarget) {
1470
+ e.stopPropagation();
1471
+ const anchor = target.closest("a");
1472
+ if (anchor) e.preventDefault();
1473
+ }
1474
+ };
1475
+ document.addEventListener("click", this.editClickCaptureHandler, true);
1476
+ document.addEventListener("mousedown", this.editClickCaptureHandler, true);
1477
+ document.addEventListener("pointerdown", this.editClickCaptureHandler, true);
1478
+ document.addEventListener("touchstart", this.editClickCaptureHandler, true);
1388
1479
  this.editScrollHandler = () => {
1389
1480
  if (this.editScrollRAF) return;
1390
1481
  this.editScrollRAF = requestAnimationFrame(() => {
@@ -1395,6 +1486,13 @@ var _XtroedgeCMS = class _XtroedgeCMS {
1395
1486
  window.addEventListener("scroll", this.editScrollHandler, { passive: true });
1396
1487
  this.syncEditablePointerEvents();
1397
1488
  } else {
1489
+ if (this.editClickCaptureHandler) {
1490
+ document.removeEventListener("click", this.editClickCaptureHandler, true);
1491
+ document.removeEventListener("mousedown", this.editClickCaptureHandler, true);
1492
+ document.removeEventListener("pointerdown", this.editClickCaptureHandler, true);
1493
+ document.removeEventListener("touchstart", this.editClickCaptureHandler, true);
1494
+ this.editClickCaptureHandler = null;
1495
+ }
1398
1496
  if (this.editScrollHandler) {
1399
1497
  window.removeEventListener("scroll", this.editScrollHandler);
1400
1498
  this.editScrollHandler = null;
@@ -1557,6 +1655,11 @@ var _XtroedgeCMS = class _XtroedgeCMS {
1557
1655
  }
1558
1656
  }
1559
1657
  this.observer?.observe(document.body, { childList: true, subtree: true });
1658
+ setTimeout(() => {
1659
+ if (this.editMode) {
1660
+ this.applyEditMode(true);
1661
+ }
1662
+ }, 200);
1560
1663
  }
1561
1664
  cleanupManagedElements() {
1562
1665
  for (const [el] of this.managedElements) this.detachElement(el);
@@ -1598,7 +1701,10 @@ var _XtroedgeCMS = class _XtroedgeCMS {
1598
1701
  if (editMode) this.enableImageEdit(img, info.ctxHandler);
1599
1702
  else this.disableImageEdit(img, info.ctxHandler);
1600
1703
  }
1601
- if (!editMode) this.dismissImageCtxMenu();
1704
+ if (!editMode) {
1705
+ this.dismissImageCtxMenu();
1706
+ this.dismissFloatingEditDialog();
1707
+ }
1602
1708
  }
1603
1709
  enableImageEdit(img, ctxHandler) {
1604
1710
  if (!img.dataset.origTitle) img.dataset.origTitle = img.title || "";
@@ -1675,6 +1781,123 @@ var _XtroedgeCMS = class _XtroedgeCMS {
1675
1781
  this.imageCtxMenu = null;
1676
1782
  }
1677
1783
  }
1784
+ showFloatingEditDialog(el, key) {
1785
+ this.dismissFloatingEditDialog();
1786
+ const currentText = el.textContent || "";
1787
+ const rect = el.getBoundingClientRect();
1788
+ const dialog = document.createElement("div");
1789
+ Object.assign(dialog.style, {
1790
+ position: "fixed",
1791
+ zIndex: "99999",
1792
+ left: `${Math.max(20, rect.left)}px`,
1793
+ top: `${Math.max(20, rect.top - 120)}px`,
1794
+ background: "rgba(15, 10, 40, 0.95)",
1795
+ backdropFilter: "blur(20px)",
1796
+ webkitBackdropFilter: "blur(20px)",
1797
+ border: "2px solid rgba(139, 92, 246, 0.5)",
1798
+ borderRadius: "12px",
1799
+ padding: "16px",
1800
+ boxShadow: "0 12px 48px rgba(0, 0, 0, 0.6)",
1801
+ minWidth: "320px",
1802
+ maxWidth: "500px"
1803
+ });
1804
+ const title = document.createElement("div");
1805
+ title.textContent = "Edit Text";
1806
+ Object.assign(title.style, {
1807
+ color: "rgba(255, 255, 255, 0.9)",
1808
+ fontSize: "13px",
1809
+ fontWeight: "600",
1810
+ marginBottom: "12px",
1811
+ fontFamily: "system-ui, sans-serif"
1812
+ });
1813
+ const textarea = document.createElement("textarea");
1814
+ textarea.value = currentText;
1815
+ Object.assign(textarea.style, {
1816
+ width: "100%",
1817
+ minHeight: "80px",
1818
+ background: "rgba(255, 255, 255, 0.08)",
1819
+ border: "1px solid rgba(139, 92, 246, 0.3)",
1820
+ borderRadius: "8px",
1821
+ color: "white",
1822
+ padding: "10px",
1823
+ fontSize: "14px",
1824
+ fontFamily: "system-ui, sans-serif",
1825
+ resize: "vertical",
1826
+ outline: "none"
1827
+ });
1828
+ textarea.addEventListener("focus", () => {
1829
+ textarea.style.borderColor = this.highlightColor;
1830
+ });
1831
+ textarea.addEventListener("blur", () => {
1832
+ textarea.style.borderColor = "rgba(139, 92, 246, 0.3)";
1833
+ });
1834
+ const buttons = document.createElement("div");
1835
+ Object.assign(buttons.style, {
1836
+ display: "flex",
1837
+ gap: "8px",
1838
+ marginTop: "12px",
1839
+ justifyContent: "flex-end"
1840
+ });
1841
+ const cancelBtn = document.createElement("button");
1842
+ cancelBtn.textContent = "Cancel";
1843
+ Object.assign(cancelBtn.style, {
1844
+ padding: "8px 16px",
1845
+ background: "rgba(255, 255, 255, 0.1)",
1846
+ border: "none",
1847
+ borderRadius: "6px",
1848
+ color: "rgba(255, 255, 255, 0.7)",
1849
+ fontSize: "13px",
1850
+ fontWeight: "500",
1851
+ cursor: "pointer",
1852
+ fontFamily: "system-ui, sans-serif"
1853
+ });
1854
+ cancelBtn.addEventListener("click", () => this.dismissFloatingEditDialog());
1855
+ cancelBtn.addEventListener("mouseenter", () => cancelBtn.style.background = "rgba(255, 255, 255, 0.15)");
1856
+ cancelBtn.addEventListener("mouseleave", () => cancelBtn.style.background = "rgba(255, 255, 255, 0.1)");
1857
+ const saveBtn = document.createElement("button");
1858
+ saveBtn.textContent = "Save";
1859
+ Object.assign(saveBtn.style, {
1860
+ padding: "8px 16px",
1861
+ background: this.highlightColor,
1862
+ border: "none",
1863
+ borderRadius: "6px",
1864
+ color: "white",
1865
+ fontSize: "13px",
1866
+ fontWeight: "600",
1867
+ cursor: "pointer",
1868
+ fontFamily: "system-ui, sans-serif"
1869
+ });
1870
+ saveBtn.addEventListener("click", () => {
1871
+ const newText = textarea.value;
1872
+ this.setElementContent(el, newText);
1873
+ this.onTextChanged(key, newText);
1874
+ this.dismissFloatingEditDialog();
1875
+ });
1876
+ saveBtn.addEventListener("mouseenter", () => saveBtn.style.opacity = "0.9");
1877
+ saveBtn.addEventListener("mouseleave", () => saveBtn.style.opacity = "1");
1878
+ buttons.appendChild(cancelBtn);
1879
+ buttons.appendChild(saveBtn);
1880
+ dialog.appendChild(title);
1881
+ dialog.appendChild(textarea);
1882
+ dialog.appendChild(buttons);
1883
+ (this.rootEl || document.body).appendChild(dialog);
1884
+ this.floatingEditDialog = dialog;
1885
+ textarea.focus();
1886
+ textarea.select();
1887
+ const closeHandler = (e) => {
1888
+ if (!dialog.contains(e.target)) {
1889
+ this.dismissFloatingEditDialog();
1890
+ document.removeEventListener("mousedown", closeHandler);
1891
+ }
1892
+ };
1893
+ setTimeout(() => document.addEventListener("mousedown", closeHandler), 0);
1894
+ }
1895
+ dismissFloatingEditDialog() {
1896
+ if (this.floatingEditDialog) {
1897
+ this.floatingEditDialog.remove();
1898
+ this.floatingEditDialog = null;
1899
+ }
1900
+ }
1678
1901
  onImageFileSelected(event) {
1679
1902
  const input = event.target;
1680
1903
  const file = input.files?.[0];