Files

222 lines
10 KiB
JavaScript
Raw Permalink Normal View History

/**
* Stoic CMS — in-place editor script
*
* Vendor this file into your website's asset pipeline.
*
* Configuration (set before this script runs):
* window.STOIC_HOST = "https://your-cms-domain.com"; // default: same origin
*
* Usage on your HTML elements:
* data-stoic="schema/slug" open editor for this entry
* data-stoic="schema/slug#field" open editor and focus a specific field
* data-stoic="singleton#field" open editor for a singleton schema
*
* SSO: if STOIC_SSO_SECRET is configured on the server, the admin link
* will generate a signed token so the user lands directly in the CMS.
* Set window.STOIC_SSO_TOKEN before loading this script:
* window.STOIC_SSO_TOKEN = "<?= $ssoToken ?>";
*/
(function () {
"use strict";
const HOST = (window.STOIC_HOST || "").replace(/\/$/, "");
// ── State ────────────────────────────────────────────────────────────────
let editMode = false;
let activePane = null;
let messageListener = null;
// ── Toolbar ──────────────────────────────────────────────────────────────
const bar = document.createElement("div");
bar.className = "stoic-bar";
const btn = document.createElement("button");
btn.className = "stoic-edit-toggle";
btn.setAttribute("aria-label", "Toggle edit mode");
btn.innerHTML =
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg> Edit';
btn.addEventListener("click", function () {
editMode ? exitEditMode() : enterEditMode();
});
const adminLink = document.createElement("a");
adminLink.className = "stoic-admin-link";
adminLink.target = "_blank";
adminLink.rel = "noopener noreferrer";
adminLink.setAttribute("aria-label", "Open CMS admin");
adminLink.innerHTML =
'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>';
// If a pre-generated SSO token is provided, use the SSO endpoint directly.
// Otherwise link to the standard login page.
if (window.STOIC_SSO_TOKEN) {
adminLink.href = HOST + "/cms/sso?" + window.STOIC_SSO_TOKEN;
} else {
adminLink.href = HOST;
}
bar.appendChild(btn);
bar.appendChild(adminLink);
// ── Edit mode ────────────────────────────────────────────────────────────
function enterEditMode() {
editMode = true;
btn.classList.add("active");
document.querySelectorAll("[data-stoic]").forEach(function (el) {
el.classList.add("stoic-target");
el.addEventListener("click", onTargetClick, true);
});
}
function exitEditMode() {
editMode = false;
btn.classList.remove("active");
document.querySelectorAll("[data-stoic]").forEach(function (el) {
el.classList.remove("stoic-target", "stoic-target--active");
el.removeEventListener("click", onTargetClick, true);
});
closePane();
}
function onTargetClick(e) {
e.preventDefault();
e.stopPropagation();
document
.querySelectorAll(".stoic-target--active")
.forEach(function (el) {
el.classList.remove("stoic-target--active");
});
this.classList.add("stoic-target--active");
const attr = this.getAttribute("data-stoic");
const hashIdx = attr.indexOf("#");
const path = hashIdx === -1 ? attr : attr.slice(0, hashIdx);
const field = hashIdx === -1 ? "" : attr.slice(hashIdx + 1);
openPane(path, field);
}
// ── Side pane ────────────────────────────────────────────────────────────
function openPane(path, field) {
closePane();
const overlay = document.createElement("div");
overlay.className = "stoic-overlay";
overlay.addEventListener("click", closePane);
const pane = document.createElement("div");
pane.className = "stoic-pane";
const closeBtn = document.createElement("button");
closeBtn.className = "stoic-pane-close";
closeBtn.setAttribute("aria-label", "Close");
closeBtn.innerHTML =
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
closeBtn.addEventListener("click", closePane);
const iframe = document.createElement("iframe");
iframe.className = "stoic-pane-iframe";
let src = HOST + "/cms/embed/" + path;
if (field) src += "?focus=" + encodeURIComponent(field);
iframe.src = src;
pane.appendChild(closeBtn);
pane.appendChild(iframe);
document.body.appendChild(overlay);
document.body.appendChild(pane);
activePane = { pane: pane, overlay: overlay };
messageListener = function (e) {
if (e.data && e.data.type === "stoic:saved") {
onSaved();
}
};
window.addEventListener("message", messageListener);
requestAnimationFrame(function () {
pane.classList.add("open");
overlay.classList.add("open");
});
}
function closePane() {
if (!activePane) return;
var pane = activePane.pane;
var overlay = activePane.overlay;
pane.classList.remove("open");
overlay.classList.remove("open");
setTimeout(function () {
pane.remove();
overlay.remove();
}, 220);
if (messageListener) {
window.removeEventListener("message", messageListener);
messageListener = null;
}
document
.querySelectorAll(".stoic-target--active")
.forEach(function (el) {
el.classList.remove("stoic-target--active");
});
activePane = null;
}
function onSaved() {
var activeEl = document.querySelector(".stoic-target--active");
if (activeEl) {
activeEl.dispatchEvent(
new CustomEvent("stoic:saved", {
bubbles: true,
composed: true,
}),
);
}
closePane();
}
// ── Styles ───────────────────────────────────────────────────────────────
var style = document.createElement("style");
style.textContent = [
".stoic-bar{position:fixed;bottom:1.25rem;right:1.25rem;z-index:2147483646;display:inline-flex;align-items:center;gap:0.375rem;background:#1a1a2e;border-radius:9999px;padding:0.25rem 0.25rem 0.25rem 0.5rem;box-shadow:0 2px 12px rgba(0,0,0,.35);}",
".stoic-edit-toggle{display:inline-flex;align-items:center;gap:0.4rem;padding:0.35rem 0.75rem;border:none;border-radius:9999px;background:transparent;color:#fff;font-size:0.8rem;font-weight:600;font-family:system-ui,sans-serif;cursor:pointer;transition:background .15s;}",
".stoic-edit-toggle:hover{background:rgba(255,255,255,.1);}",
".stoic-edit-toggle.active{background:#4f46e5;}",
".stoic-admin-link{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;border-radius:50%;color:rgba(255,255,255,.6);text-decoration:none;transition:color .15s,background .15s;}",
".stoic-admin-link:hover{color:#fff;background:rgba(255,255,255,.1);}",
".stoic-target{outline:2px dashed rgba(79,70,229,.5)!important;outline-offset:2px!important;cursor:pointer!important;transition:outline-color .15s;}",
".stoic-target:hover{outline-color:rgba(79,70,229,.9)!important;}",
".stoic-target--active{outline:2px solid #4f46e5!important;outline-offset:2px!important;}",
".stoic-overlay{position:fixed;inset:0;z-index:2147483644;background:rgba(0,0,0,.25);opacity:0;transition:opacity .2s;}",
".stoic-overlay.open{opacity:1;}",
".stoic-pane{position:fixed;top:0;right:0;bottom:0;z-index:2147483645;width:min(480px,100vw);background:#fff;box-shadow:-4px 0 24px rgba(0,0,0,.18);transform:translateX(100%);transition:transform .22s cubic-bezier(.4,0,.2,1);display:flex;flex-direction:column;}",
".stoic-pane.open{transform:translateX(0);}",
".stoic-pane-close{position:absolute;top:.75rem;right:.75rem;z-index:1;width:1.75rem;height:1.75rem;border:none;border-radius:50%;background:rgba(0,0,0,.07);color:#555;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;}",
".stoic-pane-close:hover{background:rgba(0,0,0,.14);}",
".stoic-pane-iframe{flex:1;width:100%;border:none;}",
].join("");
// ── Init ─────────────────────────────────────────────────────────────────
document.head.appendChild(style);
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () {
document.body.appendChild(bar);
});
} else {
document.body.appendChild(bar);
}
})();