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.mjs
CHANGED
|
@@ -278,6 +278,16 @@ var CMS_STYLES = `
|
|
|
278
278
|
}
|
|
279
279
|
.lcms-login-input:focus { border-color: var(--lcms-primary, #00C853); }
|
|
280
280
|
.lcms-login-input::placeholder { color: rgba(255,255,255,0.25); }
|
|
281
|
+
.lcms-password-wrapper { position: relative; display: flex; align-items: center; }
|
|
282
|
+
.lcms-password-wrapper .lcms-login-input { padding-right: 40px; width: 100%; }
|
|
283
|
+
.lcms-password-toggle {
|
|
284
|
+
position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
|
|
285
|
+
background: none; border: none !important; outline: none !important; cursor: pointer; padding: 4px;
|
|
286
|
+
color: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center;
|
|
287
|
+
transition: color 0.2s; line-height: 0; z-index: 1;
|
|
288
|
+
}
|
|
289
|
+
.lcms-password-toggle:hover { color: rgba(0,0,0,0.8); }
|
|
290
|
+
.lcms-password-toggle svg { display: block; }
|
|
281
291
|
.lcms-login-btn {
|
|
282
292
|
width: 100%; padding: 11px; margin-top: 6px; border: none; border-radius: 8px; cursor: pointer;
|
|
283
293
|
background: linear-gradient(135deg, var(--lcms-primary, #00C853), var(--lcms-primary-dark, #2E7D32));
|
|
@@ -293,8 +303,100 @@ var CMS_STYLES = `
|
|
|
293
303
|
}
|
|
294
304
|
.lcms-login-error.visible { display: block; }
|
|
295
305
|
|
|
306
|
+
/* LICENSE OVERLAY */
|
|
307
|
+
.lcms-license-overlay {
|
|
308
|
+
position: fixed; inset: 0; z-index: 10020;
|
|
309
|
+
display: flex; align-items: center; justify-content: center;
|
|
310
|
+
background: rgba(8, 8, 15, 0.85);
|
|
311
|
+
backdrop-filter: blur(20px) saturate(1.4);
|
|
312
|
+
-webkit-backdrop-filter: blur(20px) saturate(1.4);
|
|
313
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
314
|
+
}
|
|
315
|
+
.lcms-license-box {
|
|
316
|
+
background: #13151a; border: 1px solid rgba(255,255,255,0.1);
|
|
317
|
+
border-radius: 20px; padding: 40px 36px; width: 380px; text-align: center;
|
|
318
|
+
box-shadow: 0 24px 60px rgba(0,0,0,0.6);
|
|
319
|
+
}
|
|
320
|
+
.lcms-license-icon {
|
|
321
|
+
width: 56px; height: 56px; border-radius: 16px; margin: 0 auto 20px;
|
|
322
|
+
background: linear-gradient(135deg, rgba(239,68,68,0.2), rgba(239,68,68,0.1));
|
|
323
|
+
border: 1px solid rgba(239,68,68,0.3);
|
|
324
|
+
display: flex; align-items: center; justify-content: center;
|
|
325
|
+
}
|
|
326
|
+
.lcms-license-icon.trial {
|
|
327
|
+
background: linear-gradient(135deg, rgba(251,191,36,0.2), rgba(251,191,36,0.1));
|
|
328
|
+
border-color: rgba(251,191,36,0.3);
|
|
329
|
+
}
|
|
330
|
+
.lcms-license-title {
|
|
331
|
+
color: white; font-size: 20px; font-weight: 700; margin-bottom: 8px;
|
|
332
|
+
}
|
|
333
|
+
.lcms-license-msg {
|
|
334
|
+
color: rgba(255,255,255,0.5); font-size: 13px; line-height: 1.6; margin-bottom: 24px;
|
|
335
|
+
}
|
|
336
|
+
.lcms-license-btn {
|
|
337
|
+
display: inline-block; padding: 11px 28px; border: none; border-radius: 8px; cursor: pointer;
|
|
338
|
+
background: linear-gradient(135deg, var(--lcms-primary, #00C853), var(--lcms-primary-dark, #2E7D32));
|
|
339
|
+
color: white; font-size: 13px; font-weight: 700; font-family: inherit;
|
|
340
|
+
letter-spacing: 0.3px; transition: filter 0.2s; text-decoration: none;
|
|
341
|
+
}
|
|
342
|
+
.lcms-license-btn:hover { filter: brightness(1.1); }
|
|
343
|
+
.lcms-license-sub {
|
|
344
|
+
color: rgba(255,255,255,0.3); font-size: 11px; margin-top: 16px;
|
|
345
|
+
}
|
|
346
|
+
.lcms-license-sub a { color: rgba(var(--lcms-primary-rgb, 0,200,83),0.7); text-decoration: none; }
|
|
347
|
+
.lcms-license-sub a:hover { text-decoration: underline; }
|
|
348
|
+
|
|
349
|
+
/* TRIAL BANNER */
|
|
350
|
+
.lcms-trial-banner {
|
|
351
|
+
display: flex; align-items: center; gap: 6px;
|
|
352
|
+
background: rgba(251,191,36,0.12); border: 1px solid rgba(251,191,36,0.3);
|
|
353
|
+
border-radius: 8px; padding: 6px 10px; margin-bottom: 10px;
|
|
354
|
+
font-size: 11px; font-weight: 600; color: #fbbf24;
|
|
355
|
+
}
|
|
356
|
+
.lcms-trial-banner-icon { font-size: 14px; }
|
|
357
|
+
.lcms-trial-banner-text { flex: 1; }
|
|
358
|
+
.lcms-trial-banner-dismiss {
|
|
359
|
+
background: none; border: none; color: rgba(251,191,36,0.5); cursor: pointer;
|
|
360
|
+
padding: 0; font-size: 14px; line-height: 1; transition: color 0.2s;
|
|
361
|
+
}
|
|
362
|
+
.lcms-trial-banner-dismiss:hover { color: #fbbf24; }
|
|
363
|
+
|
|
364
|
+
/* RICH TEXT TOOLBAR */
|
|
365
|
+
.lcms-rich-toolbar {
|
|
366
|
+
position: fixed; z-index: 10010;
|
|
367
|
+
display: flex; align-items: center; gap: 2px;
|
|
368
|
+
padding: 4px 6px;
|
|
369
|
+
background: rgba(30, 15, 60, 0.92);
|
|
370
|
+
backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
|
|
371
|
+
border: 1px solid rgba(var(--lcms-primary-rgb, 0,200,83), 0.3);
|
|
372
|
+
border-radius: 8px;
|
|
373
|
+
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
|
374
|
+
opacity: 0; transform: translateY(8px);
|
|
375
|
+
transition: opacity 0.15s, transform 0.15s;
|
|
376
|
+
pointer-events: none;
|
|
377
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
378
|
+
}
|
|
379
|
+
.lcms-rich-toolbar.visible { opacity: 1; transform: translateY(0); pointer-events: auto; }
|
|
380
|
+
.lcms-rich-toolbar button {
|
|
381
|
+
width: 28px; height: 28px; border: none; border-radius: 4px;
|
|
382
|
+
background: transparent; color: #d0d0d0; cursor: pointer;
|
|
383
|
+
display: flex; align-items: center; justify-content: center;
|
|
384
|
+
font-size: 13px; font-weight: 700; padding: 0;
|
|
385
|
+
transition: background 0.15s, color 0.15s;
|
|
386
|
+
}
|
|
387
|
+
.lcms-rich-toolbar button:hover { background: rgba(var(--lcms-primary-rgb, 0,200,83), 0.3); color: #fff; }
|
|
388
|
+
.lcms-rich-toolbar button.active { background: rgba(var(--lcms-primary-rgb, 0,200,83), 0.5); color: #fff; }
|
|
389
|
+
.lcms-rich-toolbar .lcms-tb-sep { width: 1px; height: 18px; background: rgba(255,255,255,0.12); margin: 0 3px; flex-shrink: 0; }
|
|
390
|
+
|
|
296
391
|
/* HIDDEN ELEMENTS */
|
|
297
392
|
.lcms-hidden { display: none !important; }
|
|
393
|
+
|
|
394
|
+
/* EDIT MODE \u2014 keep animations running, just make CMS elements editable */
|
|
395
|
+
body.lcms-editing [data-cms] {
|
|
396
|
+
user-select: text !important;
|
|
397
|
+
-webkit-user-select: text !important;
|
|
398
|
+
cursor: text !important;
|
|
399
|
+
}
|
|
298
400
|
`;
|
|
299
401
|
|
|
300
402
|
// src/xtroedge-cms.ts
|
|
@@ -338,7 +440,7 @@ var DEFAULT_EDITABLE_TAGS = [
|
|
|
338
440
|
"header",
|
|
339
441
|
"footer"
|
|
340
442
|
];
|
|
341
|
-
var
|
|
443
|
+
var _XtroedgeCMS = class _XtroedgeCMS {
|
|
342
444
|
constructor(config) {
|
|
343
445
|
this.brandingEl = null;
|
|
344
446
|
this.siteIdentifier = "";
|
|
@@ -373,6 +475,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
373
475
|
this.dirtyImageKeys = /* @__PURE__ */ new Set();
|
|
374
476
|
this.registeredKeys = /* @__PURE__ */ new Set();
|
|
375
477
|
// ===== DOM =====
|
|
478
|
+
this.domOriginals = {};
|
|
376
479
|
this.managedElements = /* @__PURE__ */ new Map();
|
|
377
480
|
this.managedImages = /* @__PURE__ */ new Map();
|
|
378
481
|
this.autoDetectedElements = /* @__PURE__ */ new Set();
|
|
@@ -380,6 +483,11 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
380
483
|
this.scanTimeout = null;
|
|
381
484
|
this.activeImageEl = null;
|
|
382
485
|
this.imageCtxMenu = null;
|
|
486
|
+
// ===== Rich Text Toolbar =====
|
|
487
|
+
this.richToolbarEl = null;
|
|
488
|
+
this.activeEditableEl = null;
|
|
489
|
+
this.toolbarHideTimeout = null;
|
|
490
|
+
this.selectionChangeHandler = null;
|
|
383
491
|
// ===== Navigation =====
|
|
384
492
|
this.currentSlug = "";
|
|
385
493
|
this.currentTitle = "";
|
|
@@ -409,6 +517,9 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
409
517
|
this.editModeContent = null;
|
|
410
518
|
this.fileInput = null;
|
|
411
519
|
this.imgOverlay = null;
|
|
520
|
+
// ===== Edit-mode visibility tracking =====
|
|
521
|
+
this.editScrollHandler = null;
|
|
522
|
+
this.editScrollRAF = null;
|
|
412
523
|
// ===== FAB Drag =====
|
|
413
524
|
this.posX = 20;
|
|
414
525
|
this.posY = 20;
|
|
@@ -420,11 +531,19 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
420
531
|
this.hasMoved = false;
|
|
421
532
|
// ===== Timers =====
|
|
422
533
|
this.toastTimer = null;
|
|
534
|
+
// ===== License =====
|
|
535
|
+
this.licenseValid = false;
|
|
536
|
+
this.licenseStatus = null;
|
|
537
|
+
this.licenseOverlayEl = null;
|
|
538
|
+
this.trialBannerDismissed = false;
|
|
423
539
|
// ===== IndexedDB =====
|
|
424
540
|
this.db = null;
|
|
425
541
|
// ===== i18n cache =====
|
|
426
542
|
this.translationCache = /* @__PURE__ */ new Map();
|
|
543
|
+
/** Set of positioned ancestors we added pointer-events:none to (for cleanup) */
|
|
544
|
+
this.modifiedAncestors = /* @__PURE__ */ new Set();
|
|
427
545
|
this.config = config || {};
|
|
546
|
+
if (!this.config.apiBase) this.config.apiBase = "https://backend-xi-lime-d90e4p1ysf.vercel.app/api";
|
|
428
547
|
this.containerSelector = this.config.containerSelector || "";
|
|
429
548
|
this.editableTags = (this.config.editableTags || DEFAULT_EDITABLE_TAGS).map((t) => t.toLowerCase());
|
|
430
549
|
this.languages = this.config.languages || ["en"];
|
|
@@ -433,6 +552,8 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
433
552
|
this.historyRetentionMs = (this.config.historyRetentionDays || 7) * 24 * 60 * 60 * 1e3;
|
|
434
553
|
this.currentLang = this.detectCurrentLanguage();
|
|
435
554
|
this.siteIdentifier = this.resolveSiteIdentifier();
|
|
555
|
+
const savedClientApi = _XtroedgeCMS.secureGet("xtroedge_client_api");
|
|
556
|
+
if (savedClientApi) this.config.clientApi = savedClientApi;
|
|
436
557
|
const savedColor = localStorage.getItem("xtroedge_theme_color");
|
|
437
558
|
if (savedColor) this.highlightColor = savedColor;
|
|
438
559
|
this.boundMouseMove = (e) => this.onDragMove(e);
|
|
@@ -442,13 +563,69 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
442
563
|
this.boundPopState = () => this.handleNavigation();
|
|
443
564
|
this.boundHashChange = () => this.handleNavigation();
|
|
444
565
|
}
|
|
566
|
+
static _enc(value) {
|
|
567
|
+
const k = _XtroedgeCMS._SK;
|
|
568
|
+
let result = "";
|
|
569
|
+
for (let i = 0; i < value.length; i++) {
|
|
570
|
+
result += String.fromCharCode(value.charCodeAt(i) ^ k.charCodeAt(i % k.length));
|
|
571
|
+
}
|
|
572
|
+
return btoa(result);
|
|
573
|
+
}
|
|
574
|
+
static _dec(encoded) {
|
|
575
|
+
try {
|
|
576
|
+
const k = _XtroedgeCMS._SK;
|
|
577
|
+
const decoded = atob(encoded);
|
|
578
|
+
let result = "";
|
|
579
|
+
for (let i = 0; i < decoded.length; i++) {
|
|
580
|
+
result += String.fromCharCode(decoded.charCodeAt(i) ^ k.charCodeAt(i % k.length));
|
|
581
|
+
}
|
|
582
|
+
return result;
|
|
583
|
+
} catch {
|
|
584
|
+
return "";
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
static _readVault() {
|
|
588
|
+
try {
|
|
589
|
+
const raw = localStorage.getItem(_XtroedgeCMS._VAULT_KEY);
|
|
590
|
+
if (!raw) return {};
|
|
591
|
+
return JSON.parse(_XtroedgeCMS._dec(raw));
|
|
592
|
+
} catch {
|
|
593
|
+
return {};
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
static _writeVault(vault) {
|
|
597
|
+
try {
|
|
598
|
+
localStorage.setItem(_XtroedgeCMS._VAULT_KEY, _XtroedgeCMS._enc(JSON.stringify(vault)));
|
|
599
|
+
} catch {
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
static secureSet(key, value) {
|
|
603
|
+
const v = _XtroedgeCMS._readVault();
|
|
604
|
+
v[key] = value;
|
|
605
|
+
_XtroedgeCMS._writeVault(v);
|
|
606
|
+
}
|
|
607
|
+
static secureGet(key) {
|
|
608
|
+
return _XtroedgeCMS._readVault()[key] || "";
|
|
609
|
+
}
|
|
610
|
+
static secureRemove(key) {
|
|
611
|
+
const v = _XtroedgeCMS._readVault();
|
|
612
|
+
delete v[key];
|
|
613
|
+
_XtroedgeCMS._writeVault(v);
|
|
614
|
+
}
|
|
615
|
+
static secureClear() {
|
|
616
|
+
try {
|
|
617
|
+
localStorage.removeItem(_XtroedgeCMS._VAULT_KEY);
|
|
618
|
+
} catch {
|
|
619
|
+
}
|
|
620
|
+
}
|
|
445
621
|
// ===============================================
|
|
446
622
|
// PUBLIC API
|
|
447
623
|
// ===============================================
|
|
448
|
-
init() {
|
|
449
|
-
this.posY = window.innerHeight - 72;
|
|
624
|
+
async init() {
|
|
450
625
|
this.injectStyles();
|
|
451
626
|
this.applyThemeColor(this.highlightColor);
|
|
627
|
+
await this.validateLicense();
|
|
628
|
+
this.posY = window.innerHeight - 72;
|
|
452
629
|
this.buildUI();
|
|
453
630
|
this.interceptNavigation();
|
|
454
631
|
this.observer = new MutationObserver(() => {
|
|
@@ -459,7 +636,12 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
459
636
|
document.head.appendChild(this.styleEl);
|
|
460
637
|
}
|
|
461
638
|
if (this.scanTimeout) clearTimeout(this.scanTimeout);
|
|
462
|
-
this.scanTimeout = setTimeout(() =>
|
|
639
|
+
this.scanTimeout = setTimeout(() => {
|
|
640
|
+
this.autoDetectAndScan();
|
|
641
|
+
if (Object.keys(this.pageTexts).length > 0) {
|
|
642
|
+
this.updateElementTexts();
|
|
643
|
+
}
|
|
644
|
+
}, 150);
|
|
463
645
|
});
|
|
464
646
|
this.observer.observe(document.body, { childList: true, subtree: true });
|
|
465
647
|
window.addEventListener("popstate", this.boundPopState);
|
|
@@ -470,6 +652,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
470
652
|
this.cleanupManagedElements();
|
|
471
653
|
this.styleEl?.remove();
|
|
472
654
|
this.rootEl?.remove();
|
|
655
|
+
this.licenseOverlayEl?.remove();
|
|
473
656
|
window.removeEventListener("popstate", this.boundPopState);
|
|
474
657
|
window.removeEventListener("hashchange", this.boundHashChange);
|
|
475
658
|
document.removeEventListener("mousemove", this.boundMouseMove);
|
|
@@ -483,6 +666,113 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
483
666
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
484
667
|
this.db?.close();
|
|
485
668
|
}
|
|
669
|
+
// 24 hours
|
|
670
|
+
async validateLicense() {
|
|
671
|
+
const licenseKey = this.config.licenseKey || _XtroedgeCMS.secureGet("builder_token") || "";
|
|
672
|
+
if (!licenseKey) {
|
|
673
|
+
this.licenseStatus = { valid: false, plan: "invalid", message: "No license key provided." };
|
|
674
|
+
this.licenseValid = false;
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const cached = this.getLicenseCache();
|
|
678
|
+
if (cached) {
|
|
679
|
+
this.licenseStatus = cached;
|
|
680
|
+
this.licenseValid = cached.valid;
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const validateUrl = this.config.licenseValidateUrl || (this.config.apiBase ? `${this.config.apiBase}/license/validate` : "");
|
|
684
|
+
if (!validateUrl) {
|
|
685
|
+
this.licenseValid = true;
|
|
686
|
+
this.licenseStatus = { valid: true, plan: "trial", message: "No license API configured." };
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
const builderToken = _XtroedgeCMS.secureGet("builder_token") || "";
|
|
691
|
+
const headers = { "Content-Type": "application/json" };
|
|
692
|
+
if (builderToken) headers["Authorization"] = `Bearer ${builderToken}`;
|
|
693
|
+
const res = await fetch(validateUrl, {
|
|
694
|
+
method: "POST",
|
|
695
|
+
headers,
|
|
696
|
+
body: JSON.stringify({
|
|
697
|
+
licenseKey,
|
|
698
|
+
domain: window.location.hostname
|
|
699
|
+
})
|
|
700
|
+
});
|
|
701
|
+
let data;
|
|
702
|
+
try {
|
|
703
|
+
data = await res.json();
|
|
704
|
+
} catch {
|
|
705
|
+
throw new Error("Invalid response from license server");
|
|
706
|
+
}
|
|
707
|
+
this.licenseStatus = data;
|
|
708
|
+
this.setLicenseCache(data);
|
|
709
|
+
this.licenseValid = data.valid;
|
|
710
|
+
} catch {
|
|
711
|
+
const fallback = this.getLicenseCache(true);
|
|
712
|
+
if (fallback && fallback.valid) {
|
|
713
|
+
this.licenseValid = true;
|
|
714
|
+
this.licenseStatus = fallback;
|
|
715
|
+
} else {
|
|
716
|
+
this.licenseStatus = { valid: false, plan: "invalid", message: "Unable to verify license. Please check your internet connection." };
|
|
717
|
+
this.licenseValid = false;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
getLicenseCache(ignoreTTL = false) {
|
|
722
|
+
try {
|
|
723
|
+
const raw = _XtroedgeCMS.secureGet(_XtroedgeCMS.LICENSE_CACHE_KEY);
|
|
724
|
+
if (!raw) return null;
|
|
725
|
+
const { status, timestamp, key } = JSON.parse(raw);
|
|
726
|
+
const currentKey = this.config.licenseKey || _XtroedgeCMS.secureGet("builder_token") || "";
|
|
727
|
+
if (key !== currentKey) return null;
|
|
728
|
+
if (!ignoreTTL && Date.now() - timestamp > _XtroedgeCMS.LICENSE_CACHE_TTL) return null;
|
|
729
|
+
return status;
|
|
730
|
+
} catch {
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
setLicenseCache(status) {
|
|
735
|
+
try {
|
|
736
|
+
const currentKey = this.config.licenseKey || _XtroedgeCMS.secureGet("builder_token") || "";
|
|
737
|
+
_XtroedgeCMS.secureSet(_XtroedgeCMS.LICENSE_CACHE_KEY, JSON.stringify({
|
|
738
|
+
status,
|
|
739
|
+
timestamp: Date.now(),
|
|
740
|
+
key: currentKey
|
|
741
|
+
}));
|
|
742
|
+
} catch {
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
/** Shows toast when user tries to edit with expired/invalid license */
|
|
746
|
+
showLicenseExpiredToast() {
|
|
747
|
+
const isExpired = this.licenseStatus?.plan === "expired";
|
|
748
|
+
const msg = isExpired ? "Your free trial has expired. Subscribe to enable editing." : this.licenseStatus?.message || "License validation failed. Editing is disabled.";
|
|
749
|
+
this.showToast(msg, "error");
|
|
750
|
+
}
|
|
751
|
+
/** Returns trial/expired banner HTML if applicable, or empty string */
|
|
752
|
+
getTrialBannerHTML() {
|
|
753
|
+
if (this.trialBannerDismissed || !this.licenseStatus) return "";
|
|
754
|
+
if (!this.licenseValid) {
|
|
755
|
+
const msg = this.licenseStatus.plan === "expired" ? "Trial expired \u2014 editing disabled. Subscribe to continue." : "License invalid \u2014 editing disabled.";
|
|
756
|
+
return `
|
|
757
|
+
<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;">
|
|
758
|
+
<span class="lcms-trial-banner-icon">\u{1F512}</span>
|
|
759
|
+
<span class="lcms-trial-banner-text">${msg}</span>
|
|
760
|
+
<button class="lcms-trial-banner-dismiss" id="lcms-trial-dismiss" title="Dismiss" style="color:rgba(239,68,68,0.5);">\u2715</button>
|
|
761
|
+
</div>
|
|
762
|
+
`;
|
|
763
|
+
}
|
|
764
|
+
if (this.licenseStatus.plan === "trial") {
|
|
765
|
+
const days = this.licenseStatus.daysLeft ?? 0;
|
|
766
|
+
return `
|
|
767
|
+
<div class="lcms-trial-banner" id="lcms-trial-banner">
|
|
768
|
+
<span class="lcms-trial-banner-icon">\u23F3</span>
|
|
769
|
+
<span class="lcms-trial-banner-text">Free Trial: ${days} day${days !== 1 ? "s" : ""} remaining</span>
|
|
770
|
+
<button class="lcms-trial-banner-dismiss" id="lcms-trial-dismiss" title="Dismiss">\u2715</button>
|
|
771
|
+
</div>
|
|
772
|
+
`;
|
|
773
|
+
}
|
|
774
|
+
return "";
|
|
775
|
+
}
|
|
486
776
|
// ===============================================
|
|
487
777
|
// STYLES INJECTION
|
|
488
778
|
// ===============================================
|
|
@@ -538,7 +828,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
538
828
|
const editFromSession = sessionStorage.getItem("builder_edit_mode") === "true";
|
|
539
829
|
const wantsEdit = editViaParam || editFromSession;
|
|
540
830
|
if (wantsEdit) {
|
|
541
|
-
const token =
|
|
831
|
+
const token = _XtroedgeCMS.secureGet("builder_token");
|
|
542
832
|
if (!token) {
|
|
543
833
|
if (!this.pendingEditMode) {
|
|
544
834
|
this.pendingEditMode = true;
|
|
@@ -551,7 +841,12 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
551
841
|
}
|
|
552
842
|
return;
|
|
553
843
|
}
|
|
554
|
-
this.
|
|
844
|
+
if (!this.licenseValid) {
|
|
845
|
+
this.showLicenseExpiredToast();
|
|
846
|
+
this.editMode = false;
|
|
847
|
+
} else {
|
|
848
|
+
this.editMode = true;
|
|
849
|
+
}
|
|
555
850
|
}
|
|
556
851
|
if (!slugChanged && this.initialized) {
|
|
557
852
|
this.updateUI();
|
|
@@ -644,6 +939,21 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
644
939
|
const isDomain = this.siteIdentifier.includes(".");
|
|
645
940
|
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>`;
|
|
646
941
|
this.panelEl.appendChild(this.siteIdEl);
|
|
942
|
+
const trialHTML = this.getTrialBannerHTML();
|
|
943
|
+
if (trialHTML) {
|
|
944
|
+
const trialWrap = this.createElement("div", "");
|
|
945
|
+
trialWrap.innerHTML = trialHTML;
|
|
946
|
+
this.panelEl.appendChild(trialWrap);
|
|
947
|
+
setTimeout(() => {
|
|
948
|
+
const dismissBtn = document.getElementById("lcms-trial-dismiss");
|
|
949
|
+
dismissBtn?.addEventListener("click", (e) => {
|
|
950
|
+
e.stopPropagation();
|
|
951
|
+
this.trialBannerDismissed = true;
|
|
952
|
+
const banner = document.getElementById("lcms-trial-banner");
|
|
953
|
+
banner?.remove();
|
|
954
|
+
});
|
|
955
|
+
}, 0);
|
|
956
|
+
}
|
|
647
957
|
this.editModeContent = this.createElement("div", "lcms-hidden");
|
|
648
958
|
this.changesInfoEl = this.createElement("div", "lcms-changes-info lcms-hidden");
|
|
649
959
|
this.editModeContent.appendChild(this.changesInfoEl);
|
|
@@ -848,8 +1158,8 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
848
1158
|
let parent = el.parentElement;
|
|
849
1159
|
while (parent && parent !== document.body) {
|
|
850
1160
|
const tag = parent.tagName.toLowerCase();
|
|
851
|
-
if (tag
|
|
852
|
-
if (tag
|
|
1161
|
+
if (tag.endsWith("-header")) return "/header";
|
|
1162
|
+
if (tag.endsWith("-footer")) return "/footer";
|
|
853
1163
|
parent = parent.parentElement;
|
|
854
1164
|
}
|
|
855
1165
|
return this.currentSlug;
|
|
@@ -867,11 +1177,13 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
867
1177
|
el.setAttribute("data-cms-section", sectionSlug);
|
|
868
1178
|
this.autoDetectedElements.add(el);
|
|
869
1179
|
};
|
|
1180
|
+
const RICH_TEXT_INLINE = /* @__PURE__ */ new Set(["B", "STRONG", "I", "EM", "U", "S", "STRIKE", "DEL", "SUB", "SUP", "MARK"]);
|
|
870
1181
|
const selector = this.editableTags.join(",");
|
|
871
1182
|
const elements = container.querySelectorAll(selector);
|
|
872
1183
|
elements.forEach((el) => {
|
|
873
1184
|
if (el.hasAttribute("data-cms")) return;
|
|
874
|
-
if (el.closest("#xtroedge-cms-root, script, style, noscript")) return;
|
|
1185
|
+
if (el.closest("#xtroedge-cms-root, #lcms-login-modal, script, style, noscript")) return;
|
|
1186
|
+
if (RICH_TEXT_INLINE.has(el.tagName) && el.parentElement?.closest("[data-cms]")) return;
|
|
875
1187
|
if (el.children.length > 3) return;
|
|
876
1188
|
const text = this.getDirectTextContent(el).trim();
|
|
877
1189
|
if (text.length < 2) return;
|
|
@@ -880,14 +1192,14 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
880
1192
|
const dataEditables = container.querySelectorAll("[data-editable]");
|
|
881
1193
|
dataEditables.forEach((el) => {
|
|
882
1194
|
if (el.hasAttribute("data-cms")) return;
|
|
883
|
-
if (el.closest("#xtroedge-cms-root, script, style, noscript")) return;
|
|
1195
|
+
if (el.closest("#xtroedge-cms-root, #lcms-login-modal, script, style, noscript")) return;
|
|
884
1196
|
assignKey(el, getSectionSlug(el));
|
|
885
1197
|
});
|
|
886
1198
|
}
|
|
887
1199
|
scanDOM() {
|
|
888
1200
|
const elements = document.querySelectorAll("[data-cms]");
|
|
889
1201
|
elements.forEach((el) => {
|
|
890
|
-
if (el.closest("#xtroedge-cms-root")) return;
|
|
1202
|
+
if (el.closest("#xtroedge-cms-root, #lcms-login-modal")) return;
|
|
891
1203
|
const key = el.getAttribute("data-cms");
|
|
892
1204
|
this.registeredKeys.add(key);
|
|
893
1205
|
if (!this.managedElements.has(el)) {
|
|
@@ -935,7 +1247,13 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
935
1247
|
hasEditableChildren(el) {
|
|
936
1248
|
return !!el.querySelector("[data-cms]");
|
|
937
1249
|
}
|
|
938
|
-
|
|
1250
|
+
getElementContent(el) {
|
|
1251
|
+
if (this.hasEditableChildren(el)) {
|
|
1252
|
+
return this.getDirectTextContent(el).trim();
|
|
1253
|
+
}
|
|
1254
|
+
return this.richTextEnabled ? el.innerHTML?.trim() || "" : el.textContent?.trim() || "";
|
|
1255
|
+
}
|
|
1256
|
+
setElementContent(el, val) {
|
|
939
1257
|
if (this.hasEditableChildren(el)) {
|
|
940
1258
|
const textNodes = [];
|
|
941
1259
|
for (let i = 0; i < el.childNodes.length; i++) {
|
|
@@ -943,28 +1261,38 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
943
1261
|
}
|
|
944
1262
|
if (textNodes.length > 0) textNodes[0].textContent = val;
|
|
945
1263
|
else el.prepend(document.createTextNode(val));
|
|
1264
|
+
} else if (this.richTextEnabled) {
|
|
1265
|
+
el.innerHTML = this.sanitizeHTML(val);
|
|
946
1266
|
} else {
|
|
947
1267
|
el.textContent = val;
|
|
948
1268
|
}
|
|
949
1269
|
}
|
|
1270
|
+
// Legacy alias for backward compat with internal calls
|
|
1271
|
+
setDirectTextContent(el, val) {
|
|
1272
|
+
this.setElementContent(el, val);
|
|
1273
|
+
}
|
|
950
1274
|
// ===============================================
|
|
951
1275
|
// ELEMENT EDITING
|
|
952
1276
|
// ===============================================
|
|
953
1277
|
attachElement(el, key) {
|
|
954
|
-
const
|
|
1278
|
+
const getContent = () => this.getElementContent(el);
|
|
955
1279
|
const blurHandler = () => {
|
|
956
|
-
const text =
|
|
1280
|
+
const text = getContent();
|
|
957
1281
|
const currentVal = this.getPageText(key);
|
|
958
1282
|
if (text !== currentVal) this.onTextChanged(key, text);
|
|
1283
|
+
this.hideRichToolbar();
|
|
959
1284
|
};
|
|
960
1285
|
const keydownHandler = (e) => {
|
|
961
1286
|
if (e.key === "Enter") {
|
|
1287
|
+
if (this.richTextEnabled && !_XtroedgeCMS.SINGLE_LINE_TAGS.has(el.tagName)) {
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
962
1290
|
e.preventDefault();
|
|
963
1291
|
el.blur();
|
|
964
1292
|
}
|
|
965
1293
|
};
|
|
966
1294
|
const inputHandler = () => {
|
|
967
|
-
const text =
|
|
1295
|
+
const text = getContent();
|
|
968
1296
|
const currentVal = this.getPageText(key);
|
|
969
1297
|
if (text !== currentVal) this.onTextChanged(key, text);
|
|
970
1298
|
};
|
|
@@ -991,12 +1319,16 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
991
1319
|
this.managedElements.delete(el);
|
|
992
1320
|
}
|
|
993
1321
|
}
|
|
1322
|
+
/** Stop propagation so parent carousels (Swiper, Owl, etc.) don't hijack the event */
|
|
1323
|
+
static stopProp(e) {
|
|
1324
|
+
e.stopPropagation();
|
|
1325
|
+
}
|
|
994
1326
|
enableElementEdit(el, _key, blurH, keyH, inputH, clickH) {
|
|
995
1327
|
const val = this.getPageText(_key);
|
|
996
|
-
if (val) this.
|
|
1328
|
+
if (val) this.setElementContent(el, val);
|
|
997
1329
|
el.setAttribute("contenteditable", "true");
|
|
998
|
-
el.style.outline
|
|
999
|
-
el.style.
|
|
1330
|
+
el.style.setProperty("outline", `2px dashed ${this.highlightColor}`, "important");
|
|
1331
|
+
el.style.setProperty("outline-offset", "-2px", "important");
|
|
1000
1332
|
el.style.cursor = "text";
|
|
1001
1333
|
el.style.transition = "background 0.2s";
|
|
1002
1334
|
el.style.minWidth = "20px";
|
|
@@ -1004,37 +1336,199 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1004
1336
|
el.addEventListener("keydown", keyH);
|
|
1005
1337
|
el.addEventListener("input", inputH);
|
|
1006
1338
|
el.addEventListener("click", clickH, true);
|
|
1339
|
+
el.addEventListener("mousedown", _XtroedgeCMS.stopProp, true);
|
|
1340
|
+
el.addEventListener("touchstart", _XtroedgeCMS.stopProp, true);
|
|
1341
|
+
el.addEventListener("focus", () => this.showRichToolbar(el));
|
|
1007
1342
|
}
|
|
1008
1343
|
disableElementEdit(el, _key, blurH, keyH, inputH, clickH) {
|
|
1009
1344
|
el.removeAttribute("contenteditable");
|
|
1010
|
-
el.style.outline
|
|
1011
|
-
el.style.outline
|
|
1012
|
-
el.style.outlineOffset = "";
|
|
1345
|
+
el.style.removeProperty("outline");
|
|
1346
|
+
el.style.removeProperty("outline-offset");
|
|
1013
1347
|
el.style.cursor = "";
|
|
1014
1348
|
el.style.transition = "";
|
|
1015
1349
|
el.style.minWidth = "";
|
|
1016
1350
|
const val = this.getPageText(_key);
|
|
1017
|
-
if (val) this.
|
|
1351
|
+
if (val) this.setElementContent(el, val);
|
|
1018
1352
|
el.removeEventListener("blur", blurH);
|
|
1019
1353
|
el.removeEventListener("keydown", keyH);
|
|
1020
1354
|
el.removeEventListener("input", inputH);
|
|
1021
1355
|
el.removeEventListener("click", clickH, true);
|
|
1356
|
+
el.removeEventListener("mousedown", _XtroedgeCMS.stopProp, true);
|
|
1357
|
+
el.removeEventListener("touchstart", _XtroedgeCMS.stopProp, true);
|
|
1022
1358
|
}
|
|
1023
1359
|
applyEditMode(editMode) {
|
|
1360
|
+
document.body.classList.toggle("lcms-editing", editMode);
|
|
1361
|
+
if (editMode) {
|
|
1362
|
+
this.editScrollHandler = () => {
|
|
1363
|
+
if (this.editScrollRAF) return;
|
|
1364
|
+
this.editScrollRAF = requestAnimationFrame(() => {
|
|
1365
|
+
this.syncEditablePointerEvents();
|
|
1366
|
+
this.editScrollRAF = null;
|
|
1367
|
+
});
|
|
1368
|
+
};
|
|
1369
|
+
window.addEventListener("scroll", this.editScrollHandler, { passive: true });
|
|
1370
|
+
this.syncEditablePointerEvents();
|
|
1371
|
+
} else {
|
|
1372
|
+
if (this.editScrollHandler) {
|
|
1373
|
+
window.removeEventListener("scroll", this.editScrollHandler);
|
|
1374
|
+
this.editScrollHandler = null;
|
|
1375
|
+
}
|
|
1376
|
+
if (this.editScrollRAF) {
|
|
1377
|
+
cancelAnimationFrame(this.editScrollRAF);
|
|
1378
|
+
this.editScrollRAF = null;
|
|
1379
|
+
}
|
|
1380
|
+
for (const [el] of this.managedElements) el.style.pointerEvents = "";
|
|
1381
|
+
this.modifiedAncestors.forEach((a) => {
|
|
1382
|
+
a.style.pointerEvents = "";
|
|
1383
|
+
});
|
|
1384
|
+
this.modifiedAncestors.clear();
|
|
1385
|
+
}
|
|
1024
1386
|
for (const [el, info] of this.managedElements) {
|
|
1025
1387
|
if (editMode) this.enableElementEdit(el, info.key, info.blurHandler, info.keydownHandler, info.inputHandler, info.clickHandler);
|
|
1026
1388
|
else this.disableElementEdit(el, info.key, info.blurHandler, info.keydownHandler, info.inputHandler, info.clickHandler);
|
|
1027
1389
|
}
|
|
1028
1390
|
this.applyImageEditMode(editMode);
|
|
1391
|
+
if (editMode) this.createRichToolbar();
|
|
1392
|
+
else this.destroyRichToolbar();
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* For each managed element, find the nearest position:absolute/fixed ancestor.
|
|
1396
|
+
* If that ancestor is invisible (opacity ≈ 0), set pointer-events:none on it
|
|
1397
|
+
* so clicks pass through to the visible element behind it.
|
|
1398
|
+
*/
|
|
1399
|
+
syncEditablePointerEvents() {
|
|
1400
|
+
const checked = /* @__PURE__ */ new Map();
|
|
1401
|
+
for (const [el] of this.managedElements) {
|
|
1402
|
+
const ancestor = this.findPositionedAncestor(el);
|
|
1403
|
+
if (!ancestor) continue;
|
|
1404
|
+
if (!checked.has(ancestor)) {
|
|
1405
|
+
const cs = window.getComputedStyle(ancestor);
|
|
1406
|
+
const visible = parseFloat(cs.opacity) >= 0.15 && cs.visibility !== "hidden" && cs.display !== "none";
|
|
1407
|
+
checked.set(ancestor, visible);
|
|
1408
|
+
if (!visible) {
|
|
1409
|
+
ancestor.style.pointerEvents = "none";
|
|
1410
|
+
this.modifiedAncestors.add(ancestor);
|
|
1411
|
+
} else {
|
|
1412
|
+
if (this.modifiedAncestors.has(ancestor)) {
|
|
1413
|
+
ancestor.style.pointerEvents = "";
|
|
1414
|
+
this.modifiedAncestors.delete(ancestor);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
/** Walk up from el to find the nearest position:absolute or position:fixed ancestor */
|
|
1421
|
+
findPositionedAncestor(el) {
|
|
1422
|
+
let cur = el.parentElement;
|
|
1423
|
+
while (cur && cur !== document.body) {
|
|
1424
|
+
if (cur.closest("#xtroedge-cms-root")) return null;
|
|
1425
|
+
const pos = window.getComputedStyle(cur).position;
|
|
1426
|
+
if (pos === "absolute" || pos === "fixed") return cur;
|
|
1427
|
+
cur = cur.parentElement;
|
|
1428
|
+
}
|
|
1429
|
+
return null;
|
|
1430
|
+
}
|
|
1431
|
+
killAnimations() {
|
|
1432
|
+
const win = window;
|
|
1433
|
+
if (win.ScrollTrigger) {
|
|
1434
|
+
try {
|
|
1435
|
+
win.ScrollTrigger.getAll().forEach((t) => t.kill(true));
|
|
1436
|
+
} catch {
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
if (win.gsap) {
|
|
1440
|
+
try {
|
|
1441
|
+
win.gsap.globalTimeline.clear();
|
|
1442
|
+
win.gsap.killTweensOf("*");
|
|
1443
|
+
} catch {
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
if (win.AOS) {
|
|
1447
|
+
try {
|
|
1448
|
+
win.AOS.refreshHard?.();
|
|
1449
|
+
} catch {
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
this.removePinSpacers();
|
|
1453
|
+
this.clearAnimationInlineStyles();
|
|
1454
|
+
setTimeout(() => {
|
|
1455
|
+
this.removePinSpacers();
|
|
1456
|
+
this.clearAnimationInlineStyles();
|
|
1457
|
+
}, 500);
|
|
1458
|
+
setTimeout(() => {
|
|
1459
|
+
this.removePinSpacers();
|
|
1460
|
+
this.clearAnimationInlineStyles();
|
|
1461
|
+
}, 1500);
|
|
1462
|
+
}
|
|
1463
|
+
/** Remove GSAP ScrollTrigger pin-spacer wrapper divs and restore original elements */
|
|
1464
|
+
removePinSpacers() {
|
|
1465
|
+
document.querySelectorAll(".pin-spacer").forEach((spacer) => {
|
|
1466
|
+
const child = spacer.firstElementChild;
|
|
1467
|
+
if (child) {
|
|
1468
|
+
spacer.parentNode?.insertBefore(child, spacer);
|
|
1469
|
+
child.style.cssText = "";
|
|
1470
|
+
}
|
|
1471
|
+
spacer.remove();
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
/** Clear only animation-related inline styles (set by GSAP/ScrollTrigger) without breaking layout */
|
|
1475
|
+
clearAnimationInlineStyles() {
|
|
1476
|
+
const container = this.containerSelector ? document.querySelector(this.containerSelector) || document.body : document.body;
|
|
1477
|
+
container.querySelectorAll("*").forEach((el) => {
|
|
1478
|
+
if (el.closest("#xtroedge-cms-root")) return;
|
|
1479
|
+
const s = el.style;
|
|
1480
|
+
if (s.pointerEvents === "none") s.pointerEvents = "";
|
|
1481
|
+
if (s.userSelect === "none") s.userSelect = "";
|
|
1482
|
+
if (s.visibility === "hidden") s.visibility = "";
|
|
1483
|
+
if (s.clipPath && s.clipPath !== "none") s.clipPath = "";
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
/** Capture each element's current DOM text before CMS applies saved edits */
|
|
1487
|
+
captureDomOriginals() {
|
|
1488
|
+
this.domOriginals = {};
|
|
1489
|
+
for (const [el, info] of this.managedElements) {
|
|
1490
|
+
this.domOriginals[info.key] = this.getElementContent(el);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
/** Ensure every entry in pageTexts has _orig for current language (from domOriginals) */
|
|
1494
|
+
ensureOriginals() {
|
|
1495
|
+
const origKey = `_orig_${this.currentLang}`;
|
|
1496
|
+
for (const key of Object.keys(this.pageTexts)) {
|
|
1497
|
+
if (!this.pageTexts[key][origKey] && this.domOriginals[key]) {
|
|
1498
|
+
this.pageTexts[key][origKey] = this.domOriginals[key];
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
/** Find a managed element whose current text matches the given original text */
|
|
1503
|
+
findElementByOriginal(originalText, tagName) {
|
|
1504
|
+
for (const [el] of this.managedElements) {
|
|
1505
|
+
if (el.tagName !== tagName) continue;
|
|
1506
|
+
const current = this.getElementContent(el);
|
|
1507
|
+
if (current === originalText) return el;
|
|
1508
|
+
}
|
|
1509
|
+
return null;
|
|
1029
1510
|
}
|
|
1030
1511
|
updateElementTexts() {
|
|
1031
1512
|
this.observer?.disconnect();
|
|
1513
|
+
let orphanedCount = 0;
|
|
1032
1514
|
for (const [el, info] of this.managedElements) {
|
|
1033
1515
|
if (document.activeElement === el) continue;
|
|
1034
|
-
const
|
|
1516
|
+
const entry = this.pageTexts[info.key];
|
|
1517
|
+
if (!entry) continue;
|
|
1518
|
+
const val = entry[this.currentLang];
|
|
1035
1519
|
if (!val) continue;
|
|
1036
|
-
const
|
|
1037
|
-
if (
|
|
1520
|
+
const orig = entry[`_orig_${this.currentLang}`];
|
|
1521
|
+
if (!orig) {
|
|
1522
|
+
const current = this.getElementContent(el);
|
|
1523
|
+
if (val !== current) this.setElementContent(el, val);
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1526
|
+
const domText = this.domOriginals[info.key] || this.getElementContent(el);
|
|
1527
|
+
if (domText === orig) {
|
|
1528
|
+
if (val !== this.getElementContent(el)) this.setElementContent(el, val);
|
|
1529
|
+
} else {
|
|
1530
|
+
orphanedCount++;
|
|
1531
|
+
}
|
|
1038
1532
|
}
|
|
1039
1533
|
this.observer?.observe(document.body, { childList: true, subtree: true });
|
|
1040
1534
|
}
|
|
@@ -1043,6 +1537,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1043
1537
|
this.managedElements.clear();
|
|
1044
1538
|
for (const el of this.autoDetectedElements) el.removeAttribute("data-cms");
|
|
1045
1539
|
this.autoDetectedElements.clear();
|
|
1540
|
+
this.domOriginals = {};
|
|
1046
1541
|
this.cleanupManagedImages();
|
|
1047
1542
|
}
|
|
1048
1543
|
// ===============================================
|
|
@@ -1062,7 +1557,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1062
1557
|
const info = this.managedImages.get(img);
|
|
1063
1558
|
if (info) {
|
|
1064
1559
|
img.removeEventListener("contextmenu", info.ctxHandler);
|
|
1065
|
-
img.style.outline
|
|
1560
|
+
img.style.removeProperty("outline");
|
|
1066
1561
|
img.style.cursor = "";
|
|
1067
1562
|
this.managedImages.delete(img);
|
|
1068
1563
|
}
|
|
@@ -1082,14 +1577,14 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1082
1577
|
enableImageEdit(img, ctxHandler) {
|
|
1083
1578
|
if (!img.dataset.origTitle) img.dataset.origTitle = img.title || "";
|
|
1084
1579
|
img.title = "Right-click to change image";
|
|
1085
|
-
img.style.outline
|
|
1580
|
+
img.style.setProperty("outline", `2px dashed ${this.highlightColor}`, "important");
|
|
1086
1581
|
img.style.cursor = "context-menu";
|
|
1087
1582
|
img.addEventListener("contextmenu", ctxHandler);
|
|
1088
1583
|
}
|
|
1089
1584
|
disableImageEdit(img, ctxHandler) {
|
|
1090
1585
|
img.title = img.dataset.origTitle || "";
|
|
1091
1586
|
delete img.dataset.origTitle;
|
|
1092
|
-
img.style.outline
|
|
1587
|
+
img.style.removeProperty("outline");
|
|
1093
1588
|
img.style.cursor = "";
|
|
1094
1589
|
img.removeEventListener("contextmenu", ctxHandler);
|
|
1095
1590
|
}
|
|
@@ -1138,7 +1633,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1138
1633
|
this.dismissImageCtxMenu();
|
|
1139
1634
|
});
|
|
1140
1635
|
menu.appendChild(btn);
|
|
1141
|
-
document.body.appendChild(menu);
|
|
1636
|
+
(this.rootEl || document.body).appendChild(menu);
|
|
1142
1637
|
this.imageCtxMenu = menu;
|
|
1143
1638
|
const closeHandler = (e) => {
|
|
1144
1639
|
if (!menu.contains(e.target)) {
|
|
@@ -1169,14 +1664,18 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1169
1664
|
}
|
|
1170
1665
|
async uploadImageToApi(dataUrl) {
|
|
1171
1666
|
try {
|
|
1172
|
-
const
|
|
1667
|
+
const storedImageUrl = _XtroedgeCMS.secureGet("xtroedge_image_url");
|
|
1668
|
+
const clientBase = this.config.clientApi || this.config.apiBase;
|
|
1669
|
+
const uploadUrl = storedImageUrl ? this.resolveClientUrl(storedImageUrl) : `${clientBase}/web/upload-image`;
|
|
1670
|
+
const res = await this.apiFetch(uploadUrl, {
|
|
1173
1671
|
method: "POST",
|
|
1174
1672
|
headers: { "Content-Type": "application/json" },
|
|
1175
1673
|
body: JSON.stringify({ url: dataUrl })
|
|
1176
1674
|
});
|
|
1177
1675
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1178
1676
|
const data = await res.json();
|
|
1179
|
-
const
|
|
1677
|
+
const imgOriginBase = this.config.clientApi || this.config.apiBase || "";
|
|
1678
|
+
const baseUrl = this.config.imageBaseUrl || (imgOriginBase ? new URL(imgOriginBase).origin : "");
|
|
1180
1679
|
const fullUrl = baseUrl + data.path;
|
|
1181
1680
|
if (this.activeImageEl) {
|
|
1182
1681
|
const key = this.managedImages.get(this.activeImageEl)?.key || "";
|
|
@@ -1213,6 +1712,10 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1213
1712
|
this.canUndo = true;
|
|
1214
1713
|
this.canRedo = false;
|
|
1215
1714
|
if (!this.pageTexts[key]) this.pageTexts[key] = {};
|
|
1715
|
+
const origKey = `_orig_${this.currentLang}`;
|
|
1716
|
+
if (!this.pageTexts[key][origKey]) {
|
|
1717
|
+
this.pageTexts[key][origKey] = this.domOriginals[key] || "";
|
|
1718
|
+
}
|
|
1216
1719
|
this.pageTexts[key] = { ...this.pageTexts[key], [this.currentLang]: newValue };
|
|
1217
1720
|
this.dirtyKeys.add(key);
|
|
1218
1721
|
this.unsavedChanges = this.dirtyKeys.size + this.dirtyImageKeys.size;
|
|
@@ -1243,6 +1746,162 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1243
1746
|
}
|
|
1244
1747
|
}
|
|
1245
1748
|
// ===============================================
|
|
1749
|
+
// RICH TEXT TOOLBAR
|
|
1750
|
+
// ===============================================
|
|
1751
|
+
get richTextEnabled() {
|
|
1752
|
+
return this.config.richText !== false;
|
|
1753
|
+
}
|
|
1754
|
+
sanitizeHTML(html) {
|
|
1755
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
1756
|
+
const clean = (node) => {
|
|
1757
|
+
const children = Array.from(node.childNodes);
|
|
1758
|
+
for (const child of children) {
|
|
1759
|
+
if (child.nodeType === Node.TEXT_NODE) continue;
|
|
1760
|
+
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
1761
|
+
const el = child;
|
|
1762
|
+
if (!_XtroedgeCMS.ALLOWED_TAGS.has(el.tagName)) {
|
|
1763
|
+
while (el.firstChild) el.parentNode?.insertBefore(el.firstChild, el);
|
|
1764
|
+
el.remove();
|
|
1765
|
+
} else {
|
|
1766
|
+
const allowedAttrs = _XtroedgeCMS.ALLOWED_ATTRS[el.tagName] || /* @__PURE__ */ new Set();
|
|
1767
|
+
for (const attr of Array.from(el.attributes)) {
|
|
1768
|
+
if (!allowedAttrs.has(attr.name)) el.removeAttribute(attr.name);
|
|
1769
|
+
}
|
|
1770
|
+
if (el.tagName === "A") {
|
|
1771
|
+
const href = el.getAttribute("href") || "";
|
|
1772
|
+
if (href.trim().toLowerCase().startsWith("javascript:")) {
|
|
1773
|
+
el.setAttribute("href", "#");
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
clean(el);
|
|
1777
|
+
}
|
|
1778
|
+
} else {
|
|
1779
|
+
child.remove();
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
clean(doc.body);
|
|
1784
|
+
return doc.body.innerHTML;
|
|
1785
|
+
}
|
|
1786
|
+
createRichToolbar() {
|
|
1787
|
+
if (!this.richTextEnabled || this.richToolbarEl) return;
|
|
1788
|
+
const toolbar = document.createElement("div");
|
|
1789
|
+
toolbar.className = "lcms-rich-toolbar";
|
|
1790
|
+
const buttons = [
|
|
1791
|
+
{ cmd: "bold", label: "B", title: "Bold" },
|
|
1792
|
+
{ cmd: "italic", label: "<i>I</i>", title: "Italic" },
|
|
1793
|
+
{ cmd: "underline", label: "<u>U</u>", title: "Underline" },
|
|
1794
|
+
{ cmd: "strikeThrough", label: "<s>S</s>", title: "Strikethrough" }
|
|
1795
|
+
];
|
|
1796
|
+
const linkButtons = [
|
|
1797
|
+
{ cmd: "createLink", label: "\u{1F517}", title: "Insert Link" },
|
|
1798
|
+
{ cmd: "unlink", label: "\u2298", title: "Remove Link" }
|
|
1799
|
+
];
|
|
1800
|
+
const utilButtons = [
|
|
1801
|
+
{ cmd: "removeFormat", label: "\u2715", title: "Clear Formatting" }
|
|
1802
|
+
];
|
|
1803
|
+
const makeBtn = (item) => {
|
|
1804
|
+
const btn = document.createElement("button");
|
|
1805
|
+
btn.innerHTML = item.label;
|
|
1806
|
+
btn.title = item.title;
|
|
1807
|
+
btn.dataset.cmd = item.cmd;
|
|
1808
|
+
btn.addEventListener("mousedown", (e) => {
|
|
1809
|
+
e.preventDefault();
|
|
1810
|
+
this.execToolbarCommand(item.cmd);
|
|
1811
|
+
});
|
|
1812
|
+
return btn;
|
|
1813
|
+
};
|
|
1814
|
+
const addSep = () => {
|
|
1815
|
+
const sep = document.createElement("div");
|
|
1816
|
+
sep.className = "lcms-tb-sep";
|
|
1817
|
+
toolbar.appendChild(sep);
|
|
1818
|
+
};
|
|
1819
|
+
buttons.forEach((b) => toolbar.appendChild(makeBtn(b)));
|
|
1820
|
+
addSep();
|
|
1821
|
+
linkButtons.forEach((b) => toolbar.appendChild(makeBtn(b)));
|
|
1822
|
+
addSep();
|
|
1823
|
+
utilButtons.forEach((b) => toolbar.appendChild(makeBtn(b)));
|
|
1824
|
+
this.rootEl?.appendChild(toolbar);
|
|
1825
|
+
this.richToolbarEl = toolbar;
|
|
1826
|
+
this.selectionChangeHandler = () => this.updateToolbarState();
|
|
1827
|
+
document.addEventListener("selectionchange", this.selectionChangeHandler);
|
|
1828
|
+
}
|
|
1829
|
+
destroyRichToolbar() {
|
|
1830
|
+
if (this.richToolbarEl) {
|
|
1831
|
+
this.richToolbarEl.remove();
|
|
1832
|
+
this.richToolbarEl = null;
|
|
1833
|
+
}
|
|
1834
|
+
if (this.selectionChangeHandler) {
|
|
1835
|
+
document.removeEventListener("selectionchange", this.selectionChangeHandler);
|
|
1836
|
+
this.selectionChangeHandler = null;
|
|
1837
|
+
}
|
|
1838
|
+
this.activeEditableEl = null;
|
|
1839
|
+
}
|
|
1840
|
+
execToolbarCommand(cmd) {
|
|
1841
|
+
this.observer?.disconnect();
|
|
1842
|
+
if (this.scanTimeout) {
|
|
1843
|
+
clearTimeout(this.scanTimeout);
|
|
1844
|
+
this.scanTimeout = null;
|
|
1845
|
+
}
|
|
1846
|
+
if (cmd === "createLink") {
|
|
1847
|
+
const url = prompt("Enter URL:", "https://");
|
|
1848
|
+
if (url) document.execCommand("createLink", false, url);
|
|
1849
|
+
} else {
|
|
1850
|
+
document.execCommand(cmd, false);
|
|
1851
|
+
}
|
|
1852
|
+
this.updateToolbarState();
|
|
1853
|
+
if (this.activeEditableEl) {
|
|
1854
|
+
this.activeEditableEl.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1855
|
+
}
|
|
1856
|
+
setTimeout(() => {
|
|
1857
|
+
this.observer?.observe(document.body, { childList: true, subtree: true });
|
|
1858
|
+
}, 50);
|
|
1859
|
+
}
|
|
1860
|
+
showRichToolbar(el) {
|
|
1861
|
+
if (!this.richTextEnabled || !this.richToolbarEl) return;
|
|
1862
|
+
if (this.toolbarHideTimeout) {
|
|
1863
|
+
clearTimeout(this.toolbarHideTimeout);
|
|
1864
|
+
this.toolbarHideTimeout = null;
|
|
1865
|
+
}
|
|
1866
|
+
this.activeEditableEl = el;
|
|
1867
|
+
this.positionToolbar(el);
|
|
1868
|
+
this.richToolbarEl.classList.add("visible");
|
|
1869
|
+
this.updateToolbarState();
|
|
1870
|
+
}
|
|
1871
|
+
positionToolbar(el) {
|
|
1872
|
+
if (!this.richToolbarEl) return;
|
|
1873
|
+
const rect = el.getBoundingClientRect();
|
|
1874
|
+
const tbRect = this.richToolbarEl.getBoundingClientRect();
|
|
1875
|
+
let top = rect.top - tbRect.height - 8;
|
|
1876
|
+
let left = rect.left + rect.width / 2 - tbRect.width / 2;
|
|
1877
|
+
if (top < 4) top = rect.bottom + 8;
|
|
1878
|
+
if (left < 4) left = 4;
|
|
1879
|
+
if (left + tbRect.width > window.innerWidth - 4) left = window.innerWidth - tbRect.width - 4;
|
|
1880
|
+
this.richToolbarEl.style.top = `${top}px`;
|
|
1881
|
+
this.richToolbarEl.style.left = `${left}px`;
|
|
1882
|
+
}
|
|
1883
|
+
hideRichToolbar() {
|
|
1884
|
+
if (!this.richToolbarEl) return;
|
|
1885
|
+
this.toolbarHideTimeout = setTimeout(() => {
|
|
1886
|
+
this.richToolbarEl?.classList.remove("visible");
|
|
1887
|
+
this.activeEditableEl = null;
|
|
1888
|
+
this.toolbarHideTimeout = null;
|
|
1889
|
+
}, 150);
|
|
1890
|
+
}
|
|
1891
|
+
updateToolbarState() {
|
|
1892
|
+
if (!this.richToolbarEl) return;
|
|
1893
|
+
const cmds = ["bold", "italic", "underline", "strikeThrough"];
|
|
1894
|
+
for (const btn of Array.from(this.richToolbarEl.querySelectorAll("button"))) {
|
|
1895
|
+
const cmd = btn.dataset.cmd;
|
|
1896
|
+
if (cmd && cmds.includes(cmd)) {
|
|
1897
|
+
try {
|
|
1898
|
+
btn.classList.toggle("active", document.queryCommandState(cmd));
|
|
1899
|
+
} catch {
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
// ===============================================
|
|
1246
1905
|
// UNDO / REDO
|
|
1247
1906
|
// ===============================================
|
|
1248
1907
|
onUndo() {
|
|
@@ -1304,7 +1963,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1304
1963
|
this.updateUI();
|
|
1305
1964
|
}
|
|
1306
1965
|
logout() {
|
|
1307
|
-
|
|
1966
|
+
_XtroedgeCMS.secureClear();
|
|
1308
1967
|
sessionStorage.removeItem("builder_edit_mode");
|
|
1309
1968
|
this.editMode = false;
|
|
1310
1969
|
this.isEditAllowed = false;
|
|
@@ -1330,6 +1989,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1330
1989
|
}
|
|
1331
1990
|
}
|
|
1332
1991
|
this.autoDetectAndScan();
|
|
1992
|
+
this.captureDomOriginals();
|
|
1333
1993
|
if (this.editMode) this.applyEditMode(true);
|
|
1334
1994
|
if (this.config.apiBase) {
|
|
1335
1995
|
if (loadDraft) this.loadPageContent("draft");
|
|
@@ -1361,17 +2021,40 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1361
2021
|
getPageSection() {
|
|
1362
2022
|
return this.currentSlug.replace(/^\//, "").replace(/-/g, "_").toUpperCase();
|
|
1363
2023
|
}
|
|
1364
|
-
|
|
2024
|
+
/** Resolve a stored URL against our server (apiBase) */
|
|
2025
|
+
resolveServerUrl(storedUrl) {
|
|
2026
|
+
if (storedUrl.startsWith("/")) return `${this.config.apiBase}${storedUrl}`;
|
|
2027
|
+
return storedUrl;
|
|
2028
|
+
}
|
|
2029
|
+
/** Resolve a stored URL against client's server (clientApi, falls back to apiBase) */
|
|
2030
|
+
resolveClientUrl(storedUrl) {
|
|
2031
|
+
const base = this.config.clientApi || this.config.apiBase;
|
|
2032
|
+
if (storedUrl.startsWith("/")) return `${base}${storedUrl}`;
|
|
2033
|
+
return storedUrl;
|
|
2034
|
+
}
|
|
1365
2035
|
apiFetch(url, opts) {
|
|
1366
|
-
const token =
|
|
2036
|
+
const token = _XtroedgeCMS.secureGet("builder_token") || "";
|
|
2037
|
+
const licenseKey = this.config.licenseKey || "";
|
|
1367
2038
|
const headers = { ...opts?.headers || {} };
|
|
1368
2039
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
2040
|
+
if (licenseKey) headers["X-License-Key"] = licenseKey;
|
|
2041
|
+
const isOurServer = this.config.apiBase && url.startsWith(this.config.apiBase);
|
|
2042
|
+
if (!isOurServer && this.config.clientHeaders) {
|
|
2043
|
+
for (const [k, v] of Object.entries(this.config.clientHeaders)) {
|
|
2044
|
+
if (!headers[k]) headers[k] = v;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
1369
2047
|
return fetch(url, { ...opts, headers });
|
|
1370
2048
|
}
|
|
1371
|
-
// Fetch
|
|
2049
|
+
// Fetch page by slug — if get_url exists in localStorage, call it directly (bypass our server)
|
|
1372
2050
|
async fetchSection(slug, status) {
|
|
1373
2051
|
try {
|
|
1374
|
-
const
|
|
2052
|
+
const storedGetUrl = _XtroedgeCMS.secureGet("xtroedge_get_url");
|
|
2053
|
+
const clientBase = this.config.clientApi || this.config.apiBase;
|
|
2054
|
+
const getBase = storedGetUrl ? this.resolveClientUrl(storedGetUrl) : `${clientBase}/web-page/get`;
|
|
2055
|
+
let getUrl = `${getBase}?slug=${encodeURIComponent(slug)}`;
|
|
2056
|
+
if (status) getUrl += `&status=${encodeURIComponent(status)}`;
|
|
2057
|
+
const res = await this.apiFetch(getUrl);
|
|
1375
2058
|
if (!res.ok) return null;
|
|
1376
2059
|
return await res.json();
|
|
1377
2060
|
} catch {
|
|
@@ -1458,7 +2141,8 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1458
2141
|
for (const key of this.registeredKeys) {
|
|
1459
2142
|
const el = document.querySelector(`[data-cms="${key}"]`);
|
|
1460
2143
|
const val = el?.textContent?.trim() || "";
|
|
1461
|
-
|
|
2144
|
+
const entry = { [this.defaultLanguage]: val };
|
|
2145
|
+
texts[key] = entry;
|
|
1462
2146
|
}
|
|
1463
2147
|
this.pageTexts = texts;
|
|
1464
2148
|
if (this.config.i18nBasePath) {
|
|
@@ -1497,12 +2181,22 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1497
2181
|
return sections;
|
|
1498
2182
|
}
|
|
1499
2183
|
async saveChanges() {
|
|
2184
|
+
if (!this.licenseValid) {
|
|
2185
|
+
this.showLicenseExpiredToast();
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
1500
2188
|
if (!this.config.apiBase) {
|
|
1501
2189
|
this.showToast("No API configured. Set apiBase to enable save.", "error");
|
|
1502
2190
|
return;
|
|
1503
2191
|
}
|
|
1504
2192
|
this.isSaving = true;
|
|
1505
2193
|
this.updateUI();
|
|
2194
|
+
const origKey = `_orig_${this.currentLang}`;
|
|
2195
|
+
for (const key of Object.keys(this.pageTexts)) {
|
|
2196
|
+
if (!this.pageTexts[key][origKey] && this.domOriginals[key]) {
|
|
2197
|
+
this.pageTexts[key][origKey] = this.domOriginals[key];
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
1506
2200
|
try {
|
|
1507
2201
|
const sections = this.groupTextsBySection();
|
|
1508
2202
|
const requests = Object.entries(sections).map(
|
|
@@ -1512,13 +2206,18 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1512
2206
|
body: JSON.stringify({
|
|
1513
2207
|
slug,
|
|
1514
2208
|
title: slug === this.currentSlug ? this.currentTitle : slug.replace("/", ""),
|
|
1515
|
-
site_identifier: this.siteIdentifier,
|
|
1516
2209
|
content: { texts, ...slug === this.currentSlug ? { images: this.pageImages } : {} }
|
|
1517
2210
|
})
|
|
1518
2211
|
})
|
|
1519
2212
|
);
|
|
1520
|
-
const
|
|
1521
|
-
|
|
2213
|
+
const responses = await Promise.all(requests);
|
|
2214
|
+
const saveResults = [];
|
|
2215
|
+
for (const res of responses) {
|
|
2216
|
+
const data = await res.json();
|
|
2217
|
+
saveResults.push({ ok: res.ok, status: res.status, data });
|
|
2218
|
+
}
|
|
2219
|
+
if (saveResults.some((r) => !r.ok)) throw new Error("One or more sections failed to save");
|
|
2220
|
+
console.info("[XtroEdge] Save responses:", saveResults.map((r) => r.data));
|
|
1522
2221
|
this.isSaving = false;
|
|
1523
2222
|
this.resetAfterSave();
|
|
1524
2223
|
this.updateUI();
|
|
@@ -1532,12 +2231,22 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1532
2231
|
}
|
|
1533
2232
|
}
|
|
1534
2233
|
async publishChanges() {
|
|
2234
|
+
if (!this.licenseValid) {
|
|
2235
|
+
this.showLicenseExpiredToast();
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
1535
2238
|
if (!this.config.apiBase) {
|
|
1536
2239
|
this.showToast("No API configured. Set apiBase to enable publish.", "error");
|
|
1537
2240
|
return;
|
|
1538
2241
|
}
|
|
1539
2242
|
this.isPublishing = true;
|
|
1540
2243
|
this.updateUI();
|
|
2244
|
+
const origKey = `_orig_${this.currentLang}`;
|
|
2245
|
+
for (const key of Object.keys(this.pageTexts)) {
|
|
2246
|
+
if (!this.pageTexts[key][origKey] && this.domOriginals[key]) {
|
|
2247
|
+
this.pageTexts[key][origKey] = this.domOriginals[key];
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
1541
2250
|
try {
|
|
1542
2251
|
const sections = this.groupTextsBySection();
|
|
1543
2252
|
const requests = Object.entries(sections).map(
|
|
@@ -1547,13 +2256,18 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1547
2256
|
body: JSON.stringify({
|
|
1548
2257
|
slug,
|
|
1549
2258
|
title: slug === this.currentSlug ? this.currentTitle : slug.replace("/", ""),
|
|
1550
|
-
site_identifier: this.siteIdentifier,
|
|
1551
2259
|
published_content: { texts, ...slug === this.currentSlug ? { images: this.pageImages } : {} }
|
|
1552
2260
|
})
|
|
1553
2261
|
})
|
|
1554
2262
|
);
|
|
1555
2263
|
const results = await Promise.all(requests);
|
|
1556
|
-
|
|
2264
|
+
const publishResults = [];
|
|
2265
|
+
for (const res of results) {
|
|
2266
|
+
const data = await res.json();
|
|
2267
|
+
publishResults.push({ ok: res.ok, status: res.status, data });
|
|
2268
|
+
}
|
|
2269
|
+
if (publishResults.some((r) => !r.ok)) throw new Error("One or more sections failed to publish");
|
|
2270
|
+
console.info("[XtroEdge] Publish responses:", publishResults.map((r) => r.data));
|
|
1557
2271
|
this.isPublishing = false;
|
|
1558
2272
|
this.resetAfterSave();
|
|
1559
2273
|
this.updateUI();
|
|
@@ -1715,6 +2429,11 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1715
2429
|
toggleEditMode(e) {
|
|
1716
2430
|
e.stopPropagation();
|
|
1717
2431
|
const checked = e.target.checked;
|
|
2432
|
+
if (checked && !this.licenseValid) {
|
|
2433
|
+
e.target.checked = false;
|
|
2434
|
+
this.showLicenseExpiredToast();
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
1718
2437
|
if (checked) {
|
|
1719
2438
|
sessionStorage.setItem("builder_edit_mode", "true");
|
|
1720
2439
|
this.editMode = true;
|
|
@@ -1846,13 +2565,19 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1846
2565
|
</div>
|
|
1847
2566
|
<div class="lcms-login-field">
|
|
1848
2567
|
<label class="lcms-login-label">Password</label>
|
|
1849
|
-
<
|
|
2568
|
+
<div class="lcms-password-wrapper">
|
|
2569
|
+
<input class="lcms-login-input" id="lcms-login-password" type="password" placeholder="Enter your password" autocomplete="current-password" />
|
|
2570
|
+
<button type="button" class="lcms-password-toggle" id="lcms-password-toggle" tabindex="-1">
|
|
2571
|
+
<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>
|
|
2572
|
+
<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>
|
|
2573
|
+
</button>
|
|
2574
|
+
</div>
|
|
1850
2575
|
</div>
|
|
1851
2576
|
<button class="lcms-login-btn" id="lcms-login-btn">Sign In</button>
|
|
1852
2577
|
<div class="lcms-login-error" id="lcms-login-error"></div>
|
|
1853
2578
|
</div>
|
|
1854
2579
|
`;
|
|
1855
|
-
document.body.appendChild(overlay);
|
|
2580
|
+
(this.rootEl || document.body).appendChild(overlay);
|
|
1856
2581
|
this.loginModalEl = overlay;
|
|
1857
2582
|
const btn = overlay.querySelector("#lcms-login-btn");
|
|
1858
2583
|
const emailInput = overlay.querySelector("#lcms-login-email");
|
|
@@ -1862,6 +2587,16 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1862
2587
|
passInput.addEventListener("keydown", (e) => {
|
|
1863
2588
|
if (e.key === "Enter") doLogin();
|
|
1864
2589
|
});
|
|
2590
|
+
const toggleBtn = overlay.querySelector("#lcms-password-toggle");
|
|
2591
|
+
if (toggleBtn) {
|
|
2592
|
+
toggleBtn.addEventListener("click", () => {
|
|
2593
|
+
const isPassword = passInput.type === "password";
|
|
2594
|
+
passInput.type = isPassword ? "text" : "password";
|
|
2595
|
+
toggleBtn.querySelector(".lcms-eye-open").classList.toggle("lcms-hidden", !isPassword);
|
|
2596
|
+
toggleBtn.querySelector(".lcms-eye-closed").classList.toggle("lcms-hidden", isPassword);
|
|
2597
|
+
passInput.focus();
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
1865
2600
|
setTimeout(() => emailInput.focus(), 50);
|
|
1866
2601
|
}
|
|
1867
2602
|
hideLoginModal() {
|
|
@@ -1897,7 +2632,21 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1897
2632
|
}
|
|
1898
2633
|
const token = data?.token || data?.authToken || data?.auth_token || data?.data?.token;
|
|
1899
2634
|
if (!token) throw new Error("Login successful but no token returned.");
|
|
1900
|
-
|
|
2635
|
+
_XtroedgeCMS.secureSet("builder_token", token);
|
|
2636
|
+
if (data?.get_url) _XtroedgeCMS.secureSet("xtroedge_get_url", data.get_url);
|
|
2637
|
+
else _XtroedgeCMS.secureRemove("xtroedge_get_url");
|
|
2638
|
+
if (data?.published_url) _XtroedgeCMS.secureSet("xtroedge_published_url", data.published_url);
|
|
2639
|
+
else _XtroedgeCMS.secureRemove("xtroedge_published_url");
|
|
2640
|
+
if (data?.image_url) _XtroedgeCMS.secureSet("xtroedge_image_url", data.image_url);
|
|
2641
|
+
else _XtroedgeCMS.secureRemove("xtroedge_image_url");
|
|
2642
|
+
if (data?.client_api) {
|
|
2643
|
+
_XtroedgeCMS.secureSet("xtroedge_client_api", data.client_api);
|
|
2644
|
+
this.config.clientApi = data.client_api;
|
|
2645
|
+
} else {
|
|
2646
|
+
_XtroedgeCMS.secureRemove("xtroedge_client_api");
|
|
2647
|
+
}
|
|
2648
|
+
if (data?.save_url) _XtroedgeCMS.secureSet("xtroedge_save_url", data.save_url);
|
|
2649
|
+
else _XtroedgeCMS.secureRemove("xtroedge_save_url");
|
|
1901
2650
|
this.hideLoginModal();
|
|
1902
2651
|
this.editMode = true;
|
|
1903
2652
|
this.pendingEditMode = false;
|
|
@@ -1953,10 +2702,10 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1953
2702
|
document.documentElement.style.setProperty("--lcms-primary-dark", this.getDarkerColor(color));
|
|
1954
2703
|
if (this.editMode) {
|
|
1955
2704
|
for (const [el] of this.managedElements) {
|
|
1956
|
-
el.style.outline
|
|
2705
|
+
el.style.setProperty("outline", `2px dashed ${color}`, "important");
|
|
1957
2706
|
}
|
|
1958
2707
|
for (const [img] of this.managedImages) {
|
|
1959
|
-
img.style.outline
|
|
2708
|
+
img.style.setProperty("outline", `2px dashed ${color}`, "important");
|
|
1960
2709
|
}
|
|
1961
2710
|
}
|
|
1962
2711
|
const swatches = this.panelEl?.querySelectorAll(".lcms-theme-swatch");
|
|
@@ -1969,23 +2718,71 @@ var XtroedgeCMS = class _XtroedgeCMS {
|
|
|
1969
2718
|
// STATIC CONVENIENCE
|
|
1970
2719
|
// ===============================================
|
|
1971
2720
|
/** Quick init - create and start CMS in one call */
|
|
1972
|
-
static create(config) {
|
|
2721
|
+
static async create(config) {
|
|
1973
2722
|
const cms = new _XtroedgeCMS(config);
|
|
1974
|
-
cms.init();
|
|
2723
|
+
await cms.init();
|
|
1975
2724
|
return cms;
|
|
1976
2725
|
}
|
|
1977
2726
|
/** Auto-init: called automatically when package is loaded. No user code needed. */
|
|
1978
|
-
static autoInit() {
|
|
2727
|
+
static async autoInit() {
|
|
1979
2728
|
if (window.__xtroedge_cms_instance__) return;
|
|
1980
2729
|
const userConfig = window.__XTROEDGE_CMS_CONFIG__ || {};
|
|
1981
|
-
const cms = _XtroedgeCMS.create(userConfig);
|
|
2730
|
+
const cms = await _XtroedgeCMS.create(userConfig);
|
|
1982
2731
|
window.__xtroedge_cms_instance__ = cms;
|
|
1983
2732
|
}
|
|
1984
2733
|
};
|
|
2734
|
+
// ===== Encrypted vault (single key stores all sensitive data) =====
|
|
2735
|
+
_XtroedgeCMS._SK = "xTr0EdG3_s3cUr3_k3y!@#2024";
|
|
2736
|
+
_XtroedgeCMS._VAULT_KEY = "_xtd";
|
|
2737
|
+
// ===============================================
|
|
2738
|
+
// LICENSE VALIDATION
|
|
2739
|
+
// ===============================================
|
|
2740
|
+
_XtroedgeCMS.LICENSE_CACHE_KEY = "xtroedge_license_cache";
|
|
2741
|
+
_XtroedgeCMS.LICENSE_CACHE_TTL = 24 * 60 * 60 * 1e3;
|
|
2742
|
+
// Tags where Enter should be blocked (single-line elements)
|
|
2743
|
+
_XtroedgeCMS.SINGLE_LINE_TAGS = /* @__PURE__ */ new Set([
|
|
2744
|
+
"H1",
|
|
2745
|
+
"H2",
|
|
2746
|
+
"H3",
|
|
2747
|
+
"H4",
|
|
2748
|
+
"H5",
|
|
2749
|
+
"H6",
|
|
2750
|
+
"BUTTON",
|
|
2751
|
+
"LABEL",
|
|
2752
|
+
"SPAN",
|
|
2753
|
+
"A",
|
|
2754
|
+
"SMALL",
|
|
2755
|
+
"B",
|
|
2756
|
+
"STRONG",
|
|
2757
|
+
"I",
|
|
2758
|
+
"EM"
|
|
2759
|
+
]);
|
|
2760
|
+
// Allowed HTML tags for sanitization
|
|
2761
|
+
_XtroedgeCMS.ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
2762
|
+
"B",
|
|
2763
|
+
"STRONG",
|
|
2764
|
+
"I",
|
|
2765
|
+
"EM",
|
|
2766
|
+
"U",
|
|
2767
|
+
"S",
|
|
2768
|
+
"STRIKE",
|
|
2769
|
+
"A",
|
|
2770
|
+
"BR",
|
|
2771
|
+
"UL",
|
|
2772
|
+
"OL",
|
|
2773
|
+
"LI",
|
|
2774
|
+
"SUB",
|
|
2775
|
+
"SUP"
|
|
2776
|
+
]);
|
|
2777
|
+
// Allowed attributes per tag
|
|
2778
|
+
_XtroedgeCMS.ALLOWED_ATTRS = {
|
|
2779
|
+
A: /* @__PURE__ */ new Set(["href", "target", "rel"])
|
|
2780
|
+
};
|
|
2781
|
+
var XtroedgeCMS = _XtroedgeCMS;
|
|
1985
2782
|
|
|
1986
2783
|
// src/index.ts
|
|
1987
|
-
function boot() {
|
|
1988
|
-
XtroedgeCMS.autoInit();
|
|
2784
|
+
async function boot() {
|
|
2785
|
+
await XtroedgeCMS.autoInit();
|
|
1989
2786
|
}
|
|
1990
2787
|
if (typeof window !== "undefined") {
|
|
1991
2788
|
if (document.readyState === "loading") {
|