/** * 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 = ""; */ (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 = ' 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 = ''; // 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 = ''; 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); } })();