ngx-xtroedge-cms 1.3.17 → 1.4.0
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/LICENSE +46 -0
- package/README.md +1 -0
- package/dist/index.d.mts +94 -6
- package/dist/index.d.ts +94 -6
- package/dist/index.global.js +1 -327
- package/dist/index.js +850 -53
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +850 -53
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -304,6 +304,16 @@ var CMS_STYLES = `
|
|
|
304
304
|
}
|
|
305
305
|
.lcms-login-input:focus { border-color: var(--lcms-primary, #00C853); }
|
|
306
306
|
.lcms-login-input::placeholder { color: rgba(255,255,255,0.25); }
|
|
307
|
+
.lcms-password-wrapper { position: relative; display: flex; align-items: center; }
|
|
308
|
+
.lcms-password-wrapper .lcms-login-input { padding-right: 40px; width: 100%; }
|
|
309
|
+
.lcms-password-toggle {
|
|
310
|
+
position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
|
|
311
|
+
background: none; border: none !important; outline: none !important; cursor: pointer; padding: 4px;
|
|
312
|
+
color: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center;
|
|
313
|
+
transition: color 0.2s; line-height: 0; z-index: 1;
|
|
314
|
+
}
|
|
315
|
+
.lcms-password-toggle:hover { color: rgba(0,0,0,0.8); }
|
|
316
|
+
.lcms-password-toggle svg { display: block; }
|
|
307
317
|
.lcms-login-btn {
|
|
308
318
|
width: 100%; padding: 11px; margin-top: 6px; border: none; border-radius: 8px; cursor: pointer;
|
|
309
319
|
background: linear-gradient(135deg, var(--lcms-primary, #00C853), var(--lcms-primary-dark, #2E7D32));
|
|
@@ -319,8 +329,100 @@ var CMS_STYLES = `
|
|
|
319
329
|
}
|
|
320
330
|
.lcms-login-error.visible { display: block; }
|
|
321
331
|
|
|
332
|
+
/* LICENSE OVERLAY */
|
|
333
|
+
.lcms-license-overlay {
|
|
334
|
+
position: fixed; inset: 0; z-index: 10020;
|
|
335
|
+
display: flex; align-items: center; justify-content: center;
|
|
336
|
+
background: rgba(8, 8, 15, 0.85);
|
|
337
|
+
backdrop-filter: blur(20px) saturate(1.4);
|
|
338
|
+
-webkit-backdrop-filter: blur(20px) saturate(1.4);
|
|
339
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
340
|
+
}
|
|
341
|
+
.lcms-license-box {
|
|
342
|
+
background: #13151a; border: 1px solid rgba(255,255,255,0.1);
|
|
343
|
+
border-radius: 20px; padding: 40px 36px; width: 380px; text-align: center;
|
|
344
|
+
box-shadow: 0 24px 60px rgba(0,0,0,0.6);
|
|
345
|
+
}
|
|
346
|
+
.lcms-license-icon {
|
|
347
|
+
width: 56px; height: 56px; border-radius: 16px; margin: 0 auto 20px;
|
|
348
|
+
background: linear-gradient(135deg, rgba(239,68,68,0.2), rgba(239,68,68,0.1));
|
|
349
|
+
border: 1px solid rgba(239,68,68,0.3);
|
|
350
|
+
display: flex; align-items: center; justify-content: center;
|
|
351
|
+
}
|
|
352
|
+
.lcms-license-icon.trial {
|
|
353
|
+
background: linear-gradient(135deg, rgba(251,191,36,0.2), rgba(251,191,36,0.1));
|
|
354
|
+
border-color: rgba(251,191,36,0.3);
|
|
355
|
+
}
|
|
356
|
+
.lcms-license-title {
|
|
357
|
+
color: white; font-size: 20px; font-weight: 700; margin-bottom: 8px;
|
|
358
|
+
}
|
|
359
|
+
.lcms-license-msg {
|
|
360
|
+
color: rgba(255,255,255,0.5); font-size: 13px; line-height: 1.6; margin-bottom: 24px;
|
|
361
|
+
}
|
|
362
|
+
.lcms-license-btn {
|
|
363
|
+
display: inline-block; padding: 11px 28px; border: none; border-radius: 8px; cursor: pointer;
|
|
364
|
+
background: linear-gradient(135deg, var(--lcms-primary, #00C853), var(--lcms-primary-dark, #2E7D32));
|
|
365
|
+
color: white; font-size: 13px; font-weight: 700; font-family: inherit;
|
|
366
|
+
letter-spacing: 0.3px; transition: filter 0.2s; text-decoration: none;
|
|
367
|
+
}
|
|
368
|
+
.lcms-license-btn:hover { filter: brightness(1.1); }
|
|
369
|
+
.lcms-license-sub {
|
|
370
|
+
color: rgba(255,255,255,0.3); font-size: 11px; margin-top: 16px;
|
|
371
|
+
}
|
|
372
|
+
.lcms-license-sub a { color: rgba(var(--lcms-primary-rgb, 0,200,83),0.7); text-decoration: none; }
|
|
373
|
+
.lcms-license-sub a:hover { text-decoration: underline; }
|
|
374
|
+
|
|
375
|
+
/* TRIAL BANNER */
|
|
376
|
+
.lcms-trial-banner {
|
|
377
|
+
display: flex; align-items: center; gap: 6px;
|
|
378
|
+
background: rgba(251,191,36,0.12); border: 1px solid rgba(251,191,36,0.3);
|
|
379
|
+
border-radius: 8px; padding: 6px 10px; margin-bottom: 10px;
|
|
380
|
+
font-size: 11px; font-weight: 600; color: #fbbf24;
|
|
381
|
+
}
|
|
382
|
+
.lcms-trial-banner-icon { font-size: 14px; }
|
|
383
|
+
.lcms-trial-banner-text { flex: 1; }
|
|
384
|
+
.lcms-trial-banner-dismiss {
|
|
385
|
+
background: none; border: none; color: rgba(251,191,36,0.5); cursor: pointer;
|
|
386
|
+
padding: 0; font-size: 14px; line-height: 1; transition: color 0.2s;
|
|
387
|
+
}
|
|
388
|
+
.lcms-trial-banner-dismiss:hover { color: #fbbf24; }
|
|
389
|
+
|
|
390
|
+
/* RICH TEXT TOOLBAR */
|
|
391
|
+
.lcms-rich-toolbar {
|
|
392
|
+
position: fixed; z-index: 10010;
|
|
393
|
+
display: flex; align-items: center; gap: 2px;
|
|
394
|
+
padding: 4px 6px;
|
|
395
|
+
background: rgba(30, 15, 60, 0.92);
|
|
396
|
+
backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
|
|
397
|
+
border: 1px solid rgba(var(--lcms-primary-rgb, 0,200,83), 0.3);
|
|
398
|
+
border-radius: 8px;
|
|
399
|
+
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
|
400
|
+
opacity: 0; transform: translateY(8px);
|
|
401
|
+
transition: opacity 0.15s, transform 0.15s;
|
|
402
|
+
pointer-events: none;
|
|
403
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
404
|
+
}
|
|
405
|
+
.lcms-rich-toolbar.visible { opacity: 1; transform: translateY(0); pointer-events: auto; }
|
|
406
|
+
.lcms-rich-toolbar button {
|
|
407
|
+
width: 28px; height: 28px; border: none; border-radius: 4px;
|
|
408
|
+
background: transparent; color: #d0d0d0; cursor: pointer;
|
|
409
|
+
display: flex; align-items: center; justify-content: center;
|
|
410
|
+
font-size: 13px; font-weight: 700; padding: 0;
|
|
411
|
+
transition: background 0.15s, color 0.15s;
|
|
412
|
+
}
|
|
413
|
+
.lcms-rich-toolbar button:hover { background: rgba(var(--lcms-primary-rgb, 0,200,83), 0.3); color: #fff; }
|
|
414
|
+
.lcms-rich-toolbar button.active { background: rgba(var(--lcms-primary-rgb, 0,200,83), 0.5); color: #fff; }
|
|
415
|
+
.lcms-rich-toolbar .lcms-tb-sep { width: 1px; height: 18px; background: rgba(255,255,255,0.12); margin: 0 3px; flex-shrink: 0; }
|
|
416
|
+
|
|
322
417
|
/* HIDDEN ELEMENTS */
|
|
323
418
|
.lcms-hidden { display: none !important; }
|
|
419
|
+
|
|
420
|
+
/* EDIT MODE \u2014 keep animations running, just make CMS elements editable */
|
|
421
|
+
body.lcms-editing [data-cms] {
|
|
422
|
+
user-select: text !important;
|
|
423
|
+
-webkit-user-select: text !important;
|
|
424
|
+
cursor: text !important;
|
|
425
|
+
}
|
|
324
426
|
`;
|
|
325
427
|
|
|
326
428
|
// src/xtroedge-cms.ts
|
|
@@ -364,7 +466,7 @@ var DEFAULT_EDITABLE_TAGS = [
|
|
|
364
466
|
"header",
|
|
365
467
|
"footer"
|
|
366
468
|
];
|
|
367
|
-
var
|
|
469
|
+
var _XtroedgeCMS = class _XtroedgeCMS {
|
|
368
470
|
constructor(config) {
|
|
369
471
|
this.brandingEl = null;
|
|
370
472
|
this.siteIdentifier = "";
|
|
@@ -399,6 +501,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
399
501
|
this.dirtyImageKeys = /* @__PURE__ */ new Set();
|
|
400
502
|
this.registeredKeys = /* @__PURE__ */ new Set();
|
|
401
503
|
// ===== DOM =====
|
|
504
|
+
this.domOriginals = {};
|
|
402
505
|
this.managedElements = /* @__PURE__ */ new Map();
|
|
403
506
|
this.managedImages = /* @__PURE__ */ new Map();
|
|
404
507
|
this.autoDetectedElements = /* @__PURE__ */ new Set();
|
|
@@ -406,6 +509,11 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
406
509
|
this.scanTimeout = null;
|
|
407
510
|
this.activeImageEl = null;
|
|
408
511
|
this.imageCtxMenu = null;
|
|
512
|
+
// ===== Rich Text Toolbar =====
|
|
513
|
+
this.richToolbarEl = null;
|
|
514
|
+
this.activeEditableEl = null;
|
|
515
|
+
this.toolbarHideTimeout = null;
|
|
516
|
+
this.selectionChangeHandler = null;
|
|
409
517
|
// ===== Navigation =====
|
|
410
518
|
this.currentSlug = "";
|
|
411
519
|
this.currentTitle = "";
|
|
@@ -435,6 +543,9 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
435
543
|
this.editModeContent = null;
|
|
436
544
|
this.fileInput = null;
|
|
437
545
|
this.imgOverlay = null;
|
|
546
|
+
// ===== Edit-mode visibility tracking =====
|
|
547
|
+
this.editScrollHandler = null;
|
|
548
|
+
this.editScrollRAF = null;
|
|
438
549
|
// ===== FAB Drag =====
|
|
439
550
|
this.posX = 20;
|
|
440
551
|
this.posY = 20;
|
|
@@ -446,11 +557,19 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
446
557
|
this.hasMoved = false;
|
|
447
558
|
// ===== Timers =====
|
|
448
559
|
this.toastTimer = null;
|
|
560
|
+
// ===== License =====
|
|
561
|
+
this.licenseValid = false;
|
|
562
|
+
this.licenseStatus = null;
|
|
563
|
+
this.licenseOverlayEl = null;
|
|
564
|
+
this.trialBannerDismissed = false;
|
|
449
565
|
// ===== IndexedDB =====
|
|
450
566
|
this.db = null;
|
|
451
567
|
// ===== i18n cache =====
|
|
452
568
|
this.translationCache = /* @__PURE__ */ new Map();
|
|
569
|
+
/** Set of positioned ancestors we added pointer-events:none to (for cleanup) */
|
|
570
|
+
this.modifiedAncestors = /* @__PURE__ */ new Set();
|
|
453
571
|
this.config = config || {};
|
|
572
|
+
if (!this.config.apiBase) this.config.apiBase = "https://backend-xi-lime-d90e4p1ysf.vercel.app/api";
|
|
454
573
|
this.containerSelector = this.config.containerSelector || "";
|
|
455
574
|
this.editableTags = (this.config.editableTags || DEFAULT_EDITABLE_TAGS).map((t) => t.toLowerCase());
|
|
456
575
|
this.languages = this.config.languages || ["en"];
|
|
@@ -459,6 +578,8 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
459
578
|
this.historyRetentionMs = (this.config.historyRetentionDays || 7) * 24 * 60 * 60 * 1e3;
|
|
460
579
|
this.currentLang = this.detectCurrentLanguage();
|
|
461
580
|
this.siteIdentifier = this.resolveSiteIdentifier();
|
|
581
|
+
const savedClientApi = _XtroedgeCMS.secureGet("xtroedge_client_api");
|
|
582
|
+
if (savedClientApi) this.config.clientApi = savedClientApi;
|
|
462
583
|
const savedColor = localStorage.getItem("xtroedge_theme_color");
|
|
463
584
|
if (savedColor) this.highlightColor = savedColor;
|
|
464
585
|
this.boundMouseMove = (e) => this.onDragMove(e);
|
|
@@ -468,13 +589,69 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
468
589
|
this.boundPopState = () => this.handleNavigation();
|
|
469
590
|
this.boundHashChange = () => this.handleNavigation();
|
|
470
591
|
}
|
|
592
|
+
static _enc(value) {
|
|
593
|
+
const k = _XtroedgeCMS._SK;
|
|
594
|
+
let result = "";
|
|
595
|
+
for (let i = 0; i < value.length; i++) {
|
|
596
|
+
result += String.fromCharCode(value.charCodeAt(i) ^ k.charCodeAt(i % k.length));
|
|
597
|
+
}
|
|
598
|
+
return btoa(result);
|
|
599
|
+
}
|
|
600
|
+
static _dec(encoded) {
|
|
601
|
+
try {
|
|
602
|
+
const k = _XtroedgeCMS._SK;
|
|
603
|
+
const decoded = atob(encoded);
|
|
604
|
+
let result = "";
|
|
605
|
+
for (let i = 0; i < decoded.length; i++) {
|
|
606
|
+
result += String.fromCharCode(decoded.charCodeAt(i) ^ k.charCodeAt(i % k.length));
|
|
607
|
+
}
|
|
608
|
+
return result;
|
|
609
|
+
} catch {
|
|
610
|
+
return "";
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
static _readVault() {
|
|
614
|
+
try {
|
|
615
|
+
const raw = localStorage.getItem(_XtroedgeCMS._VAULT_KEY);
|
|
616
|
+
if (!raw) return {};
|
|
617
|
+
return JSON.parse(_XtroedgeCMS._dec(raw));
|
|
618
|
+
} catch {
|
|
619
|
+
return {};
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
static _writeVault(vault) {
|
|
623
|
+
try {
|
|
624
|
+
localStorage.setItem(_XtroedgeCMS._VAULT_KEY, _XtroedgeCMS._enc(JSON.stringify(vault)));
|
|
625
|
+
} catch {
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
static secureSet(key, value) {
|
|
629
|
+
const v = _XtroedgeCMS._readVault();
|
|
630
|
+
v[key] = value;
|
|
631
|
+
_XtroedgeCMS._writeVault(v);
|
|
632
|
+
}
|
|
633
|
+
static secureGet(key) {
|
|
634
|
+
return _XtroedgeCMS._readVault()[key] || "";
|
|
635
|
+
}
|
|
636
|
+
static secureRemove(key) {
|
|
637
|
+
const v = _XtroedgeCMS._readVault();
|
|
638
|
+
delete v[key];
|
|
639
|
+
_XtroedgeCMS._writeVault(v);
|
|
640
|
+
}
|
|
641
|
+
static secureClear() {
|
|
642
|
+
try {
|
|
643
|
+
localStorage.removeItem(_XtroedgeCMS._VAULT_KEY);
|
|
644
|
+
} catch {
|
|
645
|
+
}
|
|
646
|
+
}
|
|
471
647
|
// ===============================================
|
|
472
648
|
// PUBLIC API
|
|
473
649
|
// ===============================================
|
|
474
|
-
init() {
|
|
475
|
-
this.posY = window.innerHeight - 72;
|
|
650
|
+
async init() {
|
|
476
651
|
this.injectStyles();
|
|
477
652
|
this.applyThemeColor(this.highlightColor);
|
|
653
|
+
await this.validateLicense();
|
|
654
|
+
this.posY = window.innerHeight - 72;
|
|
478
655
|
this.buildUI();
|
|
479
656
|
this.interceptNavigation();
|
|
480
657
|
this.observer = new MutationObserver(() => {
|
|
@@ -485,7 +662,12 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
485
662
|
document.head.appendChild(this.styleEl);
|
|
486
663
|
}
|
|
487
664
|
if (this.scanTimeout) clearTimeout(this.scanTimeout);
|
|
488
|
-
this.scanTimeout = setTimeout(() =>
|
|
665
|
+
this.scanTimeout = setTimeout(() => {
|
|
666
|
+
this.autoDetectAndScan();
|
|
667
|
+
if (Object.keys(this.pageTexts).length > 0) {
|
|
668
|
+
this.updateElementTexts();
|
|
669
|
+
}
|
|
670
|
+
}, 150);
|
|
489
671
|
});
|
|
490
672
|
this.observer.observe(document.body, { childList: true, subtree: true });
|
|
491
673
|
window.addEventListener("popstate", this.boundPopState);
|
|
@@ -496,6 +678,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
496
678
|
this.cleanupManagedElements();
|
|
497
679
|
this.styleEl?.remove();
|
|
498
680
|
this.rootEl?.remove();
|
|
681
|
+
this.licenseOverlayEl?.remove();
|
|
499
682
|
window.removeEventListener("popstate", this.boundPopState);
|
|
500
683
|
window.removeEventListener("hashchange", this.boundHashChange);
|
|
501
684
|
document.removeEventListener("mousemove", this.boundMouseMove);
|
|
@@ -509,6 +692,113 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
509
692
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
510
693
|
this.db?.close();
|
|
511
694
|
}
|
|
695
|
+
// 24 hours
|
|
696
|
+
async validateLicense() {
|
|
697
|
+
const licenseKey = this.config.licenseKey || _XtroedgeCMS.secureGet("builder_token") || "";
|
|
698
|
+
if (!licenseKey) {
|
|
699
|
+
this.licenseStatus = { valid: false, plan: "invalid", message: "No license key provided." };
|
|
700
|
+
this.licenseValid = false;
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const cached = this.getLicenseCache();
|
|
704
|
+
if (cached) {
|
|
705
|
+
this.licenseStatus = cached;
|
|
706
|
+
this.licenseValid = cached.valid;
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
const validateUrl = this.config.licenseValidateUrl || (this.config.apiBase ? `${this.config.apiBase}/license/validate` : "");
|
|
710
|
+
if (!validateUrl) {
|
|
711
|
+
this.licenseValid = true;
|
|
712
|
+
this.licenseStatus = { valid: true, plan: "trial", message: "No license API configured." };
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
try {
|
|
716
|
+
const builderToken = _XtroedgeCMS.secureGet("builder_token") || "";
|
|
717
|
+
const headers = { "Content-Type": "application/json" };
|
|
718
|
+
if (builderToken) headers["Authorization"] = `Bearer ${builderToken}`;
|
|
719
|
+
const res = await fetch(validateUrl, {
|
|
720
|
+
method: "POST",
|
|
721
|
+
headers,
|
|
722
|
+
body: JSON.stringify({
|
|
723
|
+
licenseKey,
|
|
724
|
+
domain: window.location.hostname
|
|
725
|
+
})
|
|
726
|
+
});
|
|
727
|
+
let data;
|
|
728
|
+
try {
|
|
729
|
+
data = await res.json();
|
|
730
|
+
} catch {
|
|
731
|
+
throw new Error("Invalid response from license server");
|
|
732
|
+
}
|
|
733
|
+
this.licenseStatus = data;
|
|
734
|
+
this.setLicenseCache(data);
|
|
735
|
+
this.licenseValid = data.valid;
|
|
736
|
+
} catch {
|
|
737
|
+
const fallback = this.getLicenseCache(true);
|
|
738
|
+
if (fallback && fallback.valid) {
|
|
739
|
+
this.licenseValid = true;
|
|
740
|
+
this.licenseStatus = fallback;
|
|
741
|
+
} else {
|
|
742
|
+
this.licenseStatus = { valid: false, plan: "invalid", message: "Unable to verify license. Please check your internet connection." };
|
|
743
|
+
this.licenseValid = false;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
getLicenseCache(ignoreTTL = false) {
|
|
748
|
+
try {
|
|
749
|
+
const raw = _XtroedgeCMS.secureGet(_XtroedgeCMS.LICENSE_CACHE_KEY);
|
|
750
|
+
if (!raw) return null;
|
|
751
|
+
const { status, timestamp, key } = JSON.parse(raw);
|
|
752
|
+
const currentKey = this.config.licenseKey || _XtroedgeCMS.secureGet("builder_token") || "";
|
|
753
|
+
if (key !== currentKey) return null;
|
|
754
|
+
if (!ignoreTTL && Date.now() - timestamp > _XtroedgeCMS.LICENSE_CACHE_TTL) return null;
|
|
755
|
+
return status;
|
|
756
|
+
} catch {
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
setLicenseCache(status) {
|
|
761
|
+
try {
|
|
762
|
+
const currentKey = this.config.licenseKey || _XtroedgeCMS.secureGet("builder_token") || "";
|
|
763
|
+
_XtroedgeCMS.secureSet(_XtroedgeCMS.LICENSE_CACHE_KEY, JSON.stringify({
|
|
764
|
+
status,
|
|
765
|
+
timestamp: Date.now(),
|
|
766
|
+
key: currentKey
|
|
767
|
+
}));
|
|
768
|
+
} catch {
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
/** Shows toast when user tries to edit with expired/invalid license */
|
|
772
|
+
showLicenseExpiredToast() {
|
|
773
|
+
const isExpired = this.licenseStatus?.plan === "expired";
|
|
774
|
+
const msg = isExpired ? "Your free trial has expired. Subscribe to enable editing." : this.licenseStatus?.message || "License validation failed. Editing is disabled.";
|
|
775
|
+
this.showToast(msg, "error");
|
|
776
|
+
}
|
|
777
|
+
/** Returns trial/expired banner HTML if applicable, or empty string */
|
|
778
|
+
getTrialBannerHTML() {
|
|
779
|
+
if (this.trialBannerDismissed || !this.licenseStatus) return "";
|
|
780
|
+
if (!this.licenseValid) {
|
|
781
|
+
const msg = this.licenseStatus.plan === "expired" ? "Trial expired \u2014 editing disabled. Subscribe to continue." : "License invalid \u2014 editing disabled.";
|
|
782
|
+
return `
|
|
783
|
+
<div class="lcms-trial-banner" id="lcms-trial-banner" style="background:rgba(239,68,68,0.12);border-color:rgba(239,68,68,0.3);color:#f87171;">
|
|
784
|
+
<span class="lcms-trial-banner-icon">\u{1F512}</span>
|
|
785
|
+
<span class="lcms-trial-banner-text">${msg}</span>
|
|
786
|
+
<button class="lcms-trial-banner-dismiss" id="lcms-trial-dismiss" title="Dismiss" style="color:rgba(239,68,68,0.5);">\u2715</button>
|
|
787
|
+
</div>
|
|
788
|
+
`;
|
|
789
|
+
}
|
|
790
|
+
if (this.licenseStatus.plan === "trial") {
|
|
791
|
+
const days = this.licenseStatus.daysLeft ?? 0;
|
|
792
|
+
return `
|
|
793
|
+
<div class="lcms-trial-banner" id="lcms-trial-banner">
|
|
794
|
+
<span class="lcms-trial-banner-icon">\u23F3</span>
|
|
795
|
+
<span class="lcms-trial-banner-text">Free Trial: ${days} day${days !== 1 ? "s" : ""} remaining</span>
|
|
796
|
+
<button class="lcms-trial-banner-dismiss" id="lcms-trial-dismiss" title="Dismiss">\u2715</button>
|
|
797
|
+
</div>
|
|
798
|
+
`;
|
|
799
|
+
}
|
|
800
|
+
return "";
|
|
801
|
+
}
|
|
512
802
|
// ===============================================
|
|
513
803
|
// STYLES INJECTION
|
|
514
804
|
// ===============================================
|
|
@@ -564,7 +854,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
564
854
|
const editFromSession = sessionStorage.getItem("builder_edit_mode") === "true";
|
|
565
855
|
const wantsEdit = editViaParam || editFromSession;
|
|
566
856
|
if (wantsEdit) {
|
|
567
|
-
const token =
|
|
857
|
+
const token = _XtroedgeCMS.secureGet("builder_token");
|
|
568
858
|
if (!token) {
|
|
569
859
|
if (!this.pendingEditMode) {
|
|
570
860
|
this.pendingEditMode = true;
|
|
@@ -577,7 +867,12 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
577
867
|
}
|
|
578
868
|
return;
|
|
579
869
|
}
|
|
580
|
-
this.
|
|
870
|
+
if (!this.licenseValid) {
|
|
871
|
+
this.showLicenseExpiredToast();
|
|
872
|
+
this.editMode = false;
|
|
873
|
+
} else {
|
|
874
|
+
this.editMode = true;
|
|
875
|
+
}
|
|
581
876
|
}
|
|
582
877
|
if (!slugChanged && this.initialized) {
|
|
583
878
|
this.updateUI();
|
|
@@ -670,6 +965,21 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
670
965
|
const isDomain = this.siteIdentifier.includes(".");
|
|
671
966
|
this.siteIdEl.innerHTML = `<span class="lcms-site-id-icon">${isDomain ? "\u{1F310}" : "\u{1F511}"}</span><span class="lcms-site-id-text">${isDomain ? this.siteIdentifier : this.siteIdentifier.substring(0, 8)}</span>`;
|
|
672
967
|
this.panelEl.appendChild(this.siteIdEl);
|
|
968
|
+
const trialHTML = this.getTrialBannerHTML();
|
|
969
|
+
if (trialHTML) {
|
|
970
|
+
const trialWrap = this.createElement("div", "");
|
|
971
|
+
trialWrap.innerHTML = trialHTML;
|
|
972
|
+
this.panelEl.appendChild(trialWrap);
|
|
973
|
+
setTimeout(() => {
|
|
974
|
+
const dismissBtn = document.getElementById("lcms-trial-dismiss");
|
|
975
|
+
dismissBtn?.addEventListener("click", (e) => {
|
|
976
|
+
e.stopPropagation();
|
|
977
|
+
this.trialBannerDismissed = true;
|
|
978
|
+
const banner = document.getElementById("lcms-trial-banner");
|
|
979
|
+
banner?.remove();
|
|
980
|
+
});
|
|
981
|
+
}, 0);
|
|
982
|
+
}
|
|
673
983
|
this.editModeContent = this.createElement("div", "lcms-hidden");
|
|
674
984
|
this.changesInfoEl = this.createElement("div", "lcms-changes-info lcms-hidden");
|
|
675
985
|
this.editModeContent.appendChild(this.changesInfoEl);
|
|
@@ -874,8 +1184,8 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
874
1184
|
let parent = el.parentElement;
|
|
875
1185
|
while (parent && parent !== document.body) {
|
|
876
1186
|
const tag = parent.tagName.toLowerCase();
|
|
877
|
-
if (tag
|
|
878
|
-
if (tag
|
|
1187
|
+
if (tag.endsWith("-header")) return "/header";
|
|
1188
|
+
if (tag.endsWith("-footer")) return "/footer";
|
|
879
1189
|
parent = parent.parentElement;
|
|
880
1190
|
}
|
|
881
1191
|
return this.currentSlug;
|
|
@@ -893,11 +1203,13 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
893
1203
|
el.setAttribute("data-cms-section", sectionSlug);
|
|
894
1204
|
this.autoDetectedElements.add(el);
|
|
895
1205
|
};
|
|
1206
|
+
const RICH_TEXT_INLINE = /* @__PURE__ */ new Set(["B", "STRONG", "I", "EM", "U", "S", "STRIKE", "DEL", "SUB", "SUP", "MARK"]);
|
|
896
1207
|
const selector = this.editableTags.join(",");
|
|
897
1208
|
const elements = container.querySelectorAll(selector);
|
|
898
1209
|
elements.forEach((el) => {
|
|
899
1210
|
if (el.hasAttribute("data-cms")) return;
|
|
900
|
-
if (el.closest("#xtroedge-cms-root, script, style, noscript")) return;
|
|
1211
|
+
if (el.closest("#xtroedge-cms-root, #lcms-login-modal, script, style, noscript")) return;
|
|
1212
|
+
if (RICH_TEXT_INLINE.has(el.tagName) && el.parentElement?.closest("[data-cms]")) return;
|
|
901
1213
|
if (el.children.length > 3) return;
|
|
902
1214
|
const text = this.getDirectTextContent(el).trim();
|
|
903
1215
|
if (text.length < 2) return;
|
|
@@ -906,14 +1218,14 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
906
1218
|
const dataEditables = container.querySelectorAll("[data-editable]");
|
|
907
1219
|
dataEditables.forEach((el) => {
|
|
908
1220
|
if (el.hasAttribute("data-cms")) return;
|
|
909
|
-
if (el.closest("#xtroedge-cms-root, script, style, noscript")) return;
|
|
1221
|
+
if (el.closest("#xtroedge-cms-root, #lcms-login-modal, script, style, noscript")) return;
|
|
910
1222
|
assignKey(el, getSectionSlug(el));
|
|
911
1223
|
});
|
|
912
1224
|
}
|
|
913
1225
|
scanDOM() {
|
|
914
1226
|
const elements = document.querySelectorAll("[data-cms]");
|
|
915
1227
|
elements.forEach((el) => {
|
|
916
|
-
if (el.closest("#xtroedge-cms-root")) return;
|
|
1228
|
+
if (el.closest("#xtroedge-cms-root, #lcms-login-modal")) return;
|
|
917
1229
|
const key = el.getAttribute("data-cms");
|
|
918
1230
|
this.registeredKeys.add(key);
|
|
919
1231
|
if (!this.managedElements.has(el)) {
|
|
@@ -961,7 +1273,13 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
961
1273
|
hasEditableChildren(el) {
|
|
962
1274
|
return !!el.querySelector("[data-cms]");
|
|
963
1275
|
}
|
|
964
|
-
|
|
1276
|
+
getElementContent(el) {
|
|
1277
|
+
if (this.hasEditableChildren(el)) {
|
|
1278
|
+
return this.getDirectTextContent(el).trim();
|
|
1279
|
+
}
|
|
1280
|
+
return this.richTextEnabled ? el.innerHTML?.trim() || "" : el.textContent?.trim() || "";
|
|
1281
|
+
}
|
|
1282
|
+
setElementContent(el, val) {
|
|
965
1283
|
if (this.hasEditableChildren(el)) {
|
|
966
1284
|
const textNodes = [];
|
|
967
1285
|
for (let i = 0; i < el.childNodes.length; i++) {
|
|
@@ -969,28 +1287,38 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
969
1287
|
}
|
|
970
1288
|
if (textNodes.length > 0) textNodes[0].textContent = val;
|
|
971
1289
|
else el.prepend(document.createTextNode(val));
|
|
1290
|
+
} else if (this.richTextEnabled) {
|
|
1291
|
+
el.innerHTML = this.sanitizeHTML(val);
|
|
972
1292
|
} else {
|
|
973
1293
|
el.textContent = val;
|
|
974
1294
|
}
|
|
975
1295
|
}
|
|
1296
|
+
// Legacy alias for backward compat with internal calls
|
|
1297
|
+
setDirectTextContent(el, val) {
|
|
1298
|
+
this.setElementContent(el, val);
|
|
1299
|
+
}
|
|
976
1300
|
// ===============================================
|
|
977
1301
|
// ELEMENT EDITING
|
|
978
1302
|
// ===============================================
|
|
979
1303
|
attachElement(el, key) {
|
|
980
|
-
const
|
|
1304
|
+
const getContent = () => this.getElementContent(el);
|
|
981
1305
|
const blurHandler = () => {
|
|
982
|
-
const text =
|
|
1306
|
+
const text = getContent();
|
|
983
1307
|
const currentVal = this.getPageText(key);
|
|
984
1308
|
if (text !== currentVal) this.onTextChanged(key, text);
|
|
1309
|
+
this.hideRichToolbar();
|
|
985
1310
|
};
|
|
986
1311
|
const keydownHandler = (e) => {
|
|
987
1312
|
if (e.key === "Enter") {
|
|
1313
|
+
if (this.richTextEnabled && !_XtroedgeCMS.SINGLE_LINE_TAGS.has(el.tagName)) {
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
988
1316
|
e.preventDefault();
|
|
989
1317
|
el.blur();
|
|
990
1318
|
}
|
|
991
1319
|
};
|
|
992
1320
|
const inputHandler = () => {
|
|
993
|
-
const text =
|
|
1321
|
+
const text = getContent();
|
|
994
1322
|
const currentVal = this.getPageText(key);
|
|
995
1323
|
if (text !== currentVal) this.onTextChanged(key, text);
|
|
996
1324
|
};
|
|
@@ -1017,12 +1345,16 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1017
1345
|
this.managedElements.delete(el);
|
|
1018
1346
|
}
|
|
1019
1347
|
}
|
|
1348
|
+
/** Stop propagation so parent carousels (Swiper, Owl, etc.) don't hijack the event */
|
|
1349
|
+
static stopProp(e) {
|
|
1350
|
+
e.stopPropagation();
|
|
1351
|
+
}
|
|
1020
1352
|
enableElementEdit(el, _key, blurH, keyH, inputH, clickH) {
|
|
1021
1353
|
const val = this.getPageText(_key);
|
|
1022
|
-
if (val) this.
|
|
1354
|
+
if (val) this.setElementContent(el, val);
|
|
1023
1355
|
el.setAttribute("contenteditable", "true");
|
|
1024
|
-
el.style.outline
|
|
1025
|
-
el.style.
|
|
1356
|
+
el.style.setProperty("outline", `2px dashed ${this.highlightColor}`, "important");
|
|
1357
|
+
el.style.setProperty("outline-offset", "-2px", "important");
|
|
1026
1358
|
el.style.cursor = "text";
|
|
1027
1359
|
el.style.transition = "background 0.2s";
|
|
1028
1360
|
el.style.minWidth = "20px";
|
|
@@ -1030,37 +1362,199 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1030
1362
|
el.addEventListener("keydown", keyH);
|
|
1031
1363
|
el.addEventListener("input", inputH);
|
|
1032
1364
|
el.addEventListener("click", clickH, true);
|
|
1365
|
+
el.addEventListener("mousedown", _XtroedgeCMS.stopProp, true);
|
|
1366
|
+
el.addEventListener("touchstart", _XtroedgeCMS.stopProp, true);
|
|
1367
|
+
el.addEventListener("focus", () => this.showRichToolbar(el));
|
|
1033
1368
|
}
|
|
1034
1369
|
disableElementEdit(el, _key, blurH, keyH, inputH, clickH) {
|
|
1035
1370
|
el.removeAttribute("contenteditable");
|
|
1036
|
-
el.style.outline
|
|
1037
|
-
el.style.outline
|
|
1038
|
-
el.style.outlineOffset = "";
|
|
1371
|
+
el.style.removeProperty("outline");
|
|
1372
|
+
el.style.removeProperty("outline-offset");
|
|
1039
1373
|
el.style.cursor = "";
|
|
1040
1374
|
el.style.transition = "";
|
|
1041
1375
|
el.style.minWidth = "";
|
|
1042
1376
|
const val = this.getPageText(_key);
|
|
1043
|
-
if (val) this.
|
|
1377
|
+
if (val) this.setElementContent(el, val);
|
|
1044
1378
|
el.removeEventListener("blur", blurH);
|
|
1045
1379
|
el.removeEventListener("keydown", keyH);
|
|
1046
1380
|
el.removeEventListener("input", inputH);
|
|
1047
1381
|
el.removeEventListener("click", clickH, true);
|
|
1382
|
+
el.removeEventListener("mousedown", _XtroedgeCMS.stopProp, true);
|
|
1383
|
+
el.removeEventListener("touchstart", _XtroedgeCMS.stopProp, true);
|
|
1048
1384
|
}
|
|
1049
1385
|
applyEditMode(editMode) {
|
|
1386
|
+
document.body.classList.toggle("lcms-editing", editMode);
|
|
1387
|
+
if (editMode) {
|
|
1388
|
+
this.editScrollHandler = () => {
|
|
1389
|
+
if (this.editScrollRAF) return;
|
|
1390
|
+
this.editScrollRAF = requestAnimationFrame(() => {
|
|
1391
|
+
this.syncEditablePointerEvents();
|
|
1392
|
+
this.editScrollRAF = null;
|
|
1393
|
+
});
|
|
1394
|
+
};
|
|
1395
|
+
window.addEventListener("scroll", this.editScrollHandler, { passive: true });
|
|
1396
|
+
this.syncEditablePointerEvents();
|
|
1397
|
+
} else {
|
|
1398
|
+
if (this.editScrollHandler) {
|
|
1399
|
+
window.removeEventListener("scroll", this.editScrollHandler);
|
|
1400
|
+
this.editScrollHandler = null;
|
|
1401
|
+
}
|
|
1402
|
+
if (this.editScrollRAF) {
|
|
1403
|
+
cancelAnimationFrame(this.editScrollRAF);
|
|
1404
|
+
this.editScrollRAF = null;
|
|
1405
|
+
}
|
|
1406
|
+
for (const [el] of this.managedElements) el.style.pointerEvents = "";
|
|
1407
|
+
this.modifiedAncestors.forEach((a) => {
|
|
1408
|
+
a.style.pointerEvents = "";
|
|
1409
|
+
});
|
|
1410
|
+
this.modifiedAncestors.clear();
|
|
1411
|
+
}
|
|
1050
1412
|
for (const [el, info] of this.managedElements) {
|
|
1051
1413
|
if (editMode) this.enableElementEdit(el, info.key, info.blurHandler, info.keydownHandler, info.inputHandler, info.clickHandler);
|
|
1052
1414
|
else this.disableElementEdit(el, info.key, info.blurHandler, info.keydownHandler, info.inputHandler, info.clickHandler);
|
|
1053
1415
|
}
|
|
1054
1416
|
this.applyImageEditMode(editMode);
|
|
1417
|
+
if (editMode) this.createRichToolbar();
|
|
1418
|
+
else this.destroyRichToolbar();
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* For each managed element, find the nearest position:absolute/fixed ancestor.
|
|
1422
|
+
* If that ancestor is invisible (opacity ≈ 0), set pointer-events:none on it
|
|
1423
|
+
* so clicks pass through to the visible element behind it.
|
|
1424
|
+
*/
|
|
1425
|
+
syncEditablePointerEvents() {
|
|
1426
|
+
const checked = /* @__PURE__ */ new Map();
|
|
1427
|
+
for (const [el] of this.managedElements) {
|
|
1428
|
+
const ancestor = this.findPositionedAncestor(el);
|
|
1429
|
+
if (!ancestor) continue;
|
|
1430
|
+
if (!checked.has(ancestor)) {
|
|
1431
|
+
const cs = window.getComputedStyle(ancestor);
|
|
1432
|
+
const visible = parseFloat(cs.opacity) >= 0.15 && cs.visibility !== "hidden" && cs.display !== "none";
|
|
1433
|
+
checked.set(ancestor, visible);
|
|
1434
|
+
if (!visible) {
|
|
1435
|
+
ancestor.style.pointerEvents = "none";
|
|
1436
|
+
this.modifiedAncestors.add(ancestor);
|
|
1437
|
+
} else {
|
|
1438
|
+
if (this.modifiedAncestors.has(ancestor)) {
|
|
1439
|
+
ancestor.style.pointerEvents = "";
|
|
1440
|
+
this.modifiedAncestors.delete(ancestor);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
/** Walk up from el to find the nearest position:absolute or position:fixed ancestor */
|
|
1447
|
+
findPositionedAncestor(el) {
|
|
1448
|
+
let cur = el.parentElement;
|
|
1449
|
+
while (cur && cur !== document.body) {
|
|
1450
|
+
if (cur.closest("#xtroedge-cms-root")) return null;
|
|
1451
|
+
const pos = window.getComputedStyle(cur).position;
|
|
1452
|
+
if (pos === "absolute" || pos === "fixed") return cur;
|
|
1453
|
+
cur = cur.parentElement;
|
|
1454
|
+
}
|
|
1455
|
+
return null;
|
|
1456
|
+
}
|
|
1457
|
+
killAnimations() {
|
|
1458
|
+
const win = window;
|
|
1459
|
+
if (win.ScrollTrigger) {
|
|
1460
|
+
try {
|
|
1461
|
+
win.ScrollTrigger.getAll().forEach((t) => t.kill(true));
|
|
1462
|
+
} catch {
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
if (win.gsap) {
|
|
1466
|
+
try {
|
|
1467
|
+
win.gsap.globalTimeline.clear();
|
|
1468
|
+
win.gsap.killTweensOf("*");
|
|
1469
|
+
} catch {
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
if (win.AOS) {
|
|
1473
|
+
try {
|
|
1474
|
+
win.AOS.refreshHard?.();
|
|
1475
|
+
} catch {
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
this.removePinSpacers();
|
|
1479
|
+
this.clearAnimationInlineStyles();
|
|
1480
|
+
setTimeout(() => {
|
|
1481
|
+
this.removePinSpacers();
|
|
1482
|
+
this.clearAnimationInlineStyles();
|
|
1483
|
+
}, 500);
|
|
1484
|
+
setTimeout(() => {
|
|
1485
|
+
this.removePinSpacers();
|
|
1486
|
+
this.clearAnimationInlineStyles();
|
|
1487
|
+
}, 1500);
|
|
1488
|
+
}
|
|
1489
|
+
/** Remove GSAP ScrollTrigger pin-spacer wrapper divs and restore original elements */
|
|
1490
|
+
removePinSpacers() {
|
|
1491
|
+
document.querySelectorAll(".pin-spacer").forEach((spacer) => {
|
|
1492
|
+
const child = spacer.firstElementChild;
|
|
1493
|
+
if (child) {
|
|
1494
|
+
spacer.parentNode?.insertBefore(child, spacer);
|
|
1495
|
+
child.style.cssText = "";
|
|
1496
|
+
}
|
|
1497
|
+
spacer.remove();
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
/** Clear only animation-related inline styles (set by GSAP/ScrollTrigger) without breaking layout */
|
|
1501
|
+
clearAnimationInlineStyles() {
|
|
1502
|
+
const container = this.containerSelector ? document.querySelector(this.containerSelector) || document.body : document.body;
|
|
1503
|
+
container.querySelectorAll("*").forEach((el) => {
|
|
1504
|
+
if (el.closest("#xtroedge-cms-root")) return;
|
|
1505
|
+
const s = el.style;
|
|
1506
|
+
if (s.pointerEvents === "none") s.pointerEvents = "";
|
|
1507
|
+
if (s.userSelect === "none") s.userSelect = "";
|
|
1508
|
+
if (s.visibility === "hidden") s.visibility = "";
|
|
1509
|
+
if (s.clipPath && s.clipPath !== "none") s.clipPath = "";
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
/** Capture each element's current DOM text before CMS applies saved edits */
|
|
1513
|
+
captureDomOriginals() {
|
|
1514
|
+
this.domOriginals = {};
|
|
1515
|
+
for (const [el, info] of this.managedElements) {
|
|
1516
|
+
this.domOriginals[info.key] = this.getElementContent(el);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
/** Ensure every entry in pageTexts has _orig for current language (from domOriginals) */
|
|
1520
|
+
ensureOriginals() {
|
|
1521
|
+
const origKey = `_orig_${this.currentLang}`;
|
|
1522
|
+
for (const key of Object.keys(this.pageTexts)) {
|
|
1523
|
+
if (!this.pageTexts[key][origKey] && this.domOriginals[key]) {
|
|
1524
|
+
this.pageTexts[key][origKey] = this.domOriginals[key];
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
/** Find a managed element whose current text matches the given original text */
|
|
1529
|
+
findElementByOriginal(originalText, tagName) {
|
|
1530
|
+
for (const [el] of this.managedElements) {
|
|
1531
|
+
if (el.tagName !== tagName) continue;
|
|
1532
|
+
const current = this.getElementContent(el);
|
|
1533
|
+
if (current === originalText) return el;
|
|
1534
|
+
}
|
|
1535
|
+
return null;
|
|
1055
1536
|
}
|
|
1056
1537
|
updateElementTexts() {
|
|
1057
1538
|
this.observer?.disconnect();
|
|
1539
|
+
let orphanedCount = 0;
|
|
1058
1540
|
for (const [el, info] of this.managedElements) {
|
|
1059
1541
|
if (document.activeElement === el) continue;
|
|
1060
|
-
const
|
|
1542
|
+
const entry = this.pageTexts[info.key];
|
|
1543
|
+
if (!entry) continue;
|
|
1544
|
+
const val = entry[this.currentLang];
|
|
1061
1545
|
if (!val) continue;
|
|
1062
|
-
const
|
|
1063
|
-
if (
|
|
1546
|
+
const orig = entry[`_orig_${this.currentLang}`];
|
|
1547
|
+
if (!orig) {
|
|
1548
|
+
const current = this.getElementContent(el);
|
|
1549
|
+
if (val !== current) this.setElementContent(el, val);
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
const domText = this.domOriginals[info.key] || this.getElementContent(el);
|
|
1553
|
+
if (domText === orig) {
|
|
1554
|
+
if (val !== this.getElementContent(el)) this.setElementContent(el, val);
|
|
1555
|
+
} else {
|
|
1556
|
+
orphanedCount++;
|
|
1557
|
+
}
|
|
1064
1558
|
}
|
|
1065
1559
|
this.observer?.observe(document.body, { childList: true, subtree: true });
|
|
1066
1560
|
}
|
|
@@ -1069,6 +1563,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1069
1563
|
this.managedElements.clear();
|
|
1070
1564
|
for (const el of this.autoDetectedElements) el.removeAttribute("data-cms");
|
|
1071
1565
|
this.autoDetectedElements.clear();
|
|
1566
|
+
this.domOriginals = {};
|
|
1072
1567
|
this.cleanupManagedImages();
|
|
1073
1568
|
}
|
|
1074
1569
|
// ===============================================
|
|
@@ -1088,7 +1583,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1088
1583
|
const info = this.managedImages.get(img);
|
|
1089
1584
|
if (info) {
|
|
1090
1585
|
img.removeEventListener("contextmenu", info.ctxHandler);
|
|
1091
|
-
img.style.outline
|
|
1586
|
+
img.style.removeProperty("outline");
|
|
1092
1587
|
img.style.cursor = "";
|
|
1093
1588
|
this.managedImages.delete(img);
|
|
1094
1589
|
}
|
|
@@ -1108,14 +1603,14 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1108
1603
|
enableImageEdit(img, ctxHandler) {
|
|
1109
1604
|
if (!img.dataset.origTitle) img.dataset.origTitle = img.title || "";
|
|
1110
1605
|
img.title = "Right-click to change image";
|
|
1111
|
-
img.style.outline
|
|
1606
|
+
img.style.setProperty("outline", `2px dashed ${this.highlightColor}`, "important");
|
|
1112
1607
|
img.style.cursor = "context-menu";
|
|
1113
1608
|
img.addEventListener("contextmenu", ctxHandler);
|
|
1114
1609
|
}
|
|
1115
1610
|
disableImageEdit(img, ctxHandler) {
|
|
1116
1611
|
img.title = img.dataset.origTitle || "";
|
|
1117
1612
|
delete img.dataset.origTitle;
|
|
1118
|
-
img.style.outline
|
|
1613
|
+
img.style.removeProperty("outline");
|
|
1119
1614
|
img.style.cursor = "";
|
|
1120
1615
|
img.removeEventListener("contextmenu", ctxHandler);
|
|
1121
1616
|
}
|
|
@@ -1164,7 +1659,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1164
1659
|
this.dismissImageCtxMenu();
|
|
1165
1660
|
});
|
|
1166
1661
|
menu.appendChild(btn);
|
|
1167
|
-
document.body.appendChild(menu);
|
|
1662
|
+
(this.rootEl || document.body).appendChild(menu);
|
|
1168
1663
|
this.imageCtxMenu = menu;
|
|
1169
1664
|
const closeHandler = (e) => {
|
|
1170
1665
|
if (!menu.contains(e.target)) {
|
|
@@ -1195,14 +1690,18 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1195
1690
|
}
|
|
1196
1691
|
async uploadImageToApi(dataUrl) {
|
|
1197
1692
|
try {
|
|
1198
|
-
const
|
|
1693
|
+
const storedImageUrl = _XtroedgeCMS.secureGet("xtroedge_image_url");
|
|
1694
|
+
const clientBase = this.config.clientApi || this.config.apiBase;
|
|
1695
|
+
const uploadUrl = storedImageUrl ? this.resolveClientUrl(storedImageUrl) : `${clientBase}/web/upload-image`;
|
|
1696
|
+
const res = await this.apiFetch(uploadUrl, {
|
|
1199
1697
|
method: "POST",
|
|
1200
1698
|
headers: { "Content-Type": "application/json" },
|
|
1201
1699
|
body: JSON.stringify({ url: dataUrl })
|
|
1202
1700
|
});
|
|
1203
1701
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1204
1702
|
const data = await res.json();
|
|
1205
|
-
const
|
|
1703
|
+
const imgOriginBase = this.config.clientApi || this.config.apiBase || "";
|
|
1704
|
+
const baseUrl = this.config.imageBaseUrl || (imgOriginBase ? new URL(imgOriginBase).origin : "");
|
|
1206
1705
|
const fullUrl = baseUrl + data.path;
|
|
1207
1706
|
if (this.activeImageEl) {
|
|
1208
1707
|
const key = this.managedImages.get(this.activeImageEl)?.key || "";
|
|
@@ -1239,6 +1738,10 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1239
1738
|
this.canUndo = true;
|
|
1240
1739
|
this.canRedo = false;
|
|
1241
1740
|
if (!this.pageTexts[key]) this.pageTexts[key] = {};
|
|
1741
|
+
const origKey = `_orig_${this.currentLang}`;
|
|
1742
|
+
if (!this.pageTexts[key][origKey]) {
|
|
1743
|
+
this.pageTexts[key][origKey] = this.domOriginals[key] || "";
|
|
1744
|
+
}
|
|
1242
1745
|
this.pageTexts[key] = { ...this.pageTexts[key], [this.currentLang]: newValue };
|
|
1243
1746
|
this.dirtyKeys.add(key);
|
|
1244
1747
|
this.unsavedChanges = this.dirtyKeys.size + this.dirtyImageKeys.size;
|
|
@@ -1269,6 +1772,162 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1269
1772
|
}
|
|
1270
1773
|
}
|
|
1271
1774
|
// ===============================================
|
|
1775
|
+
// RICH TEXT TOOLBAR
|
|
1776
|
+
// ===============================================
|
|
1777
|
+
get richTextEnabled() {
|
|
1778
|
+
return this.config.richText !== false;
|
|
1779
|
+
}
|
|
1780
|
+
sanitizeHTML(html) {
|
|
1781
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
1782
|
+
const clean = (node) => {
|
|
1783
|
+
const children = Array.from(node.childNodes);
|
|
1784
|
+
for (const child of children) {
|
|
1785
|
+
if (child.nodeType === Node.TEXT_NODE) continue;
|
|
1786
|
+
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
1787
|
+
const el = child;
|
|
1788
|
+
if (!_XtroedgeCMS.ALLOWED_TAGS.has(el.tagName)) {
|
|
1789
|
+
while (el.firstChild) el.parentNode?.insertBefore(el.firstChild, el);
|
|
1790
|
+
el.remove();
|
|
1791
|
+
} else {
|
|
1792
|
+
const allowedAttrs = _XtroedgeCMS.ALLOWED_ATTRS[el.tagName] || /* @__PURE__ */ new Set();
|
|
1793
|
+
for (const attr of Array.from(el.attributes)) {
|
|
1794
|
+
if (!allowedAttrs.has(attr.name)) el.removeAttribute(attr.name);
|
|
1795
|
+
}
|
|
1796
|
+
if (el.tagName === "A") {
|
|
1797
|
+
const href = el.getAttribute("href") || "";
|
|
1798
|
+
if (href.trim().toLowerCase().startsWith("javascript:")) {
|
|
1799
|
+
el.setAttribute("href", "#");
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
clean(el);
|
|
1803
|
+
}
|
|
1804
|
+
} else {
|
|
1805
|
+
child.remove();
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
};
|
|
1809
|
+
clean(doc.body);
|
|
1810
|
+
return doc.body.innerHTML;
|
|
1811
|
+
}
|
|
1812
|
+
createRichToolbar() {
|
|
1813
|
+
if (!this.richTextEnabled || this.richToolbarEl) return;
|
|
1814
|
+
const toolbar = document.createElement("div");
|
|
1815
|
+
toolbar.className = "lcms-rich-toolbar";
|
|
1816
|
+
const buttons = [
|
|
1817
|
+
{ cmd: "bold", label: "B", title: "Bold" },
|
|
1818
|
+
{ cmd: "italic", label: "<i>I</i>", title: "Italic" },
|
|
1819
|
+
{ cmd: "underline", label: "<u>U</u>", title: "Underline" },
|
|
1820
|
+
{ cmd: "strikeThrough", label: "<s>S</s>", title: "Strikethrough" }
|
|
1821
|
+
];
|
|
1822
|
+
const linkButtons = [
|
|
1823
|
+
{ cmd: "createLink", label: "\u{1F517}", title: "Insert Link" },
|
|
1824
|
+
{ cmd: "unlink", label: "\u2298", title: "Remove Link" }
|
|
1825
|
+
];
|
|
1826
|
+
const utilButtons = [
|
|
1827
|
+
{ cmd: "removeFormat", label: "\u2715", title: "Clear Formatting" }
|
|
1828
|
+
];
|
|
1829
|
+
const makeBtn = (item) => {
|
|
1830
|
+
const btn = document.createElement("button");
|
|
1831
|
+
btn.innerHTML = item.label;
|
|
1832
|
+
btn.title = item.title;
|
|
1833
|
+
btn.dataset.cmd = item.cmd;
|
|
1834
|
+
btn.addEventListener("mousedown", (e) => {
|
|
1835
|
+
e.preventDefault();
|
|
1836
|
+
this.execToolbarCommand(item.cmd);
|
|
1837
|
+
});
|
|
1838
|
+
return btn;
|
|
1839
|
+
};
|
|
1840
|
+
const addSep = () => {
|
|
1841
|
+
const sep = document.createElement("div");
|
|
1842
|
+
sep.className = "lcms-tb-sep";
|
|
1843
|
+
toolbar.appendChild(sep);
|
|
1844
|
+
};
|
|
1845
|
+
buttons.forEach((b) => toolbar.appendChild(makeBtn(b)));
|
|
1846
|
+
addSep();
|
|
1847
|
+
linkButtons.forEach((b) => toolbar.appendChild(makeBtn(b)));
|
|
1848
|
+
addSep();
|
|
1849
|
+
utilButtons.forEach((b) => toolbar.appendChild(makeBtn(b)));
|
|
1850
|
+
this.rootEl?.appendChild(toolbar);
|
|
1851
|
+
this.richToolbarEl = toolbar;
|
|
1852
|
+
this.selectionChangeHandler = () => this.updateToolbarState();
|
|
1853
|
+
document.addEventListener("selectionchange", this.selectionChangeHandler);
|
|
1854
|
+
}
|
|
1855
|
+
destroyRichToolbar() {
|
|
1856
|
+
if (this.richToolbarEl) {
|
|
1857
|
+
this.richToolbarEl.remove();
|
|
1858
|
+
this.richToolbarEl = null;
|
|
1859
|
+
}
|
|
1860
|
+
if (this.selectionChangeHandler) {
|
|
1861
|
+
document.removeEventListener("selectionchange", this.selectionChangeHandler);
|
|
1862
|
+
this.selectionChangeHandler = null;
|
|
1863
|
+
}
|
|
1864
|
+
this.activeEditableEl = null;
|
|
1865
|
+
}
|
|
1866
|
+
execToolbarCommand(cmd) {
|
|
1867
|
+
this.observer?.disconnect();
|
|
1868
|
+
if (this.scanTimeout) {
|
|
1869
|
+
clearTimeout(this.scanTimeout);
|
|
1870
|
+
this.scanTimeout = null;
|
|
1871
|
+
}
|
|
1872
|
+
if (cmd === "createLink") {
|
|
1873
|
+
const url = prompt("Enter URL:", "https://");
|
|
1874
|
+
if (url) document.execCommand("createLink", false, url);
|
|
1875
|
+
} else {
|
|
1876
|
+
document.execCommand(cmd, false);
|
|
1877
|
+
}
|
|
1878
|
+
this.updateToolbarState();
|
|
1879
|
+
if (this.activeEditableEl) {
|
|
1880
|
+
this.activeEditableEl.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1881
|
+
}
|
|
1882
|
+
setTimeout(() => {
|
|
1883
|
+
this.observer?.observe(document.body, { childList: true, subtree: true });
|
|
1884
|
+
}, 50);
|
|
1885
|
+
}
|
|
1886
|
+
showRichToolbar(el) {
|
|
1887
|
+
if (!this.richTextEnabled || !this.richToolbarEl) return;
|
|
1888
|
+
if (this.toolbarHideTimeout) {
|
|
1889
|
+
clearTimeout(this.toolbarHideTimeout);
|
|
1890
|
+
this.toolbarHideTimeout = null;
|
|
1891
|
+
}
|
|
1892
|
+
this.activeEditableEl = el;
|
|
1893
|
+
this.positionToolbar(el);
|
|
1894
|
+
this.richToolbarEl.classList.add("visible");
|
|
1895
|
+
this.updateToolbarState();
|
|
1896
|
+
}
|
|
1897
|
+
positionToolbar(el) {
|
|
1898
|
+
if (!this.richToolbarEl) return;
|
|
1899
|
+
const rect = el.getBoundingClientRect();
|
|
1900
|
+
const tbRect = this.richToolbarEl.getBoundingClientRect();
|
|
1901
|
+
let top = rect.top - tbRect.height - 8;
|
|
1902
|
+
let left = rect.left + rect.width / 2 - tbRect.width / 2;
|
|
1903
|
+
if (top < 4) top = rect.bottom + 8;
|
|
1904
|
+
if (left < 4) left = 4;
|
|
1905
|
+
if (left + tbRect.width > window.innerWidth - 4) left = window.innerWidth - tbRect.width - 4;
|
|
1906
|
+
this.richToolbarEl.style.top = `${top}px`;
|
|
1907
|
+
this.richToolbarEl.style.left = `${left}px`;
|
|
1908
|
+
}
|
|
1909
|
+
hideRichToolbar() {
|
|
1910
|
+
if (!this.richToolbarEl) return;
|
|
1911
|
+
this.toolbarHideTimeout = setTimeout(() => {
|
|
1912
|
+
this.richToolbarEl?.classList.remove("visible");
|
|
1913
|
+
this.activeEditableEl = null;
|
|
1914
|
+
this.toolbarHideTimeout = null;
|
|
1915
|
+
}, 150);
|
|
1916
|
+
}
|
|
1917
|
+
updateToolbarState() {
|
|
1918
|
+
if (!this.richToolbarEl) return;
|
|
1919
|
+
const cmds = ["bold", "italic", "underline", "strikeThrough"];
|
|
1920
|
+
for (const btn of Array.from(this.richToolbarEl.querySelectorAll("button"))) {
|
|
1921
|
+
const cmd = btn.dataset.cmd;
|
|
1922
|
+
if (cmd && cmds.includes(cmd)) {
|
|
1923
|
+
try {
|
|
1924
|
+
btn.classList.toggle("active", document.queryCommandState(cmd));
|
|
1925
|
+
} catch {
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
// ===============================================
|
|
1272
1931
|
// UNDO / REDO
|
|
1273
1932
|
// ===============================================
|
|
1274
1933
|
onUndo() {
|
|
@@ -1330,7 +1989,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1330
1989
|
this.updateUI();
|
|
1331
1990
|
}
|
|
1332
1991
|
logout() {
|
|
1333
|
-
|
|
1992
|
+
_XtroedgeCMS.secureClear();
|
|
1334
1993
|
sessionStorage.removeItem("builder_edit_mode");
|
|
1335
1994
|
this.editMode = false;
|
|
1336
1995
|
this.isEditAllowed = false;
|
|
@@ -1356,6 +2015,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1356
2015
|
}
|
|
1357
2016
|
}
|
|
1358
2017
|
this.autoDetectAndScan();
|
|
2018
|
+
this.captureDomOriginals();
|
|
1359
2019
|
if (this.editMode) this.applyEditMode(true);
|
|
1360
2020
|
if (this.config.apiBase) {
|
|
1361
2021
|
if (loadDraft) this.loadPageContent("draft");
|
|
@@ -1387,17 +2047,40 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1387
2047
|
getPageSection() {
|
|
1388
2048
|
return this.currentSlug.replace(/^\//, "").replace(/-/g, "_").toUpperCase();
|
|
1389
2049
|
}
|
|
1390
|
-
|
|
2050
|
+
/** Resolve a stored URL against our server (apiBase) */
|
|
2051
|
+
resolveServerUrl(storedUrl) {
|
|
2052
|
+
if (storedUrl.startsWith("/")) return `${this.config.apiBase}${storedUrl}`;
|
|
2053
|
+
return storedUrl;
|
|
2054
|
+
}
|
|
2055
|
+
/** Resolve a stored URL against client's server (clientApi, falls back to apiBase) */
|
|
2056
|
+
resolveClientUrl(storedUrl) {
|
|
2057
|
+
const base = this.config.clientApi || this.config.apiBase;
|
|
2058
|
+
if (storedUrl.startsWith("/")) return `${base}${storedUrl}`;
|
|
2059
|
+
return storedUrl;
|
|
2060
|
+
}
|
|
1391
2061
|
apiFetch(url, opts) {
|
|
1392
|
-
const token =
|
|
2062
|
+
const token = _XtroedgeCMS.secureGet("builder_token") || "";
|
|
2063
|
+
const licenseKey = this.config.licenseKey || "";
|
|
1393
2064
|
const headers = { ...opts?.headers || {} };
|
|
1394
2065
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
2066
|
+
if (licenseKey) headers["X-License-Key"] = licenseKey;
|
|
2067
|
+
const isOurServer = this.config.apiBase && url.startsWith(this.config.apiBase);
|
|
2068
|
+
if (!isOurServer && this.config.clientHeaders) {
|
|
2069
|
+
for (const [k, v] of Object.entries(this.config.clientHeaders)) {
|
|
2070
|
+
if (!headers[k]) headers[k] = v;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
1395
2073
|
return fetch(url, { ...opts, headers });
|
|
1396
2074
|
}
|
|
1397
|
-
// Fetch
|
|
2075
|
+
// Fetch page by slug — if get_url exists in localStorage, call it directly (bypass our server)
|
|
1398
2076
|
async fetchSection(slug, status) {
|
|
1399
2077
|
try {
|
|
1400
|
-
const
|
|
2078
|
+
const storedGetUrl = _XtroedgeCMS.secureGet("xtroedge_get_url");
|
|
2079
|
+
const clientBase = this.config.clientApi || this.config.apiBase;
|
|
2080
|
+
const getBase = storedGetUrl ? this.resolveClientUrl(storedGetUrl) : `${clientBase}/web-page/get`;
|
|
2081
|
+
let getUrl = `${getBase}?slug=${encodeURIComponent(slug)}`;
|
|
2082
|
+
if (status) getUrl += `&status=${encodeURIComponent(status)}`;
|
|
2083
|
+
const res = await this.apiFetch(getUrl);
|
|
1401
2084
|
if (!res.ok) return null;
|
|
1402
2085
|
return await res.json();
|
|
1403
2086
|
} catch {
|
|
@@ -1484,7 +2167,8 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1484
2167
|
for (const key of this.registeredKeys) {
|
|
1485
2168
|
const el = document.querySelector(`[data-cms="${key}"]`);
|
|
1486
2169
|
const val = el?.textContent?.trim() || "";
|
|
1487
|
-
|
|
2170
|
+
const entry = { [this.defaultLanguage]: val };
|
|
2171
|
+
texts[key] = entry;
|
|
1488
2172
|
}
|
|
1489
2173
|
this.pageTexts = texts;
|
|
1490
2174
|
if (this.config.i18nBasePath) {
|
|
@@ -1523,12 +2207,22 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1523
2207
|
return sections;
|
|
1524
2208
|
}
|
|
1525
2209
|
async saveChanges() {
|
|
2210
|
+
if (!this.licenseValid) {
|
|
2211
|
+
this.showLicenseExpiredToast();
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
1526
2214
|
if (!this.config.apiBase) {
|
|
1527
2215
|
this.showToast("No API configured. Set apiBase to enable save.", "error");
|
|
1528
2216
|
return;
|
|
1529
2217
|
}
|
|
1530
2218
|
this.isSaving = true;
|
|
1531
2219
|
this.updateUI();
|
|
2220
|
+
const origKey = `_orig_${this.currentLang}`;
|
|
2221
|
+
for (const key of Object.keys(this.pageTexts)) {
|
|
2222
|
+
if (!this.pageTexts[key][origKey] && this.domOriginals[key]) {
|
|
2223
|
+
this.pageTexts[key][origKey] = this.domOriginals[key];
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
1532
2226
|
try {
|
|
1533
2227
|
const sections = this.groupTextsBySection();
|
|
1534
2228
|
const requests = Object.entries(sections).map(
|
|
@@ -1538,13 +2232,18 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1538
2232
|
body: JSON.stringify({
|
|
1539
2233
|
slug,
|
|
1540
2234
|
title: slug === this.currentSlug ? this.currentTitle : slug.replace("/", ""),
|
|
1541
|
-
site_identifier: this.siteIdentifier,
|
|
1542
2235
|
content: { texts, ...slug === this.currentSlug ? { images: this.pageImages } : {} }
|
|
1543
2236
|
})
|
|
1544
2237
|
})
|
|
1545
2238
|
);
|
|
1546
|
-
const
|
|
1547
|
-
|
|
2239
|
+
const responses = await Promise.all(requests);
|
|
2240
|
+
const saveResults = [];
|
|
2241
|
+
for (const res of responses) {
|
|
2242
|
+
const data = await res.json();
|
|
2243
|
+
saveResults.push({ ok: res.ok, status: res.status, data });
|
|
2244
|
+
}
|
|
2245
|
+
if (saveResults.some((r) => !r.ok)) throw new Error("One or more sections failed to save");
|
|
2246
|
+
console.info("[XtroEdge] Save responses:", saveResults.map((r) => r.data));
|
|
1548
2247
|
this.isSaving = false;
|
|
1549
2248
|
this.resetAfterSave();
|
|
1550
2249
|
this.updateUI();
|
|
@@ -1558,12 +2257,22 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1558
2257
|
}
|
|
1559
2258
|
}
|
|
1560
2259
|
async publishChanges() {
|
|
2260
|
+
if (!this.licenseValid) {
|
|
2261
|
+
this.showLicenseExpiredToast();
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
1561
2264
|
if (!this.config.apiBase) {
|
|
1562
2265
|
this.showToast("No API configured. Set apiBase to enable publish.", "error");
|
|
1563
2266
|
return;
|
|
1564
2267
|
}
|
|
1565
2268
|
this.isPublishing = true;
|
|
1566
2269
|
this.updateUI();
|
|
2270
|
+
const origKey = `_orig_${this.currentLang}`;
|
|
2271
|
+
for (const key of Object.keys(this.pageTexts)) {
|
|
2272
|
+
if (!this.pageTexts[key][origKey] && this.domOriginals[key]) {
|
|
2273
|
+
this.pageTexts[key][origKey] = this.domOriginals[key];
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
1567
2276
|
try {
|
|
1568
2277
|
const sections = this.groupTextsBySection();
|
|
1569
2278
|
const requests = Object.entries(sections).map(
|
|
@@ -1573,13 +2282,18 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1573
2282
|
body: JSON.stringify({
|
|
1574
2283
|
slug,
|
|
1575
2284
|
title: slug === this.currentSlug ? this.currentTitle : slug.replace("/", ""),
|
|
1576
|
-
site_identifier: this.siteIdentifier,
|
|
1577
2285
|
published_content: { texts, ...slug === this.currentSlug ? { images: this.pageImages } : {} }
|
|
1578
2286
|
})
|
|
1579
2287
|
})
|
|
1580
2288
|
);
|
|
1581
2289
|
const results = await Promise.all(requests);
|
|
1582
|
-
|
|
2290
|
+
const publishResults = [];
|
|
2291
|
+
for (const res of results) {
|
|
2292
|
+
const data = await res.json();
|
|
2293
|
+
publishResults.push({ ok: res.ok, status: res.status, data });
|
|
2294
|
+
}
|
|
2295
|
+
if (publishResults.some((r) => !r.ok)) throw new Error("One or more sections failed to publish");
|
|
2296
|
+
console.info("[XtroEdge] Publish responses:", publishResults.map((r) => r.data));
|
|
1583
2297
|
this.isPublishing = false;
|
|
1584
2298
|
this.resetAfterSave();
|
|
1585
2299
|
this.updateUI();
|
|
@@ -1741,6 +2455,11 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1741
2455
|
toggleEditMode(e) {
|
|
1742
2456
|
e.stopPropagation();
|
|
1743
2457
|
const checked = e.target.checked;
|
|
2458
|
+
if (checked && !this.licenseValid) {
|
|
2459
|
+
e.target.checked = false;
|
|
2460
|
+
this.showLicenseExpiredToast();
|
|
2461
|
+
return;
|
|
2462
|
+
}
|
|
1744
2463
|
if (checked) {
|
|
1745
2464
|
sessionStorage.setItem("builder_edit_mode", "true");
|
|
1746
2465
|
this.editMode = true;
|
|
@@ -1872,13 +2591,19 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1872
2591
|
</div>
|
|
1873
2592
|
<div class="lcms-login-field">
|
|
1874
2593
|
<label class="lcms-login-label">Password</label>
|
|
1875
|
-
<
|
|
2594
|
+
<div class="lcms-password-wrapper">
|
|
2595
|
+
<input class="lcms-login-input" id="lcms-login-password" type="password" placeholder="Enter your password" autocomplete="current-password" />
|
|
2596
|
+
<button type="button" class="lcms-password-toggle" id="lcms-password-toggle" tabindex="-1">
|
|
2597
|
+
<svg class="lcms-eye-open" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
|
2598
|
+
<svg class="lcms-eye-closed lcms-hidden" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
|
|
2599
|
+
</button>
|
|
2600
|
+
</div>
|
|
1876
2601
|
</div>
|
|
1877
2602
|
<button class="lcms-login-btn" id="lcms-login-btn">Sign In</button>
|
|
1878
2603
|
<div class="lcms-login-error" id="lcms-login-error"></div>
|
|
1879
2604
|
</div>
|
|
1880
2605
|
`;
|
|
1881
|
-
document.body.appendChild(overlay);
|
|
2606
|
+
(this.rootEl || document.body).appendChild(overlay);
|
|
1882
2607
|
this.loginModalEl = overlay;
|
|
1883
2608
|
const btn = overlay.querySelector("#lcms-login-btn");
|
|
1884
2609
|
const emailInput = overlay.querySelector("#lcms-login-email");
|
|
@@ -1888,6 +2613,16 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1888
2613
|
passInput.addEventListener("keydown", (e) => {
|
|
1889
2614
|
if (e.key === "Enter") doLogin();
|
|
1890
2615
|
});
|
|
2616
|
+
const toggleBtn = overlay.querySelector("#lcms-password-toggle");
|
|
2617
|
+
if (toggleBtn) {
|
|
2618
|
+
toggleBtn.addEventListener("click", () => {
|
|
2619
|
+
const isPassword = passInput.type === "password";
|
|
2620
|
+
passInput.type = isPassword ? "text" : "password";
|
|
2621
|
+
toggleBtn.querySelector(".lcms-eye-open").classList.toggle("lcms-hidden", !isPassword);
|
|
2622
|
+
toggleBtn.querySelector(".lcms-eye-closed").classList.toggle("lcms-hidden", isPassword);
|
|
2623
|
+
passInput.focus();
|
|
2624
|
+
});
|
|
2625
|
+
}
|
|
1891
2626
|
setTimeout(() => emailInput.focus(), 50);
|
|
1892
2627
|
}
|
|
1893
2628
|
hideLoginModal() {
|
|
@@ -1923,7 +2658,21 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1923
2658
|
}
|
|
1924
2659
|
const token = data?.token || data?.authToken || data?.auth_token || data?.data?.token;
|
|
1925
2660
|
if (!token) throw new Error("Login successful but no token returned.");
|
|
1926
|
-
|
|
2661
|
+
_XtroedgeCMS.secureSet("builder_token", token);
|
|
2662
|
+
if (data?.get_url) _XtroedgeCMS.secureSet("xtroedge_get_url", data.get_url);
|
|
2663
|
+
else _XtroedgeCMS.secureRemove("xtroedge_get_url");
|
|
2664
|
+
if (data?.published_url) _XtroedgeCMS.secureSet("xtroedge_published_url", data.published_url);
|
|
2665
|
+
else _XtroedgeCMS.secureRemove("xtroedge_published_url");
|
|
2666
|
+
if (data?.image_url) _XtroedgeCMS.secureSet("xtroedge_image_url", data.image_url);
|
|
2667
|
+
else _XtroedgeCMS.secureRemove("xtroedge_image_url");
|
|
2668
|
+
if (data?.client_api) {
|
|
2669
|
+
_XtroedgeCMS.secureSet("xtroedge_client_api", data.client_api);
|
|
2670
|
+
this.config.clientApi = data.client_api;
|
|
2671
|
+
} else {
|
|
2672
|
+
_XtroedgeCMS.secureRemove("xtroedge_client_api");
|
|
2673
|
+
}
|
|
2674
|
+
if (data?.save_url) _XtroedgeCMS.secureSet("xtroedge_save_url", data.save_url);
|
|
2675
|
+
else _XtroedgeCMS.secureRemove("xtroedge_save_url");
|
|
1927
2676
|
this.hideLoginModal();
|
|
1928
2677
|
this.editMode = true;
|
|
1929
2678
|
this.pendingEditMode = false;
|
|
@@ -1979,10 +2728,10 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1979
2728
|
document.documentElement.style.setProperty("--lcms-primary-dark", this.getDarkerColor(color));
|
|
1980
2729
|
if (this.editMode) {
|
|
1981
2730
|
for (const [el] of this.managedElements) {
|
|
1982
|
-
el.style.outline
|
|
2731
|
+
el.style.setProperty("outline", `2px dashed ${color}`, "important");
|
|
1983
2732
|
}
|
|
1984
2733
|
for (const [img] of this.managedImages) {
|
|
1985
|
-
img.style.outline
|
|
2734
|
+
img.style.setProperty("outline", `2px dashed ${color}`, "important");
|
|
1986
2735
|
}
|
|
1987
2736
|
}
|
|
1988
2737
|
const swatches = this.panelEl?.querySelectorAll(".lcms-theme-swatch");
|
|
@@ -1995,23 +2744,71 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1995
2744
|
// STATIC CONVENIENCE
|
|
1996
2745
|
// ===============================================
|
|
1997
2746
|
/** Quick init - create and start CMS in one call */
|
|
1998
|
-
static create(config) {
|
|
2747
|
+
static async create(config) {
|
|
1999
2748
|
const cms = new _XtroedgeCMS(config);
|
|
2000
|
-
cms.init();
|
|
2749
|
+
await cms.init();
|
|
2001
2750
|
return cms;
|
|
2002
2751
|
}
|
|
2003
2752
|
/** Auto-init: called automatically when package is loaded. No user code needed. */
|
|
2004
|
-
static autoInit() {
|
|
2753
|
+
static async autoInit() {
|
|
2005
2754
|
if (window.__xtroedge_cms_instance__) return;
|
|
2006
2755
|
const userConfig = window.__XTROEDGE_CMS_CONFIG__ || {};
|
|
2007
|
-
const cms = _XtroedgeCMS.create(userConfig);
|
|
2756
|
+
const cms = await _XtroedgeCMS.create(userConfig);
|
|
2008
2757
|
window.__xtroedge_cms_instance__ = cms;
|
|
2009
2758
|
}
|
|
2010
2759
|
};
|
|
2760
|
+
// ===== Encrypted vault (single key stores all sensitive data) =====
|
|
2761
|
+
_XtroedgeCMS._SK = "xTr0EdG3_s3cUr3_k3y!@#2024";
|
|
2762
|
+
_XtroedgeCMS._VAULT_KEY = "_xtd";
|
|
2763
|
+
// ===============================================
|
|
2764
|
+
// LICENSE VALIDATION
|
|
2765
|
+
// ===============================================
|
|
2766
|
+
_XtroedgeCMS.LICENSE_CACHE_KEY = "xtroedge_license_cache";
|
|
2767
|
+
_XtroedgeCMS.LICENSE_CACHE_TTL = 24 * 60 * 60 * 1e3;
|
|
2768
|
+
// Tags where Enter should be blocked (single-line elements)
|
|
2769
|
+
_XtroedgeCMS.SINGLE_LINE_TAGS = /* @__PURE__ */ new Set([
|
|
2770
|
+
"H1",
|
|
2771
|
+
"H2",
|
|
2772
|
+
"H3",
|
|
2773
|
+
"H4",
|
|
2774
|
+
"H5",
|
|
2775
|
+
"H6",
|
|
2776
|
+
"BUTTON",
|
|
2777
|
+
"LABEL",
|
|
2778
|
+
"SPAN",
|
|
2779
|
+
"A",
|
|
2780
|
+
"SMALL",
|
|
2781
|
+
"B",
|
|
2782
|
+
"STRONG",
|
|
2783
|
+
"I",
|
|
2784
|
+
"EM"
|
|
2785
|
+
]);
|
|
2786
|
+
// Allowed HTML tags for sanitization
|
|
2787
|
+
_XtroedgeCMS.ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
2788
|
+
"B",
|
|
2789
|
+
"STRONG",
|
|
2790
|
+
"I",
|
|
2791
|
+
"EM",
|
|
2792
|
+
"U",
|
|
2793
|
+
"S",
|
|
2794
|
+
"STRIKE",
|
|
2795
|
+
"A",
|
|
2796
|
+
"BR",
|
|
2797
|
+
"UL",
|
|
2798
|
+
"OL",
|
|
2799
|
+
"LI",
|
|
2800
|
+
"SUB",
|
|
2801
|
+
"SUP"
|
|
2802
|
+
]);
|
|
2803
|
+
// Allowed attributes per tag
|
|
2804
|
+
_XtroedgeCMS.ALLOWED_ATTRS = {
|
|
2805
|
+
A: /* @__PURE__ */ new Set(["href", "target", "rel"])
|
|
2806
|
+
};
|
|
2807
|
+
var XtroedgeCMS = _XtroedgeCMS;
|
|
2011
2808
|
|
|
2012
2809
|
// src/index.ts
|
|
2013
|
-
function boot() {
|
|
2014
|
-
XtroedgeCMS.autoInit();
|
|
2810
|
+
async function boot() {
|
|
2811
|
+
await XtroedgeCMS.autoInit();
|
|
2015
2812
|
}
|
|
2016
2813
|
if (typeof window !== "undefined") {
|
|
2017
2814
|
if (document.readyState === "loading") {
|