@broberg/cms-inline-edit 0.4.15 → 0.4.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +97 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -7
- package/dist/index.d.ts +24 -7
- package/dist/index.js +94 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -20,16 +20,31 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
applyFieldSlice: () => applyFieldSlice,
|
|
23
24
|
buildConnectUrl: () => buildConnectUrl,
|
|
24
25
|
disconnect: () => disconnect,
|
|
25
26
|
getConnectedToken: () => getConnectedToken,
|
|
26
27
|
initInlineEdit: () => initInlineEdit
|
|
27
28
|
});
|
|
28
29
|
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/field-slice.ts
|
|
32
|
+
function applyFieldSlice(current, original, next) {
|
|
33
|
+
const first = current.indexOf(original);
|
|
34
|
+
if (first === -1) throw new Error("field-slice: original not found");
|
|
35
|
+
if (current.indexOf(original, first + original.length) !== -1) {
|
|
36
|
+
throw new Error("field-slice: original found more than once (ambiguous)");
|
|
37
|
+
}
|
|
38
|
+
return current.slice(0, first) + next + current.slice(first + original.length);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/index.ts
|
|
29
42
|
var DEFAULT_LABELS = {
|
|
30
43
|
bold: "Fed",
|
|
31
44
|
italic: "Kursiv",
|
|
32
45
|
underline: "Understreget",
|
|
46
|
+
orderedList: "Nummereret liste",
|
|
47
|
+
unorderedList: "Punktliste",
|
|
33
48
|
color: "Farve",
|
|
34
49
|
emoji: "Inds\xE6t emoji",
|
|
35
50
|
done: "F\xE6rdig",
|
|
@@ -45,6 +60,8 @@ function resolveOptions(options) {
|
|
|
45
60
|
// No emoji in defaults — the square-pen SVG is the icon; some sites ban emoji.
|
|
46
61
|
connectLabel: "Log ind for at redigere",
|
|
47
62
|
disconnectLabel: "Afbryd",
|
|
63
|
+
logoutLabel: "Log ud",
|
|
64
|
+
connectPrompt: false,
|
|
48
65
|
...options,
|
|
49
66
|
labels: { ...DEFAULT_LABELS, ...options.labels ?? {} }
|
|
50
67
|
};
|
|
@@ -53,16 +70,20 @@ async function initInlineEdit(options) {
|
|
|
53
70
|
if (typeof window === "undefined") return;
|
|
54
71
|
const resolved = resolveOptions(options);
|
|
55
72
|
uiLabels = resolved.labels;
|
|
56
|
-
captureTokenFromUrl(resolved);
|
|
73
|
+
const freshFromUrl = captureTokenFromUrl(resolved);
|
|
57
74
|
const enabled = await checkEnabled(resolved);
|
|
58
75
|
if (!enabled) return;
|
|
59
76
|
const token = getConnectedToken(options);
|
|
60
|
-
if (token) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
if (!token) {
|
|
78
|
+
if (resolved.connectPrompt) {
|
|
79
|
+
showConnectPrompt(resolved);
|
|
80
|
+
listenForTokenMessage(resolved);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
65
83
|
}
|
|
84
|
+
setupFields(token, resolved);
|
|
85
|
+
if (freshFromUrl) enterEditMode();
|
|
86
|
+
else showIdlePill();
|
|
66
87
|
}
|
|
67
88
|
function listenForTokenMessage(options) {
|
|
68
89
|
let cmsOrigin;
|
|
@@ -80,7 +101,8 @@ function listenForTokenMessage(options) {
|
|
|
80
101
|
if (document.querySelector("[data-cms-inline-edit-badge]")) return;
|
|
81
102
|
localStorage.setItem(options.storageKey, data.token);
|
|
82
103
|
document.querySelector("[data-cms-inline-edit-connect]")?.remove();
|
|
83
|
-
|
|
104
|
+
setupFields(data.token, options);
|
|
105
|
+
enterEditMode();
|
|
84
106
|
});
|
|
85
107
|
}
|
|
86
108
|
function getConnectedToken(options) {
|
|
@@ -106,10 +128,11 @@ function disconnect(options) {
|
|
|
106
128
|
function captureTokenFromUrl(options) {
|
|
107
129
|
const url = new URL(window.location.href);
|
|
108
130
|
const urlToken = url.searchParams.get(options.tokenParam);
|
|
109
|
-
if (!urlToken) return;
|
|
131
|
+
if (!urlToken) return false;
|
|
110
132
|
localStorage.setItem(options.storageKey, urlToken);
|
|
111
133
|
url.searchParams.delete(options.tokenParam);
|
|
112
134
|
window.history.replaceState({}, "", url.toString());
|
|
135
|
+
return true;
|
|
113
136
|
}
|
|
114
137
|
async function checkEnabled(options) {
|
|
115
138
|
try {
|
|
@@ -127,6 +150,8 @@ function isExpired(token) {
|
|
|
127
150
|
return exp <= Date.now() / 1e3;
|
|
128
151
|
}
|
|
129
152
|
var SQUARE_PEN_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>';
|
|
153
|
+
var UL_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/></svg>';
|
|
154
|
+
var OL_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="10" x2="21" y1="6" y2="6"/><line x1="10" x2="21" y1="12" y2="12"/><line x1="10" x2="21" y1="18" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/></svg>';
|
|
130
155
|
function makeIcon() {
|
|
131
156
|
const icon = document.createElement("span");
|
|
132
157
|
icon.style.cssText = "display:inline-flex;line-height:0;flex:0 0 auto";
|
|
@@ -153,11 +178,58 @@ function showConnectPrompt(options) {
|
|
|
153
178
|
});
|
|
154
179
|
document.body.appendChild(link);
|
|
155
180
|
}
|
|
156
|
-
|
|
181
|
+
var editingActive = false;
|
|
182
|
+
var fieldsWired = false;
|
|
183
|
+
var stateOptions = null;
|
|
184
|
+
function setupFields(token, options) {
|
|
185
|
+
stateOptions = options;
|
|
186
|
+
if (fieldsWired) return;
|
|
157
187
|
injectStyles();
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
188
|
+
document.querySelectorAll("[data-cms-field]").forEach((el) => wireField(el, token, options));
|
|
189
|
+
fieldsWired = true;
|
|
190
|
+
}
|
|
191
|
+
function enterEditMode() {
|
|
192
|
+
if (!stateOptions || editingActive) return;
|
|
193
|
+
editingActive = true;
|
|
194
|
+
removeIdlePill();
|
|
195
|
+
showActiveBadge(stateOptions);
|
|
196
|
+
}
|
|
197
|
+
function exitEditMode() {
|
|
198
|
+
if (!stateOptions || !editingActive) return;
|
|
199
|
+
deactivateRich();
|
|
200
|
+
editingActive = false;
|
|
201
|
+
document.querySelector("[data-cms-inline-edit-badge]")?.remove();
|
|
202
|
+
showIdlePill();
|
|
203
|
+
}
|
|
204
|
+
function removeIdlePill() {
|
|
205
|
+
document.querySelector("[data-cms-inline-edit-idle]")?.remove();
|
|
206
|
+
}
|
|
207
|
+
function showIdlePill() {
|
|
208
|
+
if (!stateOptions) return;
|
|
209
|
+
const options = stateOptions;
|
|
210
|
+
removeIdlePill();
|
|
211
|
+
const wrap = document.createElement("div");
|
|
212
|
+
wrap.setAttribute("data-cms-inline-edit-idle", "");
|
|
213
|
+
wrap.style.cssText = "position:fixed;bottom:16px;left:16px;display:inline-flex;align-items:stretch;z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);border-radius:999px;overflow:hidden;";
|
|
214
|
+
const edit = document.createElement("button");
|
|
215
|
+
edit.type = "button";
|
|
216
|
+
edit.setAttribute("data-cms-inline-edit-enter", "");
|
|
217
|
+
edit.style.cssText = "display:inline-flex;align-items:center;gap:7px;background:#1c2027;color:#fff;font:600 12px system-ui,sans-serif;padding:8px 14px;border:none;cursor:pointer;";
|
|
218
|
+
const editText = document.createElement("span");
|
|
219
|
+
editText.textContent = options.connectLabel;
|
|
220
|
+
edit.append(makeIcon(), editText);
|
|
221
|
+
edit.addEventListener("click", enterEditMode);
|
|
222
|
+
const out = document.createElement("button");
|
|
223
|
+
out.type = "button";
|
|
224
|
+
out.setAttribute("data-cms-inline-edit-logout", "");
|
|
225
|
+
out.textContent = options.logoutLabel;
|
|
226
|
+
out.style.cssText = "background:#141821;color:#9aa4b2;font:600 11px system-ui,sans-serif;padding:8px 12px;border:none;border-left:1px solid rgba(255,255,255,.12);cursor:pointer;";
|
|
227
|
+
out.addEventListener("click", () => {
|
|
228
|
+
disconnect(options);
|
|
229
|
+
removeIdlePill();
|
|
230
|
+
});
|
|
231
|
+
wrap.append(edit, out);
|
|
232
|
+
document.body.appendChild(wrap);
|
|
161
233
|
}
|
|
162
234
|
function wireField(el, token, options) {
|
|
163
235
|
if (el.dataset.cmsRichtext === "true") {
|
|
@@ -169,6 +241,7 @@ function wireField(el, token, options) {
|
|
|
169
241
|
return;
|
|
170
242
|
}
|
|
171
243
|
el.addEventListener("click", (e) => {
|
|
244
|
+
if (!editingActive) return;
|
|
172
245
|
if (el.getAttribute("contenteditable") === "true") return;
|
|
173
246
|
e.preventDefault();
|
|
174
247
|
e.stopPropagation();
|
|
@@ -199,6 +272,7 @@ var richCtx = null;
|
|
|
199
272
|
var richToolbar = null;
|
|
200
273
|
function wireRichField(el, token, options, mode) {
|
|
201
274
|
el.addEventListener("click", (e) => {
|
|
275
|
+
if (!editingActive) return;
|
|
202
276
|
if (richCtx && richCtx.el === el) return;
|
|
203
277
|
e.preventDefault();
|
|
204
278
|
e.stopPropagation();
|
|
@@ -263,6 +337,9 @@ function buildRichToolbar() {
|
|
|
263
337
|
return s;
|
|
264
338
|
};
|
|
265
339
|
t.appendChild(sep());
|
|
340
|
+
t.appendChild(toolbarButton(UL_SVG, uiLabels.unorderedList, () => document.execCommand("insertUnorderedList")));
|
|
341
|
+
t.appendChild(toolbarButton(OL_SVG, uiLabels.orderedList, () => document.execCommand("insertOrderedList")));
|
|
342
|
+
t.appendChild(sep());
|
|
266
343
|
const clrLabel = document.createElement("label");
|
|
267
344
|
clrLabel.style.cssText = "display:flex;align-items:center;gap:5px;color:#9aa4b2;font-size:12px;cursor:pointer;";
|
|
268
345
|
clrLabel.textContent = uiLabels.color;
|
|
@@ -512,6 +589,7 @@ async function saveField(el, value, token, options) {
|
|
|
512
589
|
const slug = el.dataset.cmsSlug;
|
|
513
590
|
const field = el.dataset.cmsField;
|
|
514
591
|
if (!collection || !slug || !field) return;
|
|
592
|
+
const slice = el.dataset.cmsSlice;
|
|
515
593
|
showPill(el, "saving");
|
|
516
594
|
try {
|
|
517
595
|
const getRes = await fetch(
|
|
@@ -521,7 +599,10 @@ async function saveField(el, value, token, options) {
|
|
|
521
599
|
if (!getRes.ok) throw new Error(`GET failed: ${getRes.status}`);
|
|
522
600
|
const doc = await getRes.json();
|
|
523
601
|
const mergedData = JSON.parse(JSON.stringify(doc.data ?? {}));
|
|
524
|
-
if (
|
|
602
|
+
if (slice !== void 0) {
|
|
603
|
+
const currentVal = typeof mergedData[field] === "string" ? mergedData[field] : "";
|
|
604
|
+
mergedData[field] = applyFieldSlice(currentVal, slice, value);
|
|
605
|
+
} else if (field.includes(".")) {
|
|
525
606
|
setDeepField(mergedData, field, value);
|
|
526
607
|
} else {
|
|
527
608
|
mergedData[field] = value;
|
|
@@ -535,6 +616,7 @@ async function saveField(el, value, token, options) {
|
|
|
535
616
|
}
|
|
536
617
|
);
|
|
537
618
|
if (!patchRes.ok) throw new Error(`PATCH failed: ${patchRes.status}`);
|
|
619
|
+
if (slice !== void 0) el.dataset.cmsSlice = value;
|
|
538
620
|
showPill(el, "saved");
|
|
539
621
|
} catch {
|
|
540
622
|
showPill(el, "error");
|
|
@@ -579,10 +661,7 @@ function showActiveBadge(options) {
|
|
|
579
661
|
badge.append(makeIcon(), text);
|
|
580
662
|
badge.addEventListener("mouseenter", () => badge.style.opacity = "1");
|
|
581
663
|
badge.addEventListener("mouseleave", () => badge.style.opacity = ".9");
|
|
582
|
-
badge.addEventListener("click",
|
|
583
|
-
disconnect(options);
|
|
584
|
-
window.location.reload();
|
|
585
|
-
});
|
|
664
|
+
badge.addEventListener("click", exitEditMode);
|
|
586
665
|
document.body.appendChild(badge);
|
|
587
666
|
}
|
|
588
667
|
function decodeJwtPayload(token) {
|
|
@@ -607,6 +686,7 @@ function injectStyles() {
|
|
|
607
686
|
}
|
|
608
687
|
// Annotate the CommonJS export names for ESM import in node:
|
|
609
688
|
0 && (module.exports = {
|
|
689
|
+
applyFieldSlice,
|
|
610
690
|
buildConnectUrl,
|
|
611
691
|
disconnect,
|
|
612
692
|
getConnectedToken,
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Browser entry — zero dependencies. Site-wide: activated once per browser\n * via a \"connect\" link (see docs/features/F157-inline-editing.md), then\n * every page load on the site automatically offers click-to-edit on any\n * element carrying data-cms-collection/data-cms-slug/data-cms-field\n * (F129's attribute convention) — no per-document step.\n */\n\n/**\n * Labels for the in-editor UI — the rich-text toolbar (bold/italic/underline\n * tooltips, the visible \"colour\" + \"done\" buttons, the emoji tooltip) and the\n * save-status pill. Every field is optional; unset fields keep the Danish\n * default. Pass an English (or any-locale) set on a non-Danish site so the\n * toolbar and status text match the page language.\n */\nexport interface InlineEditLabels {\n bold?: string;\n italic?: string;\n underline?: string;\n color?: string;\n emoji?: string;\n done?: string;\n saving?: string;\n saved?: string;\n error?: string;\n}\n\nexport interface InlineEditOptions {\n /** Base URL of the CMS API, e.g. \"https://webhouse.app\". */\n cmsBaseUrl: string;\n /** Site id passed as ?site= on every CMS API call, e.g. \"broberg-ai\". */\n siteId: string;\n /** URL query param carrying a freshly-minted token from the connect redirect. Default \"cms_edit\". */\n tokenParam?: string;\n /** localStorage key the token is persisted under — survives across sessions/tabs. Default \"wh-inline-edit-token\". */\n storageKey?: string;\n /** Text for the \"not connected yet\" prompt (a square-pen icon is prepended). Default \"Log ind for at redigere\". */\n connectLabel?: string;\n /** Text for the \"connected\" badge — the whole pill is the exit-edit action (icon prepended). Default \"Afbryd\". */\n disconnectLabel?: string;\n /** Localised labels for the toolbar + save-status pill. Defaults are Danish. */\n labels?: InlineEditLabels;\n}\n\ninterface ResolvedOptions extends Required<Omit<InlineEditOptions, \"labels\">> {\n labels: Required<InlineEditLabels>;\n}\n\nconst DEFAULT_LABELS: Required<InlineEditLabels> = {\n bold: \"Fed\",\n italic: \"Kursiv\",\n underline: \"Understreget\",\n color: \"Farve\",\n emoji: \"Indsæt emoji\",\n done: \"Færdig\",\n saving: \"Gemmer…\",\n saved: \"Gemt ✓\",\n error: \"Fejl — prøv igen\",\n};\n\n// Set once from resolved options in initInlineEdit(); read by the (singleton)\n// toolbar and the save-status pill so their signatures stay unchanged. One edit\n// session per page → a single module-level value is safe.\nlet uiLabels: Required<InlineEditLabels> = DEFAULT_LABELS;\n\nfunction resolveOptions(options: InlineEditOptions): ResolvedOptions {\n return {\n tokenParam: \"cms_edit\",\n storageKey: \"wh-inline-edit-token\",\n // No emoji in defaults — the square-pen SVG is the icon; some sites ban emoji.\n connectLabel: \"Log ind for at redigere\",\n disconnectLabel: \"Afbryd\",\n ...options,\n labels: { ...DEFAULT_LABELS, ...(options.labels ?? {}) },\n };\n}\n\nexport async function initInlineEdit(options: InlineEditOptions): Promise<void> {\n if (typeof window === \"undefined\") return;\n const resolved = resolveOptions(options);\n uiLabels = resolved.labels;\n\n captureTokenFromUrl(resolved);\n\n const enabled = await checkEnabled(resolved);\n if (!enabled) return;\n\n const token = getConnectedToken(options);\n if (token) {\n activateEditMode(token, resolved);\n } else {\n showConnectPrompt(resolved);\n // F158 — the connect flow opens in a popup; it posts the freshly-minted\n // token back to this tab. Arm the (origin-validated) receiver.\n listenForTokenMessage(resolved);\n }\n}\n\n/**\n * F158 — receive the edit token from the connect popup via postMessage.\n * Hard origin check (message MUST come from cmsBaseUrl) + type + site + expiry\n * guards before the token is trusted. On success: persist, swap the connect\n * prompt for edit mode — no page reload.\n */\nfunction listenForTokenMessage(options: ResolvedOptions): void {\n let cmsOrigin: string;\n try {\n cmsOrigin = new URL(options.cmsBaseUrl).origin;\n } catch {\n return;\n }\n window.addEventListener(\"message\", (event: MessageEvent) => {\n if (event.origin !== cmsOrigin) return;\n const data = event.data as { type?: string; token?: string; site?: string } | null;\n if (!data || data.type !== \"wh-inline-edit-token\" || typeof data.token !== \"string\") return;\n if (data.site && data.site !== options.siteId) return;\n if (isExpired(data.token)) return;\n if (document.querySelector(\"[data-cms-inline-edit-badge]\")) return; // already active\n localStorage.setItem(options.storageKey, data.token);\n document.querySelector(\"[data-cms-inline-edit-connect]\")?.remove();\n activateEditMode(data.token, options);\n });\n}\n\n/**\n * A valid (non-expired) token already connected in this browser for this\n * site, or null. Also captures a fresh `?cms_edit=` token from the URL\n * first, so a page can call this directly right after the connect redirect\n * without going through initInlineEdit(). Exposed so a site's own /admin\n * page (or any other tool built on the same connected session) can reuse\n * the same storage/expiry logic instead of re-implementing it.\n */\nexport function getConnectedToken(options: InlineEditOptions): string | null {\n if (typeof window === \"undefined\") return null;\n const resolved = resolveOptions(options);\n captureTokenFromUrl(resolved);\n const token = localStorage.getItem(resolved.storageKey);\n if (!token) return null;\n if (isExpired(token)) {\n localStorage.removeItem(resolved.storageKey);\n return null;\n }\n return token;\n}\n\n/** The URL that mints a fresh 30-day site-scoped token and redirects back to `returnUrl`. */\nexport function buildConnectUrl(options: InlineEditOptions, returnUrl: string): string {\n const resolved = resolveOptions(options);\n return (\n `${resolved.cmsBaseUrl}/admin/inline-edit/connect?site=${encodeURIComponent(resolved.siteId)}` +\n `&return=${encodeURIComponent(returnUrl)}`\n );\n}\n\n/** Clears the connected token in this browser (e.g. a \"log out\" action in a site's own /admin panel). */\nexport function disconnect(options: InlineEditOptions): void {\n if (typeof window === \"undefined\") return;\n localStorage.removeItem(resolveOptions(options).storageKey);\n}\n\n/** Captures a token minted by /admin/inline-edit/connect, then strips it from the URL. */\nfunction captureTokenFromUrl(options: ResolvedOptions): void {\n const url = new URL(window.location.href);\n const urlToken = url.searchParams.get(options.tokenParam);\n if (!urlToken) return;\n localStorage.setItem(options.storageKey, urlToken);\n url.searchParams.delete(options.tokenParam);\n window.history.replaceState({}, \"\", url.toString());\n}\n\nasync function checkEnabled(options: ResolvedOptions): Promise<boolean> {\n try {\n const res = await fetch(`${options.cmsBaseUrl}/api/inline-edit/status?site=${options.siteId}`);\n if (!res.ok) return false;\n const body = (await res.json()) as { enabled?: boolean };\n return body.enabled === true;\n } catch {\n return false;\n }\n}\n\nfunction isExpired(token: string): boolean {\n const claims = decodeJwtPayload(token);\n const exp = typeof claims?.exp === \"number\" ? claims.exp : 0;\n return exp <= Date.now() / 1000;\n}\n\n// Lucide \"square-pen\" (https://lucide.dev/icons/square-pen), MIT. Inline SVG so\n// the pill needs no icon-font/runtime dep and inherits `currentColor`. Emoji are\n// deliberately avoided — some consumer sites ban them outright.\nconst SQUARE_PEN_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<path d=\"M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\"/>' +\n '<path d=\"M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z\"/>' +\n \"</svg>\";\n\nfunction makeIcon(): HTMLSpanElement {\n const icon = document.createElement(\"span\");\n icon.style.cssText = \"display:inline-flex;line-height:0;flex:0 0 auto\";\n icon.innerHTML = SQUARE_PEN_SVG;\n return icon;\n}\n\nfunction showConnectPrompt(options: ResolvedOptions): void {\n const connectUrl = buildConnectUrl(options, window.location.href);\n\n const link = document.createElement(\"a\");\n link.href = connectUrl;\n link.setAttribute(\"data-cms-inline-edit-connect\", \"\");\n link.style.cssText =\n \"position:fixed;bottom:16px;left:16px;background:#1c2027;color:#fff;\" +\n \"font:600 12px system-ui,sans-serif;padding:8px 14px;border-radius:999px;\" +\n \"text-decoration:none;z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);\" +\n \"display:inline-flex;align-items:center;gap:7px;\";\n const text = document.createElement(\"span\");\n text.textContent = options.connectLabel;\n link.append(makeIcon(), text);\n // F158 — open the connect flow in a popup so the CMS confirmation screen can\n // say \"close this window\". A user-gesture window.open is not popup-blocked;\n // if it is blocked anyway, fall back to the same-tab redirect (F157 path).\n link.addEventListener(\"click\", (e) => {\n e.preventDefault();\n const popup = window.open(\n connectUrl,\n \"wh-inline-edit-connect\",\n \"width=460,height=640,menubar=no,toolbar=no,location=yes,status=no\",\n );\n if (!popup) window.location.href = connectUrl;\n });\n document.body.appendChild(link);\n}\n\nfunction activateEditMode(token: string, options: ResolvedOptions): void {\n injectStyles();\n showActiveBadge(options);\n\n const fields = document.querySelectorAll<HTMLElement>(\"[data-cms-field]\");\n fields.forEach((el) => wireField(el, token, options));\n}\n\nfunction wireField(el: HTMLElement, token: string, options: ResolvedOptions): void {\n // Rich fields get the floating formatting toolbar. Two save modes:\n // - data-cms-richtext=\"true\" → save as Markdown (article bodies; cms\n // `richtext` contract renders Markdown via marked).\n // - data-cms-html=\"true\" → save innerHTML VERBATIM (intentional-HTML\n // fields: headings/bios/hero with a branded <em class=\"o\"> accent that\n // has no Markdown equivalent — converting would strip it).\n // Everything else stays a plain single-line contenteditable saving textContent.\n if (el.dataset.cmsRichtext === \"true\") {\n wireRichField(el, token, options, \"markdown\");\n return;\n }\n if (el.dataset.cmsHtml === \"true\") {\n wireRichField(el, token, options, \"html\");\n return;\n }\n\n el.addEventListener(\"click\", (e) => {\n if (el.getAttribute(\"contenteditable\") === \"true\") return;\n // Many editable fields (card titles/blurbs) sit inside a clickable <a>/\n // <button> ancestor (the card itself) — without this, \"click to edit\"\n // would immediately navigate away instead of focusing the field.\n e.preventDefault();\n e.stopPropagation();\n el.dataset.cmsOriginalValue = el.textContent ?? \"\";\n el.setAttribute(\"contenteditable\", \"true\");\n el.focus();\n });\n\n el.addEventListener(\"blur\", () => {\n el.removeAttribute(\"contenteditable\");\n const original = el.dataset.cmsOriginalValue ?? \"\";\n const current = el.textContent ?? \"\";\n if (current.trim() === original.trim()) return;\n void saveField(el, current.trim(), token, options);\n });\n\n el.addEventListener(\"paste\", (e) => {\n e.preventDefault();\n const text = e.clipboardData?.getData(\"text/plain\") ?? \"\";\n document.execCommand(\"insertText\", false, text);\n });\n\n el.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n el.blur();\n }\n });\n}\n\n/* ─── Rich-text mode (article bodies) ──────────────────────────────────────\n * A single editable region + a floating B/I/U · color · emoji · Done toolbar\n * (the Pitch Vault pattern, minus the iframe/postMessage — we run in the live\n * page's own document). On \"Done\" the region's innerHTML is converted back to\n * Markdown (the cms `richtext` contract: fields store Markdown, not HTML) and\n * saved. Only ONE region is active at a time. */\n\ntype RichMode = \"markdown\" | \"html\";\ninterface RichContext {\n el: HTMLElement;\n token: string;\n options: ResolvedOptions;\n originalHtml: string;\n mode: RichMode;\n}\nlet richCtx: RichContext | null = null;\nlet richToolbar: HTMLElement | null = null;\n\nfunction wireRichField(el: HTMLElement, token: string, options: ResolvedOptions, mode: RichMode): void {\n el.addEventListener(\"click\", (e) => {\n if (richCtx && richCtx.el === el) return; // already editing this region\n e.preventDefault();\n e.stopPropagation();\n activateRich(el, token, options, mode);\n });\n}\n\nfunction activateRich(el: HTMLElement, token: string, options: ResolvedOptions, mode: RichMode): void {\n deactivateRich(); // commit any previously-active region first\n richCtx = { el, token, options, originalHtml: el.innerHTML, mode };\n el.setAttribute(\"contenteditable\", \"true\");\n el.classList.add(\"cms-rich-editing\");\n el.focus();\n showRichToolbar();\n // Let the host page react (e.g. pause a rotating hero carousel while editing).\n document.dispatchEvent(new CustomEvent(\"cms-inline-edit:activate\", { detail: { el } }));\n}\n\nfunction deactivateRich(): void {\n if (!richCtx) return;\n const { el, token, options, originalHtml, mode } = richCtx;\n richCtx = null;\n el.removeAttribute(\"contenteditable\");\n el.classList.remove(\"cms-rich-editing\");\n hideRichToolbar();\n if (el.innerHTML !== originalHtml) {\n const value = mode === \"html\" ? el.innerHTML : htmlToMarkdown(el.innerHTML);\n void saveField(el, value, token, options);\n }\n document.dispatchEvent(new CustomEvent(\"cms-inline-edit:deactivate\", { detail: { el } }));\n}\n\nfunction showRichToolbar(): void {\n if (!richToolbar) richToolbar = buildRichToolbar();\n richToolbar.style.display = \"flex\";\n}\n\nfunction hideRichToolbar(): void {\n if (richToolbar) richToolbar.style.display = \"none\";\n hideEmojiPicker();\n}\n\nfunction toolbarButton(label: string, title: string, onDown: () => void): HTMLButtonElement {\n const b = document.createElement(\"button\");\n b.type = \"button\";\n b.innerHTML = label;\n b.title = title;\n b.style.cssText =\n \"background:none;border:1px solid #3a3f4a;color:#fff;min-width:30px;height:30px;\" +\n \"border-radius:6px;cursor:pointer;font-size:14px;padding:0 8px;line-height:1;\";\n b.addEventListener(\"mouseenter\", () => (b.style.background = \"#2a2f38\"));\n b.addEventListener(\"mouseleave\", () => (b.style.background = \"none\"));\n // mousedown + preventDefault so the contenteditable selection isn't lost.\n b.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n onDown();\n });\n return b;\n}\n\nfunction buildRichToolbar(): HTMLElement {\n const t = document.createElement(\"div\");\n t.setAttribute(\"data-cms-inline-edit-toolbar\", \"\");\n t.style.cssText =\n \"position:fixed;top:14px;left:50%;transform:translateX(-50%);\" +\n \"background:#1c2027;border:1px solid #3a3f4a;border-radius:10px;\" +\n \"padding:6px 10px;display:flex;gap:6px;align-items:center;\" +\n \"z-index:2147483647;box-shadow:0 8px 32px rgba(0,0,0,.5);\" +\n \"font-family:system-ui,sans-serif;\";\n\n t.appendChild(toolbarButton(\"<b>B</b>\", uiLabels.bold, () => document.execCommand(\"bold\")));\n t.appendChild(toolbarButton(\"<i>I</i>\", uiLabels.italic, () => document.execCommand(\"italic\")));\n t.appendChild(toolbarButton(\"<u>U</u>\", uiLabels.underline, () => document.execCommand(\"underline\")));\n\n const sep = () => {\n const s = document.createElement(\"div\");\n s.style.cssText = \"width:1px;height:20px;background:#3a3f4a;\";\n return s;\n };\n t.appendChild(sep());\n\n // Text color — execCommand foreColor applies to the current selection.\n const clrLabel = document.createElement(\"label\");\n clrLabel.style.cssText = \"display:flex;align-items:center;gap:5px;color:#9aa4b2;font-size:12px;cursor:pointer;\";\n clrLabel.textContent = uiLabels.color;\n const clr = document.createElement(\"input\");\n clr.type = \"color\";\n clr.style.cssText = \"width:28px;height:24px;border:1px solid #3a3f4a;border-radius:5px;cursor:pointer;padding:1px;background:none;\";\n clr.addEventListener(\"mousedown\", (e) => e.stopPropagation());\n clr.addEventListener(\"input\", () => document.execCommand(\"foreColor\", false, clr.value));\n clrLabel.prepend(clr);\n t.appendChild(clrLabel);\n\n t.appendChild(sep());\n\n const emojiBtn = toolbarButton(\"😀\", uiLabels.emoji, () => toggleEmojiPicker(emojiBtn));\n emojiBtn.style.fontSize = \"16px\";\n t.appendChild(emojiBtn);\n\n t.appendChild(sep());\n\n const done = document.createElement(\"button\");\n done.type = \"button\";\n done.textContent = uiLabels.done;\n done.style.cssText =\n \"background:#00b2ff;border:none;color:#04121c;padding:0 14px;height:30px;\" +\n \"border-radius:6px;cursor:pointer;font-size:12px;font-weight:700;\";\n done.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n deactivateRich();\n });\n t.appendChild(done);\n\n document.body.appendChild(t);\n return t;\n}\n\n/* ─── emoji picker (compact) ────────────────────────────────────────────── */\nconst EMOJIS =\n \"😀 😃 😄 😁 😆 😉 😊 😍 🤩 😎 🤔 🙌 👏 👍 👎 🙏 💪 🔥 ⚡ ✨ 🎉 ✅ ❌ 💯 ⭐ 🚀 💡 📈 📉 🧠 ❤️ 🧡 💛 💚 💙 💜 🇩🇰 🇪🇺\".split(\n \" \",\n );\nlet emojiPicker: HTMLElement | null = null;\nlet savedRange: Range | null = null;\n\nfunction toggleEmojiPicker(anchor: HTMLElement): void {\n if (!emojiPicker) emojiPicker = buildEmojiPicker();\n if (emojiPicker.style.display === \"block\") {\n hideEmojiPicker();\n return;\n }\n const sel = window.getSelection();\n if (sel && sel.rangeCount > 0) savedRange = sel.getRangeAt(0).cloneRange();\n const rect = anchor.getBoundingClientRect();\n emojiPicker.style.top = `${rect.bottom + 6}px`;\n emojiPicker.style.left = `${Math.min(rect.left, window.innerWidth - 240)}px`;\n emojiPicker.style.display = \"block\";\n}\n\nfunction hideEmojiPicker(): void {\n if (emojiPicker) emojiPicker.style.display = \"none\";\n}\n\nfunction buildEmojiPicker(): HTMLElement {\n const p = document.createElement(\"div\");\n p.setAttribute(\"data-cms-inline-edit-toolbar\", \"\");\n p.style.cssText =\n \"position:fixed;z-index:2147483646;background:#1c2027;border:1px solid #3a3f4a;\" +\n \"border-radius:10px;padding:8px;width:230px;max-height:200px;overflow-y:auto;\" +\n \"display:none;box-shadow:0 8px 32px rgba(0,0,0,.5);\";\n const grid = document.createElement(\"div\");\n grid.style.cssText = \"display:flex;flex-wrap:wrap;gap:2px;\";\n EMOJIS.forEach((em) => {\n const b = document.createElement(\"button\");\n b.type = \"button\";\n b.textContent = em;\n b.style.cssText =\n \"background:none;border:none;cursor:pointer;border-radius:5px;width:32px;height:32px;font-size:18px;\";\n b.addEventListener(\"mouseenter\", () => (b.style.background = \"#2a2f38\"));\n b.addEventListener(\"mouseleave\", () => (b.style.background = \"none\"));\n b.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n insertEmoji(em);\n });\n grid.appendChild(b);\n });\n p.appendChild(grid);\n document.body.appendChild(p);\n return p;\n}\n\nfunction insertEmoji(emoji: string): void {\n if (!richCtx) return;\n richCtx.el.focus();\n const sel = window.getSelection();\n if (savedRange && sel) {\n sel.removeAllRanges();\n sel.addRange(savedRange);\n }\n document.execCommand(\"insertText\", false, emoji);\n savedRange = null;\n hideEmojiPicker();\n}\n\n// Click outside the active region (and outside the toolbar) commits the edit.\nif (typeof document !== \"undefined\") {\n document.addEventListener(\n \"mousedown\",\n (e) => {\n if (!richCtx) return;\n const target = e.target as HTMLElement | null;\n if (!target) return;\n if (target.closest(\"[data-cms-inline-edit-toolbar]\")) return;\n if (richCtx.el.contains(target)) return;\n deactivateRich();\n },\n true,\n );\n}\n\n/**\n * Sets a value at a dot-path into a plain-data tree, where a numeric segment\n * indexes into an array (e.g. \"slides.2.eyebrow\" — flagship-style nested\n * content; a flat \"heroEyebrow\" is still just a 1-segment path). Bails\n * silently (no throw) if the path doesn't resolve — a stale/malformed path\n * must never crash the save, just fail to apply.\n */\nfunction setDeepField(data: Record<string, unknown>, path: string, value: string): void {\n const parts = path.split(\".\");\n let obj: any = data;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i] ?? \"\";\n const key: string | number = /^\\d+$/.test(part) ? Number(part) : part;\n if (obj == null || typeof obj !== \"object\" || obj[key] === undefined) return;\n obj = obj[key];\n }\n const last = parts[parts.length - 1] ?? \"\";\n const lastKey: string | number = /^\\d+$/.test(last) ? Number(last) : last;\n if (obj == null || typeof obj !== \"object\") return;\n obj[lastKey] = value;\n}\n\n/**\n * Converts an edited contenteditable region's innerHTML back to Markdown so a\n * rich article body round-trips through the cms `richtext` contract (fields\n * store Markdown, `marked` renders it — NOT HTML). Handles the structures a\n * marked-rendered body + the toolbar produce: headings, paragraphs, emphasis,\n * links, lists, blockquote, code. Formatting with no clean Markdown equivalent\n * (underline, coloured spans) is passed through as inline HTML — `marked`\n * renders inline HTML, so it survives without corrupting the source. Tables and\n * anything unrecognised are passed through as their outerHTML for the same\n * reason: never drop content just because it doesn't map to a Markdown token.\n */\nfunction htmlToMarkdown(html: string): string {\n const container = document.createElement(\"div\");\n container.innerHTML = html;\n return serializeBlockChildren(container).replace(/\\n{3,}/g, \"\\n\\n\").trim() + \"\\n\";\n}\n\nfunction serializeBlockChildren(parent: Node): string {\n const blocks: string[] = [];\n parent.childNodes.forEach((node) => {\n const s = serializeBlock(node).trim();\n if (s) blocks.push(s);\n });\n return blocks.join(\"\\n\\n\");\n}\n\nfunction serializeBlock(node: Node): string {\n if (node.nodeType === Node.TEXT_NODE) return (node.textContent ?? \"\").trim();\n if (node.nodeType !== Node.ELEMENT_NODE) return \"\";\n const el = node as HTMLElement;\n const tag = el.tagName.toLowerCase();\n switch (tag) {\n case \"h1\": return \"# \" + serializeInline(el);\n case \"h2\": return \"## \" + serializeInline(el);\n case \"h3\": return \"### \" + serializeInline(el);\n case \"h4\": return \"#### \" + serializeInline(el);\n case \"h5\": return \"##### \" + serializeInline(el);\n case \"h6\": return \"###### \" + serializeInline(el);\n case \"p\":\n case \"div\":\n return serializeInline(el);\n case \"ul\":\n return serializeList(el, false);\n case \"ol\":\n return serializeList(el, true);\n case \"blockquote\":\n // Serialize the quote's OWN block children (it usually wraps <p>s), then\n // prefix each line with \"> \" — treating it as inline dropped the <p> and\n // left a stray leading space.\n return serializeBlockChildren(el)\n .split(\"\\n\")\n .map((l) => (l ? \"> \" + l : \">\"))\n .join(\"\\n\");\n case \"pre\":\n return \"```\\n\" + (el.textContent ?? \"\").replace(/\\n+$/, \"\") + \"\\n```\";\n case \"hr\":\n return \"---\";\n case \"br\":\n return \"\";\n case \"table\":\n // Rebuild the Markdown pipe table (NOT raw-HTML passthrough) so the\n // consumer's marked renderer re-applies its own table treatment (e.g.\n // broberg's mobile scroll-wrapper) instead of getting a bare <table>.\n return serializeTable(el);\n default:\n return serializeInline(el);\n }\n}\n\nfunction serializeTable(table: HTMLElement): string {\n const cellText = (c: Element) => serializeInline(c).trim().replace(/\\|/g, \"\\\\|\");\n const rows: string[] = [];\n const headCells = Array.from(table.querySelectorAll(\"thead tr\")).flatMap((tr) =>\n Array.from(tr.children).map(cellText),\n );\n if (headCells.length) {\n rows.push(\"| \" + headCells.join(\" | \") + \" |\");\n rows.push(\"| \" + headCells.map(() => \"---\").join(\" | \") + \" |\");\n }\n const bodyRows = table.querySelector(\"tbody\")\n ? Array.from(table.querySelectorAll(\"tbody tr\"))\n : Array.from(table.querySelectorAll(\"tr\")).filter((tr) => !tr.closest(\"thead\"));\n bodyRows.forEach((tr) => {\n rows.push(\"| \" + Array.from(tr.children).map(cellText).join(\" | \") + \" |\");\n });\n return rows.join(\"\\n\");\n}\n\nfunction serializeList(listEl: HTMLElement, ordered: boolean): string {\n const items: string[] = [];\n let n = 1;\n listEl.childNodes.forEach((child) => {\n if (child.nodeType === Node.ELEMENT_NODE && (child as HTMLElement).tagName.toLowerCase() === \"li\") {\n const marker = ordered ? `${n++}. ` : \"- \";\n items.push(marker + serializeInline(child as HTMLElement).trim());\n }\n });\n return items.join(\"\\n\");\n}\n\nfunction serializeInline(node: Node): string {\n let out = \"\";\n node.childNodes.forEach((child) => {\n if (child.nodeType === Node.TEXT_NODE) {\n out += (child.textContent ?? \"\").replace(/\\s+/g, \" \");\n return;\n }\n if (child.nodeType !== Node.ELEMENT_NODE) return;\n const el = child as HTMLElement;\n const tag = el.tagName.toLowerCase();\n const inner = serializeInline(el);\n switch (tag) {\n case \"strong\":\n case \"b\":\n out += inner.trim() ? `**${inner.trim()}**` : \"\";\n break;\n case \"em\":\n case \"i\":\n out += inner.trim() ? `*${inner.trim()}*` : \"\";\n break;\n case \"code\":\n out += \"`\" + inner + \"`\";\n break;\n case \"br\":\n out += \" \\n\";\n break;\n case \"a\": {\n const href = el.getAttribute(\"href\") || \"\";\n out += href ? `[${inner}](${href})` : inner;\n break;\n }\n case \"img\": {\n // Content images embedded in the body () — dropping them\n // would silently delete images from an article on save. Void element,\n // no children, so this must be handled explicitly.\n const src = el.getAttribute(\"src\") || \"\";\n const alt = el.getAttribute(\"alt\") || \"\";\n if (src) out += ``;\n break;\n }\n case \"u\":\n out += `<u>${inner}</u>`; // no Markdown for underline — pass through\n break;\n case \"span\":\n case \"font\": {\n const color = el.style.color || el.getAttribute(\"color\") || \"\";\n out += color ? `<span style=\"color:${color}\">${inner}</span>` : inner;\n break;\n }\n default:\n out += inner;\n }\n });\n return out;\n}\n\nasync function saveField(el: HTMLElement, value: string, token: string, options: ResolvedOptions): Promise<void> {\n const collection = el.dataset.cmsCollection;\n const slug = el.dataset.cmsSlug;\n const field = el.dataset.cmsField;\n if (!collection || !slug || !field) return;\n\n showPill(el, \"saving\");\n try {\n const getRes = await fetch(\n `${options.cmsBaseUrl}/api/cms/${collection}/${slug}?site=${options.siteId}`,\n { headers: { Authorization: `Bearer ${token}` } },\n );\n if (!getRes.ok) throw new Error(`GET failed: ${getRes.status}`);\n const doc = (await getRes.json()) as { data?: Record<string, unknown> };\n // Deep-clone so mutating a nested array/object (dot-path saves) never\n // aliases the fetched doc — same safety whether field is flat or nested.\n const mergedData = JSON.parse(JSON.stringify(doc.data ?? {})) as Record<string, unknown>;\n if (field.includes(\".\")) {\n setDeepField(mergedData, field, value);\n } else {\n mergedData[field] = value;\n }\n\n const patchRes = await fetch(\n `${options.cmsBaseUrl}/api/cms/${collection}/${slug}?site=${options.siteId}`,\n {\n method: \"PATCH\",\n headers: { Authorization: `Bearer ${token}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ data: mergedData }),\n },\n );\n if (!patchRes.ok) throw new Error(`PATCH failed: ${patchRes.status}`);\n showPill(el, \"saved\");\n } catch {\n showPill(el, \"error\");\n }\n}\n\nconst pills = new WeakMap<HTMLElement, HTMLElement>();\n\nfunction showPill(el: HTMLElement, state: \"saving\" | \"saved\" | \"error\"): void {\n let pill = pills.get(el);\n if (!pill) {\n pill = document.createElement(\"span\");\n pill.setAttribute(\"data-cms-inline-edit-pill\", \"\");\n document.body.appendChild(pill);\n pills.set(el, pill);\n }\n const rect = el.getBoundingClientRect();\n pill.style.cssText =\n `position:fixed;top:${rect.top - 26}px;left:${rect.left}px;font:600 11px system-ui,sans-serif;` +\n `padding:3px 9px;border-radius:5px;z-index:2147483647;pointer-events:none;` +\n `box-shadow:0 2px 8px rgba(0,0,0,.25);`;\n\n if (state === \"saving\") {\n pill.textContent = uiLabels.saving;\n pill.style.background = \"#1c2027\";\n pill.style.color = \"#fff\";\n } else if (state === \"saved\") {\n pill.textContent = uiLabels.saved;\n pill.style.background = \"#16a34a\";\n pill.style.color = \"#fff\";\n setTimeout(() => {\n pill?.remove();\n pills.delete(el);\n }, 1500);\n } else {\n pill.textContent = uiLabels.error;\n pill.style.background = \"#dc2626\";\n pill.style.color = \"#fff\";\n }\n}\n\nfunction showActiveBadge(options: ResolvedOptions): void {\n // The connected badge IS the exit-edit action: [icon] \"Afslut redigering\".\n // No \"Redigerer som {name}\" — the whole pill is one click to leave edit mode.\n const badge = document.createElement(\"button\");\n badge.type = \"button\";\n badge.setAttribute(\"data-cms-inline-edit-badge\", \"\");\n badge.style.cssText =\n \"position:fixed;bottom:16px;left:16px;display:inline-flex;align-items:center;gap:7px;\" +\n \"background:#1c2027;color:#fff;font:600 12px system-ui,sans-serif;padding:8px 14px;\" +\n \"border-radius:999px;z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);\" +\n \"border:none;cursor:pointer;opacity:.9;transition:opacity .15s;\";\n const text = document.createElement(\"span\");\n text.textContent = options.disconnectLabel;\n badge.append(makeIcon(), text);\n badge.addEventListener(\"mouseenter\", () => (badge.style.opacity = \"1\"));\n badge.addEventListener(\"mouseleave\", () => (badge.style.opacity = \".9\"));\n badge.addEventListener(\"click\", () => {\n disconnect(options);\n window.location.reload();\n });\n document.body.appendChild(badge);\n}\n\n/** Reads JWT claims for DISPLAY ONLY — never used for authorization decisions. */\nfunction decodeJwtPayload(token: string): Record<string, unknown> | null {\n try {\n const payload = token.split(\".\")[1];\n if (!payload) return null;\n return JSON.parse(atob(payload.replace(/-/g, \"+\").replace(/_/g, \"/\")));\n } catch {\n return null;\n }\n}\n\nfunction injectStyles(): void {\n const style = document.createElement(\"style\");\n style.textContent = `\n [data-cms-field] { outline: 1px dashed transparent; outline-offset: 2px; cursor: text; transition: outline-color .15s; }\n [data-cms-field]:hover { outline-color: rgba(0,178,255,.5); }\n [data-cms-field][contenteditable=\"true\"] { outline: 2px solid #00b2ff; outline-offset: 2px; }\n [data-cms-field][data-cms-richtext=\"true\"] { cursor: text; }\n .cms-rich-editing { outline: 2px solid #00b2ff !important; outline-offset: 6px; border-radius: 4px; }\n `;\n document.head.appendChild(style);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/field-slice.ts"],"sourcesContent":["/**\n * Browser entry — zero dependencies. Site-wide: activated once per browser\n * via a \"connect\" link (see docs/features/F157-inline-editing.md), then\n * every page load on the site automatically offers click-to-edit on any\n * element carrying data-cms-collection/data-cms-slug/data-cms-field\n * (F129's attribute convention) — no per-document step.\n */\nimport { applyFieldSlice } from \"./field-slice\";\nexport { applyFieldSlice } from \"./field-slice\";\n\n/**\n * Labels for the in-editor UI — the rich-text toolbar (bold/italic/underline\n * tooltips, the visible \"colour\" + \"done\" buttons, the emoji tooltip) and the\n * save-status pill. Every field is optional; unset fields keep the Danish\n * default. Pass an English (or any-locale) set on a non-Danish site so the\n * toolbar and status text match the page language.\n */\nexport interface InlineEditLabels {\n bold?: string;\n italic?: string;\n underline?: string;\n orderedList?: string;\n unorderedList?: string;\n color?: string;\n emoji?: string;\n done?: string;\n saving?: string;\n saved?: string;\n error?: string;\n}\n\nexport interface InlineEditOptions {\n /** Base URL of the CMS API, e.g. \"https://webhouse.app\". */\n cmsBaseUrl: string;\n /** Site id passed as ?site= on every CMS API call, e.g. \"broberg-ai\". */\n siteId: string;\n /** URL query param carrying a freshly-minted token from the connect redirect. Default \"cms_edit\". */\n tokenParam?: string;\n /** localStorage key the token is persisted under — survives across sessions/tabs. Default \"wh-inline-edit-token\". */\n storageKey?: string;\n /** Text for the \"not connected yet\" prompt (a square-pen icon is prepended). Default \"Log ind for at redigere\". */\n connectLabel?: string;\n /** Text for the \"connected + editing\" badge — clicking it LEAVES edit mode but keeps the login (icon prepended). Default \"Afbryd\". */\n disconnectLabel?: string;\n /** Text for the explicit \"log out\" action on the idle pill — clicking it clears the token (back to the logged-out state). Default \"Log ud\". */\n logoutLabel?: string;\n /** Show an on-site \"connect\" pill to LOGGED-OUT visitors (click → connect flow).\n * Default FALSE — customer-safe: a visitor without a token sees NOTHING. Only\n * enable for a site with no real customers (e.g. an internal/first-party site)\n * where a self-contained on-site login entry is wanted. The safe entry for a\n * customer site is always the \"Redigér live\" button in webhouse.app CMS-admin. */\n connectPrompt?: boolean;\n /** Localised labels for the toolbar + save-status pill. Defaults are Danish. */\n labels?: InlineEditLabels;\n}\n\ninterface ResolvedOptions extends Required<Omit<InlineEditOptions, \"labels\">> {\n labels: Required<InlineEditLabels>;\n}\n\nconst DEFAULT_LABELS: Required<InlineEditLabels> = {\n bold: \"Fed\",\n italic: \"Kursiv\",\n underline: \"Understreget\",\n orderedList: \"Nummereret liste\",\n unorderedList: \"Punktliste\",\n color: \"Farve\",\n emoji: \"Indsæt emoji\",\n done: \"Færdig\",\n saving: \"Gemmer…\",\n saved: \"Gemt ✓\",\n error: \"Fejl — prøv igen\",\n};\n\n// Set once from resolved options in initInlineEdit(); read by the (singleton)\n// toolbar and the save-status pill so their signatures stay unchanged. One edit\n// session per page → a single module-level value is safe.\nlet uiLabels: Required<InlineEditLabels> = DEFAULT_LABELS;\n\nfunction resolveOptions(options: InlineEditOptions): ResolvedOptions {\n return {\n tokenParam: \"cms_edit\",\n storageKey: \"wh-inline-edit-token\",\n // No emoji in defaults — the square-pen SVG is the icon; some sites ban emoji.\n connectLabel: \"Log ind for at redigere\",\n disconnectLabel: \"Afbryd\",\n logoutLabel: \"Log ud\",\n connectPrompt: false,\n ...options,\n labels: { ...DEFAULT_LABELS, ...(options.labels ?? {}) },\n };\n}\n\nexport async function initInlineEdit(options: InlineEditOptions): Promise<void> {\n if (typeof window === \"undefined\") return;\n const resolved = resolveOptions(options);\n uiLabels = resolved.labels;\n\n // Capture BEFORE checkEnabled so we know whether the token JUST arrived from\n // the connect redirect (→ drop straight into edit mode) vs. a normal load\n // with a stored token (→ idle \"Rediger\" pill, don't force edit mode).\n const freshFromUrl = captureTokenFromUrl(resolved);\n\n const enabled = await checkEnabled(resolved);\n if (!enabled) return;\n\n const token = getConnectedToken(options);\n if (!token) {\n // Logged-out visitor / customer. DEFAULT: show NOTHING (customer-safe) —\n // the consumer can now call initInlineEdit() unconditionally without leaking\n // an edit affordance. Only sites that opt in (connectPrompt: true) get the\n // on-site connect pill + its popup token receiver.\n if (resolved.connectPrompt) {\n showConnectPrompt(resolved);\n // F158 — the connect flow opens in a popup; it posts the freshly-minted\n // token back to this tab. Arm the (origin-validated) receiver.\n listenForTokenMessage(resolved);\n }\n return;\n }\n\n // Connected editor. Wire fields once, then pick the entry state: arrived from\n // connect → edit now; normal load with a stored token → idle \"Rediger\" pill\n // (one click enters edit mode; the token persists so \"Rediger\" stays visible\n // on every page while logged in — no trip back to admin).\n setupFields(token, resolved);\n if (freshFromUrl) enterEditMode();\n else showIdlePill();\n}\n\n/**\n * F158 — receive the edit token from the connect popup via postMessage.\n * Hard origin check (message MUST come from cmsBaseUrl) + type + site + expiry\n * guards before the token is trusted. On success: persist, swap the connect\n * prompt for edit mode — no page reload.\n */\nfunction listenForTokenMessage(options: ResolvedOptions): void {\n let cmsOrigin: string;\n try {\n cmsOrigin = new URL(options.cmsBaseUrl).origin;\n } catch {\n return;\n }\n window.addEventListener(\"message\", (event: MessageEvent) => {\n if (event.origin !== cmsOrigin) return;\n const data = event.data as { type?: string; token?: string; site?: string } | null;\n if (!data || data.type !== \"wh-inline-edit-token\" || typeof data.token !== \"string\") return;\n if (data.site && data.site !== options.siteId) return;\n if (isExpired(data.token)) return;\n if (document.querySelector(\"[data-cms-inline-edit-badge]\")) return; // already active\n localStorage.setItem(options.storageKey, data.token);\n document.querySelector(\"[data-cms-inline-edit-connect]\")?.remove();\n // Fresh connect via popup → wire fields + drop straight into edit mode.\n setupFields(data.token, options);\n enterEditMode();\n });\n}\n\n/**\n * A valid (non-expired) token already connected in this browser for this\n * site, or null. Also captures a fresh `?cms_edit=` token from the URL\n * first, so a page can call this directly right after the connect redirect\n * without going through initInlineEdit(). Exposed so a site's own /admin\n * page (or any other tool built on the same connected session) can reuse\n * the same storage/expiry logic instead of re-implementing it.\n */\nexport function getConnectedToken(options: InlineEditOptions): string | null {\n if (typeof window === \"undefined\") return null;\n const resolved = resolveOptions(options);\n captureTokenFromUrl(resolved);\n const token = localStorage.getItem(resolved.storageKey);\n if (!token) return null;\n if (isExpired(token)) {\n localStorage.removeItem(resolved.storageKey);\n return null;\n }\n return token;\n}\n\n/** The URL that mints a fresh 30-day site-scoped token and redirects back to `returnUrl`. */\nexport function buildConnectUrl(options: InlineEditOptions, returnUrl: string): string {\n const resolved = resolveOptions(options);\n return (\n `${resolved.cmsBaseUrl}/admin/inline-edit/connect?site=${encodeURIComponent(resolved.siteId)}` +\n `&return=${encodeURIComponent(returnUrl)}`\n );\n}\n\n/** Clears the connected token in this browser (e.g. a \"log out\" action in a site's own /admin panel). */\nexport function disconnect(options: InlineEditOptions): void {\n if (typeof window === \"undefined\") return;\n localStorage.removeItem(resolveOptions(options).storageKey);\n}\n\n/** Captures a token minted by /admin/inline-edit/connect, then strips it from\n * the URL. Returns true when a FRESH token was just captured from the URL\n * (i.e. this is the connect-redirect landing) so the caller can auto-enter\n * edit mode; false on a normal load where the token comes from localStorage. */\nfunction captureTokenFromUrl(options: ResolvedOptions): boolean {\n const url = new URL(window.location.href);\n const urlToken = url.searchParams.get(options.tokenParam);\n if (!urlToken) return false;\n localStorage.setItem(options.storageKey, urlToken);\n url.searchParams.delete(options.tokenParam);\n window.history.replaceState({}, \"\", url.toString());\n return true;\n}\n\nasync function checkEnabled(options: ResolvedOptions): Promise<boolean> {\n try {\n const res = await fetch(`${options.cmsBaseUrl}/api/inline-edit/status?site=${options.siteId}`);\n if (!res.ok) return false;\n const body = (await res.json()) as { enabled?: boolean };\n return body.enabled === true;\n } catch {\n return false;\n }\n}\n\nfunction isExpired(token: string): boolean {\n const claims = decodeJwtPayload(token);\n const exp = typeof claims?.exp === \"number\" ? claims.exp : 0;\n return exp <= Date.now() / 1000;\n}\n\n// Lucide \"square-pen\" (https://lucide.dev/icons/square-pen), MIT. Inline SVG so\n// the pill needs no icon-font/runtime dep and inherits `currentColor`. Emoji are\n// deliberately avoided — some consumer sites ban them outright.\nconst SQUARE_PEN_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<path d=\"M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\"/>' +\n '<path d=\"M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z\"/>' +\n \"</svg>\";\n\n// Toolbar list-button faces (lucide \"list\" / \"list-ordered\"), inline so no\n// icon-font/runtime dep; stroke inherits the button's white `currentColor`.\nconst UL_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<line x1=\"8\" x2=\"21\" y1=\"6\" y2=\"6\"/><line x1=\"8\" x2=\"21\" y1=\"12\" y2=\"12\"/>' +\n '<line x1=\"8\" x2=\"21\" y1=\"18\" y2=\"18\"/><line x1=\"3\" x2=\"3.01\" y1=\"6\" y2=\"6\"/>' +\n '<line x1=\"3\" x2=\"3.01\" y1=\"12\" y2=\"12\"/><line x1=\"3\" x2=\"3.01\" y1=\"18\" y2=\"18\"/>' +\n \"</svg>\";\n\nconst OL_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<line x1=\"10\" x2=\"21\" y1=\"6\" y2=\"6\"/><line x1=\"10\" x2=\"21\" y1=\"12\" y2=\"12\"/>' +\n '<line x1=\"10\" x2=\"21\" y1=\"18\" y2=\"18\"/><path d=\"M4 6h1v4\"/><path d=\"M4 10h2\"/>' +\n '<path d=\"M6 18H4c0-1 2-2 2-3s-1-1.5-2-1\"/>' +\n \"</svg>\";\n\nfunction makeIcon(): HTMLSpanElement {\n const icon = document.createElement(\"span\");\n icon.style.cssText = \"display:inline-flex;line-height:0;flex:0 0 auto\";\n icon.innerHTML = SQUARE_PEN_SVG;\n return icon;\n}\n\nfunction showConnectPrompt(options: ResolvedOptions): void {\n const connectUrl = buildConnectUrl(options, window.location.href);\n\n const link = document.createElement(\"a\");\n link.href = connectUrl;\n link.setAttribute(\"data-cms-inline-edit-connect\", \"\");\n link.style.cssText =\n \"position:fixed;bottom:16px;left:16px;background:#1c2027;color:#fff;\" +\n \"font:600 12px system-ui,sans-serif;padding:8px 14px;border-radius:999px;\" +\n \"text-decoration:none;z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);\" +\n \"display:inline-flex;align-items:center;gap:7px;\";\n const text = document.createElement(\"span\");\n text.textContent = options.connectLabel;\n link.append(makeIcon(), text);\n // F158 — open the connect flow in a popup so the CMS confirmation screen can\n // say \"close this window\". A user-gesture window.open is not popup-blocked;\n // if it is blocked anyway, fall back to the same-tab redirect (F157 path).\n link.addEventListener(\"click\", (e) => {\n e.preventDefault();\n const popup = window.open(\n connectUrl,\n \"wh-inline-edit-connect\",\n \"width=460,height=640,menubar=no,toolbar=no,location=yes,status=no\",\n );\n if (!popup) window.location.href = connectUrl;\n });\n document.body.appendChild(link);\n}\n\n// ─── Connected-editor state machine ────────────────────────────────────────\n// A connected editor (valid token) is ALWAYS in one of two visible states:\n// idle → a \"Rediger\" pill (options.connectLabel); one click enters editing.\n// editing → fields are contenteditable + an \"Afslut redigering\" pill.\n// Leaving edit mode returns to idle and KEEPS the token — the editor stays\n// logged in and can re-enter with one click, on every page, for the token's\n// whole life. Only an explicit \"Log ud\" clears the token (→ logged-out state:\n// no pill at all, same as a customer). Fields are wired ONCE; their click\n// handlers no-op while editingActive is false, so idle is truly inert.\nlet editingActive = false;\nlet fieldsWired = false;\nlet stateOptions: ResolvedOptions | null = null;\n\nfunction setupFields(token: string, options: ResolvedOptions): void {\n stateOptions = options;\n if (fieldsWired) return;\n injectStyles();\n document.querySelectorAll<HTMLElement>(\"[data-cms-field]\").forEach((el) => wireField(el, token, options));\n fieldsWired = true;\n}\n\nfunction enterEditMode(): void {\n if (!stateOptions || editingActive) return;\n editingActive = true;\n removeIdlePill();\n showActiveBadge(stateOptions);\n}\n\nfunction exitEditMode(): void {\n if (!stateOptions || !editingActive) return;\n deactivateRich(); // commit any active rich region first\n editingActive = false;\n document.querySelector(\"[data-cms-inline-edit-badge]\")?.remove();\n showIdlePill();\n}\n\nfunction removeIdlePill(): void {\n document.querySelector(\"[data-cms-inline-edit-idle]\")?.remove();\n}\n\nfunction showIdlePill(): void {\n if (!stateOptions) return;\n const options = stateOptions;\n removeIdlePill();\n const wrap = document.createElement(\"div\");\n wrap.setAttribute(\"data-cms-inline-edit-idle\", \"\");\n wrap.style.cssText =\n \"position:fixed;bottom:16px;left:16px;display:inline-flex;align-items:stretch;\" +\n \"z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);border-radius:999px;overflow:hidden;\";\n // Main action: \"Rediger\" → enter edit mode (no server round-trip; the token\n // already exists locally).\n const edit = document.createElement(\"button\");\n edit.type = \"button\";\n edit.setAttribute(\"data-cms-inline-edit-enter\", \"\");\n edit.style.cssText =\n \"display:inline-flex;align-items:center;gap:7px;background:#1c2027;color:#fff;\" +\n \"font:600 12px system-ui,sans-serif;padding:8px 14px;border:none;cursor:pointer;\";\n const editText = document.createElement(\"span\");\n editText.textContent = options.connectLabel;\n edit.append(makeIcon(), editText);\n edit.addEventListener(\"click\", enterEditMode);\n // Secondary: \"Log ud\" → clear the token (back to the logged-out state).\n const out = document.createElement(\"button\");\n out.type = \"button\";\n out.setAttribute(\"data-cms-inline-edit-logout\", \"\");\n out.textContent = options.logoutLabel;\n out.style.cssText =\n \"background:#141821;color:#9aa4b2;font:600 11px system-ui,sans-serif;padding:8px 12px;\" +\n \"border:none;border-left:1px solid rgba(255,255,255,.12);cursor:pointer;\";\n out.addEventListener(\"click\", () => {\n disconnect(options);\n removeIdlePill();\n });\n wrap.append(edit, out);\n document.body.appendChild(wrap);\n}\n\nfunction wireField(el: HTMLElement, token: string, options: ResolvedOptions): void {\n // Rich fields get the floating formatting toolbar. Two save modes:\n // - data-cms-richtext=\"true\" → save as Markdown (article bodies; cms\n // `richtext` contract renders Markdown via marked).\n // - data-cms-html=\"true\" → save innerHTML VERBATIM (intentional-HTML\n // fields: headings/bios/hero with a branded <em class=\"o\"> accent that\n // has no Markdown equivalent — converting would strip it).\n // Everything else stays a plain single-line contenteditable saving textContent.\n if (el.dataset.cmsRichtext === \"true\") {\n wireRichField(el, token, options, \"markdown\");\n return;\n }\n if (el.dataset.cmsHtml === \"true\") {\n wireRichField(el, token, options, \"html\");\n return;\n }\n\n el.addEventListener(\"click\", (e) => {\n if (!editingActive) return; // idle: the field behaves normally, not editable\n if (el.getAttribute(\"contenteditable\") === \"true\") return;\n // Many editable fields (card titles/blurbs) sit inside a clickable <a>/\n // <button> ancestor (the card itself) — without this, \"click to edit\"\n // would immediately navigate away instead of focusing the field.\n e.preventDefault();\n e.stopPropagation();\n el.dataset.cmsOriginalValue = el.textContent ?? \"\";\n el.setAttribute(\"contenteditable\", \"true\");\n el.focus();\n });\n\n el.addEventListener(\"blur\", () => {\n el.removeAttribute(\"contenteditable\");\n const original = el.dataset.cmsOriginalValue ?? \"\";\n const current = el.textContent ?? \"\";\n if (current.trim() === original.trim()) return;\n void saveField(el, current.trim(), token, options);\n });\n\n el.addEventListener(\"paste\", (e) => {\n e.preventDefault();\n const text = e.clipboardData?.getData(\"text/plain\") ?? \"\";\n document.execCommand(\"insertText\", false, text);\n });\n\n el.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n el.blur();\n }\n });\n}\n\n/* ─── Rich-text mode (article bodies) ──────────────────────────────────────\n * A single editable region + a floating B/I/U · color · emoji · Done toolbar\n * (the Pitch Vault pattern, minus the iframe/postMessage — we run in the live\n * page's own document). On \"Done\" the region's innerHTML is converted back to\n * Markdown (the cms `richtext` contract: fields store Markdown, not HTML) and\n * saved. Only ONE region is active at a time. */\n\ntype RichMode = \"markdown\" | \"html\";\ninterface RichContext {\n el: HTMLElement;\n token: string;\n options: ResolvedOptions;\n originalHtml: string;\n mode: RichMode;\n}\nlet richCtx: RichContext | null = null;\nlet richToolbar: HTMLElement | null = null;\n\nfunction wireRichField(el: HTMLElement, token: string, options: ResolvedOptions, mode: RichMode): void {\n el.addEventListener(\"click\", (e) => {\n if (!editingActive) return; // idle: the field behaves normally, not editable\n if (richCtx && richCtx.el === el) return; // already editing this region\n e.preventDefault();\n e.stopPropagation();\n activateRich(el, token, options, mode);\n });\n}\n\nfunction activateRich(el: HTMLElement, token: string, options: ResolvedOptions, mode: RichMode): void {\n deactivateRich(); // commit any previously-active region first\n richCtx = { el, token, options, originalHtml: el.innerHTML, mode };\n el.setAttribute(\"contenteditable\", \"true\");\n el.classList.add(\"cms-rich-editing\");\n el.focus();\n showRichToolbar();\n // Let the host page react (e.g. pause a rotating hero carousel while editing).\n document.dispatchEvent(new CustomEvent(\"cms-inline-edit:activate\", { detail: { el } }));\n}\n\nfunction deactivateRich(): void {\n if (!richCtx) return;\n const { el, token, options, originalHtml, mode } = richCtx;\n richCtx = null;\n el.removeAttribute(\"contenteditable\");\n el.classList.remove(\"cms-rich-editing\");\n hideRichToolbar();\n if (el.innerHTML !== originalHtml) {\n const value = mode === \"html\" ? el.innerHTML : htmlToMarkdown(el.innerHTML);\n void saveField(el, value, token, options);\n }\n document.dispatchEvent(new CustomEvent(\"cms-inline-edit:deactivate\", { detail: { el } }));\n}\n\nfunction showRichToolbar(): void {\n if (!richToolbar) richToolbar = buildRichToolbar();\n richToolbar.style.display = \"flex\";\n}\n\nfunction hideRichToolbar(): void {\n if (richToolbar) richToolbar.style.display = \"none\";\n hideEmojiPicker();\n}\n\nfunction toolbarButton(label: string, title: string, onDown: () => void): HTMLButtonElement {\n const b = document.createElement(\"button\");\n b.type = \"button\";\n b.innerHTML = label;\n b.title = title;\n b.style.cssText =\n \"background:none;border:1px solid #3a3f4a;color:#fff;min-width:30px;height:30px;\" +\n \"border-radius:6px;cursor:pointer;font-size:14px;padding:0 8px;line-height:1;\";\n b.addEventListener(\"mouseenter\", () => (b.style.background = \"#2a2f38\"));\n b.addEventListener(\"mouseleave\", () => (b.style.background = \"none\"));\n // mousedown + preventDefault so the contenteditable selection isn't lost.\n b.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n onDown();\n });\n return b;\n}\n\nfunction buildRichToolbar(): HTMLElement {\n const t = document.createElement(\"div\");\n t.setAttribute(\"data-cms-inline-edit-toolbar\", \"\");\n t.style.cssText =\n \"position:fixed;top:14px;left:50%;transform:translateX(-50%);\" +\n \"background:#1c2027;border:1px solid #3a3f4a;border-radius:10px;\" +\n \"padding:6px 10px;display:flex;gap:6px;align-items:center;\" +\n \"z-index:2147483647;box-shadow:0 8px 32px rgba(0,0,0,.5);\" +\n \"font-family:system-ui,sans-serif;\";\n\n t.appendChild(toolbarButton(\"<b>B</b>\", uiLabels.bold, () => document.execCommand(\"bold\")));\n t.appendChild(toolbarButton(\"<i>I</i>\", uiLabels.italic, () => document.execCommand(\"italic\")));\n t.appendChild(toolbarButton(\"<u>U</u>\", uiLabels.underline, () => document.execCommand(\"underline\")));\n\n const sep = () => {\n const s = document.createElement(\"div\");\n s.style.cssText = \"width:1px;height:20px;background:#3a3f4a;\";\n return s;\n };\n t.appendChild(sep());\n\n // Bullet + numbered lists — native execCommand, and the Markdown serializer\n // already round-trips <ul>/<ol> to \"- \" / \"1. \" so no save-path change needed.\n t.appendChild(toolbarButton(UL_SVG, uiLabels.unorderedList, () => document.execCommand(\"insertUnorderedList\")));\n t.appendChild(toolbarButton(OL_SVG, uiLabels.orderedList, () => document.execCommand(\"insertOrderedList\")));\n\n t.appendChild(sep());\n\n // Text color — execCommand foreColor applies to the current selection.\n const clrLabel = document.createElement(\"label\");\n clrLabel.style.cssText = \"display:flex;align-items:center;gap:5px;color:#9aa4b2;font-size:12px;cursor:pointer;\";\n clrLabel.textContent = uiLabels.color;\n const clr = document.createElement(\"input\");\n clr.type = \"color\";\n clr.style.cssText = \"width:28px;height:24px;border:1px solid #3a3f4a;border-radius:5px;cursor:pointer;padding:1px;background:none;\";\n clr.addEventListener(\"mousedown\", (e) => e.stopPropagation());\n clr.addEventListener(\"input\", () => document.execCommand(\"foreColor\", false, clr.value));\n clrLabel.prepend(clr);\n t.appendChild(clrLabel);\n\n t.appendChild(sep());\n\n const emojiBtn = toolbarButton(\"😀\", uiLabels.emoji, () => toggleEmojiPicker(emojiBtn));\n emojiBtn.style.fontSize = \"16px\";\n t.appendChild(emojiBtn);\n\n t.appendChild(sep());\n\n const done = document.createElement(\"button\");\n done.type = \"button\";\n done.textContent = uiLabels.done;\n done.style.cssText =\n \"background:#00b2ff;border:none;color:#04121c;padding:0 14px;height:30px;\" +\n \"border-radius:6px;cursor:pointer;font-size:12px;font-weight:700;\";\n done.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n deactivateRich();\n });\n t.appendChild(done);\n\n document.body.appendChild(t);\n return t;\n}\n\n/* ─── emoji picker (compact) ────────────────────────────────────────────── */\nconst EMOJIS =\n \"😀 😃 😄 😁 😆 😉 😊 😍 🤩 😎 🤔 🙌 👏 👍 👎 🙏 💪 🔥 ⚡ ✨ 🎉 ✅ ❌ 💯 ⭐ 🚀 💡 📈 📉 🧠 ❤️ 🧡 💛 💚 💙 💜 🇩🇰 🇪🇺\".split(\n \" \",\n );\nlet emojiPicker: HTMLElement | null = null;\nlet savedRange: Range | null = null;\n\nfunction toggleEmojiPicker(anchor: HTMLElement): void {\n if (!emojiPicker) emojiPicker = buildEmojiPicker();\n if (emojiPicker.style.display === \"block\") {\n hideEmojiPicker();\n return;\n }\n const sel = window.getSelection();\n if (sel && sel.rangeCount > 0) savedRange = sel.getRangeAt(0).cloneRange();\n const rect = anchor.getBoundingClientRect();\n emojiPicker.style.top = `${rect.bottom + 6}px`;\n emojiPicker.style.left = `${Math.min(rect.left, window.innerWidth - 240)}px`;\n emojiPicker.style.display = \"block\";\n}\n\nfunction hideEmojiPicker(): void {\n if (emojiPicker) emojiPicker.style.display = \"none\";\n}\n\nfunction buildEmojiPicker(): HTMLElement {\n const p = document.createElement(\"div\");\n p.setAttribute(\"data-cms-inline-edit-toolbar\", \"\");\n p.style.cssText =\n \"position:fixed;z-index:2147483646;background:#1c2027;border:1px solid #3a3f4a;\" +\n \"border-radius:10px;padding:8px;width:230px;max-height:200px;overflow-y:auto;\" +\n \"display:none;box-shadow:0 8px 32px rgba(0,0,0,.5);\";\n const grid = document.createElement(\"div\");\n grid.style.cssText = \"display:flex;flex-wrap:wrap;gap:2px;\";\n EMOJIS.forEach((em) => {\n const b = document.createElement(\"button\");\n b.type = \"button\";\n b.textContent = em;\n b.style.cssText =\n \"background:none;border:none;cursor:pointer;border-radius:5px;width:32px;height:32px;font-size:18px;\";\n b.addEventListener(\"mouseenter\", () => (b.style.background = \"#2a2f38\"));\n b.addEventListener(\"mouseleave\", () => (b.style.background = \"none\"));\n b.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n insertEmoji(em);\n });\n grid.appendChild(b);\n });\n p.appendChild(grid);\n document.body.appendChild(p);\n return p;\n}\n\nfunction insertEmoji(emoji: string): void {\n if (!richCtx) return;\n richCtx.el.focus();\n const sel = window.getSelection();\n if (savedRange && sel) {\n sel.removeAllRanges();\n sel.addRange(savedRange);\n }\n document.execCommand(\"insertText\", false, emoji);\n savedRange = null;\n hideEmojiPicker();\n}\n\n// Click outside the active region (and outside the toolbar) commits the edit.\nif (typeof document !== \"undefined\") {\n document.addEventListener(\n \"mousedown\",\n (e) => {\n if (!richCtx) return;\n const target = e.target as HTMLElement | null;\n if (!target) return;\n if (target.closest(\"[data-cms-inline-edit-toolbar]\")) return;\n if (richCtx.el.contains(target)) return;\n deactivateRich();\n },\n true,\n );\n}\n\n/**\n * Sets a value at a dot-path into a plain-data tree, where a numeric segment\n * indexes into an array (e.g. \"slides.2.eyebrow\" — flagship-style nested\n * content; a flat \"heroEyebrow\" is still just a 1-segment path). Bails\n * silently (no throw) if the path doesn't resolve — a stale/malformed path\n * must never crash the save, just fail to apply.\n */\nfunction setDeepField(data: Record<string, unknown>, path: string, value: string): void {\n const parts = path.split(\".\");\n let obj: any = data;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i] ?? \"\";\n const key: string | number = /^\\d+$/.test(part) ? Number(part) : part;\n if (obj == null || typeof obj !== \"object\" || obj[key] === undefined) return;\n obj = obj[key];\n }\n const last = parts[parts.length - 1] ?? \"\";\n const lastKey: string | number = /^\\d+$/.test(last) ? Number(last) : last;\n if (obj == null || typeof obj !== \"object\") return;\n obj[lastKey] = value;\n}\n\n/**\n * Converts an edited contenteditable region's innerHTML back to Markdown so a\n * rich article body round-trips through the cms `richtext` contract (fields\n * store Markdown, `marked` renders it — NOT HTML). Handles the structures a\n * marked-rendered body + the toolbar produce: headings, paragraphs, emphasis,\n * links, lists, blockquote, code. Formatting with no clean Markdown equivalent\n * (underline, coloured spans) is passed through as inline HTML — `marked`\n * renders inline HTML, so it survives without corrupting the source. Tables and\n * anything unrecognised are passed through as their outerHTML for the same\n * reason: never drop content just because it doesn't map to a Markdown token.\n */\nfunction htmlToMarkdown(html: string): string {\n const container = document.createElement(\"div\");\n container.innerHTML = html;\n return serializeBlockChildren(container).replace(/\\n{3,}/g, \"\\n\\n\").trim() + \"\\n\";\n}\n\nfunction serializeBlockChildren(parent: Node): string {\n const blocks: string[] = [];\n parent.childNodes.forEach((node) => {\n const s = serializeBlock(node).trim();\n if (s) blocks.push(s);\n });\n return blocks.join(\"\\n\\n\");\n}\n\nfunction serializeBlock(node: Node): string {\n if (node.nodeType === Node.TEXT_NODE) return (node.textContent ?? \"\").trim();\n if (node.nodeType !== Node.ELEMENT_NODE) return \"\";\n const el = node as HTMLElement;\n const tag = el.tagName.toLowerCase();\n switch (tag) {\n case \"h1\": return \"# \" + serializeInline(el);\n case \"h2\": return \"## \" + serializeInline(el);\n case \"h3\": return \"### \" + serializeInline(el);\n case \"h4\": return \"#### \" + serializeInline(el);\n case \"h5\": return \"##### \" + serializeInline(el);\n case \"h6\": return \"###### \" + serializeInline(el);\n case \"p\":\n case \"div\":\n return serializeInline(el);\n case \"ul\":\n return serializeList(el, false);\n case \"ol\":\n return serializeList(el, true);\n case \"blockquote\":\n // Serialize the quote's OWN block children (it usually wraps <p>s), then\n // prefix each line with \"> \" — treating it as inline dropped the <p> and\n // left a stray leading space.\n return serializeBlockChildren(el)\n .split(\"\\n\")\n .map((l) => (l ? \"> \" + l : \">\"))\n .join(\"\\n\");\n case \"pre\":\n return \"```\\n\" + (el.textContent ?? \"\").replace(/\\n+$/, \"\") + \"\\n```\";\n case \"hr\":\n return \"---\";\n case \"br\":\n return \"\";\n case \"table\":\n // Rebuild the Markdown pipe table (NOT raw-HTML passthrough) so the\n // consumer's marked renderer re-applies its own table treatment (e.g.\n // broberg's mobile scroll-wrapper) instead of getting a bare <table>.\n return serializeTable(el);\n default:\n return serializeInline(el);\n }\n}\n\nfunction serializeTable(table: HTMLElement): string {\n const cellText = (c: Element) => serializeInline(c).trim().replace(/\\|/g, \"\\\\|\");\n const rows: string[] = [];\n const headCells = Array.from(table.querySelectorAll(\"thead tr\")).flatMap((tr) =>\n Array.from(tr.children).map(cellText),\n );\n if (headCells.length) {\n rows.push(\"| \" + headCells.join(\" | \") + \" |\");\n rows.push(\"| \" + headCells.map(() => \"---\").join(\" | \") + \" |\");\n }\n const bodyRows = table.querySelector(\"tbody\")\n ? Array.from(table.querySelectorAll(\"tbody tr\"))\n : Array.from(table.querySelectorAll(\"tr\")).filter((tr) => !tr.closest(\"thead\"));\n bodyRows.forEach((tr) => {\n rows.push(\"| \" + Array.from(tr.children).map(cellText).join(\" | \") + \" |\");\n });\n return rows.join(\"\\n\");\n}\n\nfunction serializeList(listEl: HTMLElement, ordered: boolean): string {\n const items: string[] = [];\n let n = 1;\n listEl.childNodes.forEach((child) => {\n if (child.nodeType === Node.ELEMENT_NODE && (child as HTMLElement).tagName.toLowerCase() === \"li\") {\n const marker = ordered ? `${n++}. ` : \"- \";\n items.push(marker + serializeInline(child as HTMLElement).trim());\n }\n });\n return items.join(\"\\n\");\n}\n\nfunction serializeInline(node: Node): string {\n let out = \"\";\n node.childNodes.forEach((child) => {\n if (child.nodeType === Node.TEXT_NODE) {\n out += (child.textContent ?? \"\").replace(/\\s+/g, \" \");\n return;\n }\n if (child.nodeType !== Node.ELEMENT_NODE) return;\n const el = child as HTMLElement;\n const tag = el.tagName.toLowerCase();\n const inner = serializeInline(el);\n switch (tag) {\n case \"strong\":\n case \"b\":\n out += inner.trim() ? `**${inner.trim()}**` : \"\";\n break;\n case \"em\":\n case \"i\":\n out += inner.trim() ? `*${inner.trim()}*` : \"\";\n break;\n case \"code\":\n out += \"`\" + inner + \"`\";\n break;\n case \"br\":\n out += \" \\n\";\n break;\n case \"a\": {\n const href = el.getAttribute(\"href\") || \"\";\n out += href ? `[${inner}](${href})` : inner;\n break;\n }\n case \"img\": {\n // Content images embedded in the body () — dropping them\n // would silently delete images from an article on save. Void element,\n // no children, so this must be handled explicitly.\n const src = el.getAttribute(\"src\") || \"\";\n const alt = el.getAttribute(\"alt\") || \"\";\n if (src) out += ``;\n break;\n }\n case \"u\":\n out += `<u>${inner}</u>`; // no Markdown for underline — pass through\n break;\n case \"span\":\n case \"font\": {\n const color = el.style.color || el.getAttribute(\"color\") || \"\";\n out += color ? `<span style=\"color:${color}\">${inner}</span>` : inner;\n break;\n }\n default:\n out += inner;\n }\n });\n return out;\n}\n\nasync function saveField(el: HTMLElement, value: string, token: string, options: ResolvedOptions): Promise<void> {\n const collection = el.dataset.cmsCollection;\n const slug = el.dataset.cmsSlug;\n const field = el.dataset.cmsField;\n if (!collection || !slug || !field) return;\n const slice = el.dataset.cmsSlice;\n\n showPill(el, \"saving\");\n try {\n const getRes = await fetch(\n `${options.cmsBaseUrl}/api/cms/${collection}/${slug}?site=${options.siteId}`,\n { headers: { Authorization: `Bearer ${token}` } },\n );\n if (!getRes.ok) throw new Error(`GET failed: ${getRes.status}`);\n const doc = (await getRes.json()) as { data?: Record<string, unknown> };\n // Deep-clone so mutating a nested array/object (dot-path saves) never\n // aliases the fetched doc — same safety whether field is flat or nested.\n const mergedData = JSON.parse(JSON.stringify(doc.data ?? {})) as Record<string, unknown>;\n if (slice !== undefined) {\n // Field-slice save: replace just this segment inside the full field value,\n // preserving every other segment + embed. Aborts on a non-unique match.\n const currentVal = typeof mergedData[field] === \"string\" ? (mergedData[field] as string) : \"\";\n mergedData[field] = applyFieldSlice(currentVal, slice, value);\n } else if (field.includes(\".\")) {\n setDeepField(mergedData, field, value);\n } else {\n mergedData[field] = value;\n }\n\n const patchRes = await fetch(\n `${options.cmsBaseUrl}/api/cms/${collection}/${slug}?site=${options.siteId}`,\n {\n method: \"PATCH\",\n headers: { Authorization: `Bearer ${token}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ data: mergedData }),\n },\n );\n if (!patchRes.ok) throw new Error(`PATCH failed: ${patchRes.status}`);\n // The slice we just wrote is now the current text — track it so a second\n // edit of the same segment (no reload) matches against the new value.\n if (slice !== undefined) el.dataset.cmsSlice = value;\n showPill(el, \"saved\");\n } catch {\n showPill(el, \"error\");\n }\n}\n\nconst pills = new WeakMap<HTMLElement, HTMLElement>();\n\nfunction showPill(el: HTMLElement, state: \"saving\" | \"saved\" | \"error\"): void {\n let pill = pills.get(el);\n if (!pill) {\n pill = document.createElement(\"span\");\n pill.setAttribute(\"data-cms-inline-edit-pill\", \"\");\n document.body.appendChild(pill);\n pills.set(el, pill);\n }\n const rect = el.getBoundingClientRect();\n pill.style.cssText =\n `position:fixed;top:${rect.top - 26}px;left:${rect.left}px;font:600 11px system-ui,sans-serif;` +\n `padding:3px 9px;border-radius:5px;z-index:2147483647;pointer-events:none;` +\n `box-shadow:0 2px 8px rgba(0,0,0,.25);`;\n\n if (state === \"saving\") {\n pill.textContent = uiLabels.saving;\n pill.style.background = \"#1c2027\";\n pill.style.color = \"#fff\";\n } else if (state === \"saved\") {\n pill.textContent = uiLabels.saved;\n pill.style.background = \"#16a34a\";\n pill.style.color = \"#fff\";\n setTimeout(() => {\n pill?.remove();\n pills.delete(el);\n }, 1500);\n } else {\n pill.textContent = uiLabels.error;\n pill.style.background = \"#dc2626\";\n pill.style.color = \"#fff\";\n }\n}\n\nfunction showActiveBadge(options: ResolvedOptions): void {\n // The connected badge IS the exit-edit action: [icon] \"Afslut redigering\".\n // No \"Redigerer som {name}\" — the whole pill is one click to leave edit mode.\n const badge = document.createElement(\"button\");\n badge.type = \"button\";\n badge.setAttribute(\"data-cms-inline-edit-badge\", \"\");\n badge.style.cssText =\n \"position:fixed;bottom:16px;left:16px;display:inline-flex;align-items:center;gap:7px;\" +\n \"background:#1c2027;color:#fff;font:600 12px system-ui,sans-serif;padding:8px 14px;\" +\n \"border-radius:999px;z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);\" +\n \"border:none;cursor:pointer;opacity:.9;transition:opacity .15s;\";\n const text = document.createElement(\"span\");\n text.textContent = options.disconnectLabel;\n badge.append(makeIcon(), text);\n badge.addEventListener(\"mouseenter\", () => (badge.style.opacity = \"1\"));\n badge.addEventListener(\"mouseleave\", () => (badge.style.opacity = \".9\"));\n // Clicking the badge LEAVES edit mode but KEEPS the login → back to the idle\n // \"Rediger\" pill (not a logout). Explicit logout lives on the idle pill.\n badge.addEventListener(\"click\", exitEditMode);\n document.body.appendChild(badge);\n}\n\n/** Reads JWT claims for DISPLAY ONLY — never used for authorization decisions. */\nfunction decodeJwtPayload(token: string): Record<string, unknown> | null {\n try {\n const payload = token.split(\".\")[1];\n if (!payload) return null;\n return JSON.parse(atob(payload.replace(/-/g, \"+\").replace(/_/g, \"/\")));\n } catch {\n return null;\n }\n}\n\nfunction injectStyles(): void {\n const style = document.createElement(\"style\");\n style.textContent = `\n [data-cms-field] { outline: 1px dashed transparent; outline-offset: 2px; cursor: text; transition: outline-color .15s; }\n [data-cms-field]:hover { outline-color: rgba(0,178,255,.5); }\n [data-cms-field][contenteditable=\"true\"] { outline: 2px solid #00b2ff; outline-offset: 2px; }\n [data-cms-field][data-cms-richtext=\"true\"] { cursor: text; }\n .cms-rich-editing { outline: 2px solid #00b2ff !important; outline-offset: 6px; border-radius: 4px; }\n `;\n document.head.appendChild(style);\n}\n","/**\n * Replace ONE slice of a larger text field.\n *\n * Used when an element renders a single SEGMENT of a bigger field — e.g. one\n * prose paragraph of a `content` body that is interleaved with `[block:]`\n * embeds — so editing that segment must write back into the full field without\n * clobbering the other segments or the embeds.\n *\n * Safety-first: throws if `original` is not found EXACTLY once, so an ambiguous\n * or stale slice ABORTS the save (surfaced as the error pill) instead of\n * corrupting the whole field. Pure + framework-agnostic; unit-tested.\n */\nexport function applyFieldSlice(current: string, original: string, next: string): string {\n const first = current.indexOf(original);\n if (first === -1) throw new Error(\"field-slice: original not found\");\n if (current.indexOf(original, first + original.length) !== -1) {\n throw new Error(\"field-slice: original found more than once (ambiguous)\");\n }\n return current.slice(0, first) + next + current.slice(first + original.length);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACYO,SAAS,gBAAgB,SAAiB,UAAkB,MAAsB;AACvF,QAAM,QAAQ,QAAQ,QAAQ,QAAQ;AACtC,MAAI,UAAU,GAAI,OAAM,IAAI,MAAM,iCAAiC;AACnE,MAAI,QAAQ,QAAQ,UAAU,QAAQ,SAAS,MAAM,MAAM,IAAI;AAC7D,UAAM,IAAI,MAAM,wDAAwD;AAAA,EAC1E;AACA,SAAO,QAAQ,MAAM,GAAG,KAAK,IAAI,OAAO,QAAQ,MAAM,QAAQ,SAAS,MAAM;AAC/E;;;ADyCA,IAAM,iBAA6C;AAAA,EACjD,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,aAAa;AAAA,EACb,eAAe;AAAA,EACf,OAAO;AAAA,EACP,OAAO;AAAA,EACP,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,OAAO;AACT;AAKA,IAAI,WAAuC;AAE3C,SAAS,eAAe,SAA6C;AACnE,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,YAAY;AAAA;AAAA,IAEZ,cAAc;AAAA,IACd,iBAAiB;AAAA,IACjB,aAAa;AAAA,IACb,eAAe;AAAA,IACf,GAAG;AAAA,IACH,QAAQ,EAAE,GAAG,gBAAgB,GAAI,QAAQ,UAAU,CAAC,EAAG;AAAA,EACzD;AACF;AAEA,eAAsB,eAAe,SAA2C;AAC9E,MAAI,OAAO,WAAW,YAAa;AACnC,QAAM,WAAW,eAAe,OAAO;AACvC,aAAW,SAAS;AAKpB,QAAM,eAAe,oBAAoB,QAAQ;AAEjD,QAAM,UAAU,MAAM,aAAa,QAAQ;AAC3C,MAAI,CAAC,QAAS;AAEd,QAAM,QAAQ,kBAAkB,OAAO;AACvC,MAAI,CAAC,OAAO;AAKV,QAAI,SAAS,eAAe;AAC1B,wBAAkB,QAAQ;AAG1B,4BAAsB,QAAQ;AAAA,IAChC;AACA;AAAA,EACF;AAMA,cAAY,OAAO,QAAQ;AAC3B,MAAI,aAAc,eAAc;AAAA,MAC3B,cAAa;AACpB;AAQA,SAAS,sBAAsB,SAAgC;AAC7D,MAAI;AACJ,MAAI;AACF,gBAAY,IAAI,IAAI,QAAQ,UAAU,EAAE;AAAA,EAC1C,QAAQ;AACN;AAAA,EACF;AACA,SAAO,iBAAiB,WAAW,CAAC,UAAwB;AAC1D,QAAI,MAAM,WAAW,UAAW;AAChC,UAAM,OAAO,MAAM;AACnB,QAAI,CAAC,QAAQ,KAAK,SAAS,0BAA0B,OAAO,KAAK,UAAU,SAAU;AACrF,QAAI,KAAK,QAAQ,KAAK,SAAS,QAAQ,OAAQ;AAC/C,QAAI,UAAU,KAAK,KAAK,EAAG;AAC3B,QAAI,SAAS,cAAc,8BAA8B,EAAG;AAC5D,iBAAa,QAAQ,QAAQ,YAAY,KAAK,KAAK;AACnD,aAAS,cAAc,gCAAgC,GAAG,OAAO;AAEjE,gBAAY,KAAK,OAAO,OAAO;AAC/B,kBAAc;AAAA,EAChB,CAAC;AACH;AAUO,SAAS,kBAAkB,SAA2C;AAC3E,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,QAAM,WAAW,eAAe,OAAO;AACvC,sBAAoB,QAAQ;AAC5B,QAAM,QAAQ,aAAa,QAAQ,SAAS,UAAU;AACtD,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,UAAU,KAAK,GAAG;AACpB,iBAAa,WAAW,SAAS,UAAU;AAC3C,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAGO,SAAS,gBAAgB,SAA4B,WAA2B;AACrF,QAAM,WAAW,eAAe,OAAO;AACvC,SACE,GAAG,SAAS,UAAU,mCAAmC,mBAAmB,SAAS,MAAM,CAAC,WACjF,mBAAmB,SAAS,CAAC;AAE5C;AAGO,SAAS,WAAW,SAAkC;AAC3D,MAAI,OAAO,WAAW,YAAa;AACnC,eAAa,WAAW,eAAe,OAAO,EAAE,UAAU;AAC5D;AAMA,SAAS,oBAAoB,SAAmC;AAC9D,QAAM,MAAM,IAAI,IAAI,OAAO,SAAS,IAAI;AACxC,QAAM,WAAW,IAAI,aAAa,IAAI,QAAQ,UAAU;AACxD,MAAI,CAAC,SAAU,QAAO;AACtB,eAAa,QAAQ,QAAQ,YAAY,QAAQ;AACjD,MAAI,aAAa,OAAO,QAAQ,UAAU;AAC1C,SAAO,QAAQ,aAAa,CAAC,GAAG,IAAI,IAAI,SAAS,CAAC;AAClD,SAAO;AACT;AAEA,eAAe,aAAa,SAA4C;AACtE,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,GAAG,QAAQ,UAAU,gCAAgC,QAAQ,MAAM,EAAE;AAC7F,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,WAAO,KAAK,YAAY;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,UAAU,OAAwB;AACzC,QAAM,SAAS,iBAAiB,KAAK;AACrC,QAAM,MAAM,OAAO,QAAQ,QAAQ,WAAW,OAAO,MAAM;AAC3D,SAAO,OAAO,KAAK,IAAI,IAAI;AAC7B;AAKA,IAAM,iBACJ;AASF,IAAM,SACJ;AAQF,IAAM,SACJ;AAQF,SAAS,WAA4B;AACnC,QAAM,OAAO,SAAS,cAAc,MAAM;AAC1C,OAAK,MAAM,UAAU;AACrB,OAAK,YAAY;AACjB,SAAO;AACT;AAEA,SAAS,kBAAkB,SAAgC;AACzD,QAAM,aAAa,gBAAgB,SAAS,OAAO,SAAS,IAAI;AAEhE,QAAM,OAAO,SAAS,cAAc,GAAG;AACvC,OAAK,OAAO;AACZ,OAAK,aAAa,gCAAgC,EAAE;AACpD,OAAK,MAAM,UACT;AAIF,QAAM,OAAO,SAAS,cAAc,MAAM;AAC1C,OAAK,cAAc,QAAQ;AAC3B,OAAK,OAAO,SAAS,GAAG,IAAI;AAI5B,OAAK,iBAAiB,SAAS,CAAC,MAAM;AACpC,MAAE,eAAe;AACjB,UAAM,QAAQ,OAAO;AAAA,MACnB;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,QAAI,CAAC,MAAO,QAAO,SAAS,OAAO;AAAA,EACrC,CAAC;AACD,WAAS,KAAK,YAAY,IAAI;AAChC;AAWA,IAAI,gBAAgB;AACpB,IAAI,cAAc;AAClB,IAAI,eAAuC;AAE3C,SAAS,YAAY,OAAe,SAAgC;AAClE,iBAAe;AACf,MAAI,YAAa;AACjB,eAAa;AACb,WAAS,iBAA8B,kBAAkB,EAAE,QAAQ,CAAC,OAAO,UAAU,IAAI,OAAO,OAAO,CAAC;AACxG,gBAAc;AAChB;AAEA,SAAS,gBAAsB;AAC7B,MAAI,CAAC,gBAAgB,cAAe;AACpC,kBAAgB;AAChB,iBAAe;AACf,kBAAgB,YAAY;AAC9B;AAEA,SAAS,eAAqB;AAC5B,MAAI,CAAC,gBAAgB,CAAC,cAAe;AACrC,iBAAe;AACf,kBAAgB;AAChB,WAAS,cAAc,8BAA8B,GAAG,OAAO;AAC/D,eAAa;AACf;AAEA,SAAS,iBAAuB;AAC9B,WAAS,cAAc,6BAA6B,GAAG,OAAO;AAChE;AAEA,SAAS,eAAqB;AAC5B,MAAI,CAAC,aAAc;AACnB,QAAM,UAAU;AAChB,iBAAe;AACf,QAAM,OAAO,SAAS,cAAc,KAAK;AACzC,OAAK,aAAa,6BAA6B,EAAE;AACjD,OAAK,MAAM,UACT;AAIF,QAAM,OAAO,SAAS,cAAc,QAAQ;AAC5C,OAAK,OAAO;AACZ,OAAK,aAAa,8BAA8B,EAAE;AAClD,OAAK,MAAM,UACT;AAEF,QAAM,WAAW,SAAS,cAAc,MAAM;AAC9C,WAAS,cAAc,QAAQ;AAC/B,OAAK,OAAO,SAAS,GAAG,QAAQ;AAChC,OAAK,iBAAiB,SAAS,aAAa;AAE5C,QAAM,MAAM,SAAS,cAAc,QAAQ;AAC3C,MAAI,OAAO;AACX,MAAI,aAAa,+BAA+B,EAAE;AAClD,MAAI,cAAc,QAAQ;AAC1B,MAAI,MAAM,UACR;AAEF,MAAI,iBAAiB,SAAS,MAAM;AAClC,eAAW,OAAO;AAClB,mBAAe;AAAA,EACjB,CAAC;AACD,OAAK,OAAO,MAAM,GAAG;AACrB,WAAS,KAAK,YAAY,IAAI;AAChC;AAEA,SAAS,UAAU,IAAiB,OAAe,SAAgC;AAQjF,MAAI,GAAG,QAAQ,gBAAgB,QAAQ;AACrC,kBAAc,IAAI,OAAO,SAAS,UAAU;AAC5C;AAAA,EACF;AACA,MAAI,GAAG,QAAQ,YAAY,QAAQ;AACjC,kBAAc,IAAI,OAAO,SAAS,MAAM;AACxC;AAAA,EACF;AAEA,KAAG,iBAAiB,SAAS,CAAC,MAAM;AAClC,QAAI,CAAC,cAAe;AACpB,QAAI,GAAG,aAAa,iBAAiB,MAAM,OAAQ;AAInD,MAAE,eAAe;AACjB,MAAE,gBAAgB;AAClB,OAAG,QAAQ,mBAAmB,GAAG,eAAe;AAChD,OAAG,aAAa,mBAAmB,MAAM;AACzC,OAAG,MAAM;AAAA,EACX,CAAC;AAED,KAAG,iBAAiB,QAAQ,MAAM;AAChC,OAAG,gBAAgB,iBAAiB;AACpC,UAAM,WAAW,GAAG,QAAQ,oBAAoB;AAChD,UAAM,UAAU,GAAG,eAAe;AAClC,QAAI,QAAQ,KAAK,MAAM,SAAS,KAAK,EAAG;AACxC,SAAK,UAAU,IAAI,QAAQ,KAAK,GAAG,OAAO,OAAO;AAAA,EACnD,CAAC;AAED,KAAG,iBAAiB,SAAS,CAAC,MAAM;AAClC,MAAE,eAAe;AACjB,UAAM,OAAO,EAAE,eAAe,QAAQ,YAAY,KAAK;AACvD,aAAS,YAAY,cAAc,OAAO,IAAI;AAAA,EAChD,CAAC;AAED,KAAG,iBAAiB,WAAW,CAAC,MAAM;AACpC,QAAI,EAAE,QAAQ,SAAS;AACrB,QAAE,eAAe;AACjB,SAAG,KAAK;AAAA,IACV;AAAA,EACF,CAAC;AACH;AAiBA,IAAI,UAA8B;AAClC,IAAI,cAAkC;AAEtC,SAAS,cAAc,IAAiB,OAAe,SAA0B,MAAsB;AACrG,KAAG,iBAAiB,SAAS,CAAC,MAAM;AAClC,QAAI,CAAC,cAAe;AACpB,QAAI,WAAW,QAAQ,OAAO,GAAI;AAClC,MAAE,eAAe;AACjB,MAAE,gBAAgB;AAClB,iBAAa,IAAI,OAAO,SAAS,IAAI;AAAA,EACvC,CAAC;AACH;AAEA,SAAS,aAAa,IAAiB,OAAe,SAA0B,MAAsB;AACpG,iBAAe;AACf,YAAU,EAAE,IAAI,OAAO,SAAS,cAAc,GAAG,WAAW,KAAK;AACjE,KAAG,aAAa,mBAAmB,MAAM;AACzC,KAAG,UAAU,IAAI,kBAAkB;AACnC,KAAG,MAAM;AACT,kBAAgB;AAEhB,WAAS,cAAc,IAAI,YAAY,4BAA4B,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC;AACxF;AAEA,SAAS,iBAAuB;AAC9B,MAAI,CAAC,QAAS;AACd,QAAM,EAAE,IAAI,OAAO,SAAS,cAAc,KAAK,IAAI;AACnD,YAAU;AACV,KAAG,gBAAgB,iBAAiB;AACpC,KAAG,UAAU,OAAO,kBAAkB;AACtC,kBAAgB;AAChB,MAAI,GAAG,cAAc,cAAc;AACjC,UAAM,QAAQ,SAAS,SAAS,GAAG,YAAY,eAAe,GAAG,SAAS;AAC1E,SAAK,UAAU,IAAI,OAAO,OAAO,OAAO;AAAA,EAC1C;AACA,WAAS,cAAc,IAAI,YAAY,8BAA8B,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC;AAC1F;AAEA,SAAS,kBAAwB;AAC/B,MAAI,CAAC,YAAa,eAAc,iBAAiB;AACjD,cAAY,MAAM,UAAU;AAC9B;AAEA,SAAS,kBAAwB;AAC/B,MAAI,YAAa,aAAY,MAAM,UAAU;AAC7C,kBAAgB;AAClB;AAEA,SAAS,cAAc,OAAe,OAAe,QAAuC;AAC1F,QAAM,IAAI,SAAS,cAAc,QAAQ;AACzC,IAAE,OAAO;AACT,IAAE,YAAY;AACd,IAAE,QAAQ;AACV,IAAE,MAAM,UACN;AAEF,IAAE,iBAAiB,cAAc,MAAO,EAAE,MAAM,aAAa,SAAU;AACvE,IAAE,iBAAiB,cAAc,MAAO,EAAE,MAAM,aAAa,MAAO;AAEpE,IAAE,iBAAiB,aAAa,CAAC,MAAM;AACrC,MAAE,eAAe;AACjB,MAAE,gBAAgB;AAClB,WAAO;AAAA,EACT,CAAC;AACD,SAAO;AACT;AAEA,SAAS,mBAAgC;AACvC,QAAM,IAAI,SAAS,cAAc,KAAK;AACtC,IAAE,aAAa,gCAAgC,EAAE;AACjD,IAAE,MAAM,UACN;AAMF,IAAE,YAAY,cAAc,YAAY,SAAS,MAAM,MAAM,SAAS,YAAY,MAAM,CAAC,CAAC;AAC1F,IAAE,YAAY,cAAc,YAAY,SAAS,QAAQ,MAAM,SAAS,YAAY,QAAQ,CAAC,CAAC;AAC9F,IAAE,YAAY,cAAc,YAAY,SAAS,WAAW,MAAM,SAAS,YAAY,WAAW,CAAC,CAAC;AAEpG,QAAM,MAAM,MAAM;AAChB,UAAM,IAAI,SAAS,cAAc,KAAK;AACtC,MAAE,MAAM,UAAU;AAClB,WAAO;AAAA,EACT;AACA,IAAE,YAAY,IAAI,CAAC;AAInB,IAAE,YAAY,cAAc,QAAQ,SAAS,eAAe,MAAM,SAAS,YAAY,qBAAqB,CAAC,CAAC;AAC9G,IAAE,YAAY,cAAc,QAAQ,SAAS,aAAa,MAAM,SAAS,YAAY,mBAAmB,CAAC,CAAC;AAE1G,IAAE,YAAY,IAAI,CAAC;AAGnB,QAAM,WAAW,SAAS,cAAc,OAAO;AAC/C,WAAS,MAAM,UAAU;AACzB,WAAS,cAAc,SAAS;AAChC,QAAM,MAAM,SAAS,cAAc,OAAO;AAC1C,MAAI,OAAO;AACX,MAAI,MAAM,UAAU;AACpB,MAAI,iBAAiB,aAAa,CAAC,MAAM,EAAE,gBAAgB,CAAC;AAC5D,MAAI,iBAAiB,SAAS,MAAM,SAAS,YAAY,aAAa,OAAO,IAAI,KAAK,CAAC;AACvF,WAAS,QAAQ,GAAG;AACpB,IAAE,YAAY,QAAQ;AAEtB,IAAE,YAAY,IAAI,CAAC;AAEnB,QAAM,WAAW,cAAc,aAAM,SAAS,OAAO,MAAM,kBAAkB,QAAQ,CAAC;AACtF,WAAS,MAAM,WAAW;AAC1B,IAAE,YAAY,QAAQ;AAEtB,IAAE,YAAY,IAAI,CAAC;AAEnB,QAAM,OAAO,SAAS,cAAc,QAAQ;AAC5C,OAAK,OAAO;AACZ,OAAK,cAAc,SAAS;AAC5B,OAAK,MAAM,UACT;AAEF,OAAK,iBAAiB,aAAa,CAAC,MAAM;AACxC,MAAE,eAAe;AACjB,MAAE,gBAAgB;AAClB,mBAAe;AAAA,EACjB,CAAC;AACD,IAAE,YAAY,IAAI;AAElB,WAAS,KAAK,YAAY,CAAC;AAC3B,SAAO;AACT;AAGA,IAAM,SACJ,oYAAmH;AAAA,EACjH;AACF;AACF,IAAI,cAAkC;AACtC,IAAI,aAA2B;AAE/B,SAAS,kBAAkB,QAA2B;AACpD,MAAI,CAAC,YAAa,eAAc,iBAAiB;AACjD,MAAI,YAAY,MAAM,YAAY,SAAS;AACzC,oBAAgB;AAChB;AAAA,EACF;AACA,QAAM,MAAM,OAAO,aAAa;AAChC,MAAI,OAAO,IAAI,aAAa,EAAG,cAAa,IAAI,WAAW,CAAC,EAAE,WAAW;AACzE,QAAM,OAAO,OAAO,sBAAsB;AAC1C,cAAY,MAAM,MAAM,GAAG,KAAK,SAAS,CAAC;AAC1C,cAAY,MAAM,OAAO,GAAG,KAAK,IAAI,KAAK,MAAM,OAAO,aAAa,GAAG,CAAC;AACxE,cAAY,MAAM,UAAU;AAC9B;AAEA,SAAS,kBAAwB;AAC/B,MAAI,YAAa,aAAY,MAAM,UAAU;AAC/C;AAEA,SAAS,mBAAgC;AACvC,QAAM,IAAI,SAAS,cAAc,KAAK;AACtC,IAAE,aAAa,gCAAgC,EAAE;AACjD,IAAE,MAAM,UACN;AAGF,QAAM,OAAO,SAAS,cAAc,KAAK;AACzC,OAAK,MAAM,UAAU;AACrB,SAAO,QAAQ,CAAC,OAAO;AACrB,UAAM,IAAI,SAAS,cAAc,QAAQ;AACzC,MAAE,OAAO;AACT,MAAE,cAAc;AAChB,MAAE,MAAM,UACN;AACF,MAAE,iBAAiB,cAAc,MAAO,EAAE,MAAM,aAAa,SAAU;AACvE,MAAE,iBAAiB,cAAc,MAAO,EAAE,MAAM,aAAa,MAAO;AACpE,MAAE,iBAAiB,aAAa,CAAC,MAAM;AACrC,QAAE,eAAe;AACjB,QAAE,gBAAgB;AAClB,kBAAY,EAAE;AAAA,IAChB,CAAC;AACD,SAAK,YAAY,CAAC;AAAA,EACpB,CAAC;AACD,IAAE,YAAY,IAAI;AAClB,WAAS,KAAK,YAAY,CAAC;AAC3B,SAAO;AACT;AAEA,SAAS,YAAY,OAAqB;AACxC,MAAI,CAAC,QAAS;AACd,UAAQ,GAAG,MAAM;AACjB,QAAM,MAAM,OAAO,aAAa;AAChC,MAAI,cAAc,KAAK;AACrB,QAAI,gBAAgB;AACpB,QAAI,SAAS,UAAU;AAAA,EACzB;AACA,WAAS,YAAY,cAAc,OAAO,KAAK;AAC/C,eAAa;AACb,kBAAgB;AAClB;AAGA,IAAI,OAAO,aAAa,aAAa;AACnC,WAAS;AAAA,IACP;AAAA,IACA,CAAC,MAAM;AACL,UAAI,CAAC,QAAS;AACd,YAAM,SAAS,EAAE;AACjB,UAAI,CAAC,OAAQ;AACb,UAAI,OAAO,QAAQ,gCAAgC,EAAG;AACtD,UAAI,QAAQ,GAAG,SAAS,MAAM,EAAG;AACjC,qBAAe;AAAA,IACjB;AAAA,IACA;AAAA,EACF;AACF;AASA,SAAS,aAAa,MAA+B,MAAc,OAAqB;AACtF,QAAM,QAAQ,KAAK,MAAM,GAAG;AAC5B,MAAI,MAAW;AACf,WAAS,IAAI,GAAG,IAAI,MAAM,SAAS,GAAG,KAAK;AACzC,UAAM,OAAO,MAAM,CAAC,KAAK;AACzB,UAAM,MAAuB,QAAQ,KAAK,IAAI,IAAI,OAAO,IAAI,IAAI;AACjE,QAAI,OAAO,QAAQ,OAAO,QAAQ,YAAY,IAAI,GAAG,MAAM,OAAW;AACtE,UAAM,IAAI,GAAG;AAAA,EACf;AACA,QAAM,OAAO,MAAM,MAAM,SAAS,CAAC,KAAK;AACxC,QAAM,UAA2B,QAAQ,KAAK,IAAI,IAAI,OAAO,IAAI,IAAI;AACrE,MAAI,OAAO,QAAQ,OAAO,QAAQ,SAAU;AAC5C,MAAI,OAAO,IAAI;AACjB;AAaA,SAAS,eAAe,MAAsB;AAC5C,QAAM,YAAY,SAAS,cAAc,KAAK;AAC9C,YAAU,YAAY;AACtB,SAAO,uBAAuB,SAAS,EAAE,QAAQ,WAAW,MAAM,EAAE,KAAK,IAAI;AAC/E;AAEA,SAAS,uBAAuB,QAAsB;AACpD,QAAM,SAAmB,CAAC;AAC1B,SAAO,WAAW,QAAQ,CAAC,SAAS;AAClC,UAAM,IAAI,eAAe,IAAI,EAAE,KAAK;AACpC,QAAI,EAAG,QAAO,KAAK,CAAC;AAAA,EACtB,CAAC;AACD,SAAO,OAAO,KAAK,MAAM;AAC3B;AAEA,SAAS,eAAe,MAAoB;AAC1C,MAAI,KAAK,aAAa,KAAK,UAAW,SAAQ,KAAK,eAAe,IAAI,KAAK;AAC3E,MAAI,KAAK,aAAa,KAAK,aAAc,QAAO;AAChD,QAAM,KAAK;AACX,QAAM,MAAM,GAAG,QAAQ,YAAY;AACnC,UAAQ,KAAK;AAAA,IACX,KAAK;AAAM,aAAO,OAAO,gBAAgB,EAAE;AAAA,IAC3C,KAAK;AAAM,aAAO,QAAQ,gBAAgB,EAAE;AAAA,IAC5C,KAAK;AAAM,aAAO,SAAS,gBAAgB,EAAE;AAAA,IAC7C,KAAK;AAAM,aAAO,UAAU,gBAAgB,EAAE;AAAA,IAC9C,KAAK;AAAM,aAAO,WAAW,gBAAgB,EAAE;AAAA,IAC/C,KAAK;AAAM,aAAO,YAAY,gBAAgB,EAAE;AAAA,IAChD,KAAK;AAAA,IACL,KAAK;AACH,aAAO,gBAAgB,EAAE;AAAA,IAC3B,KAAK;AACH,aAAO,cAAc,IAAI,KAAK;AAAA,IAChC,KAAK;AACH,aAAO,cAAc,IAAI,IAAI;AAAA,IAC/B,KAAK;AAIH,aAAO,uBAAuB,EAAE,EAC7B,MAAM,IAAI,EACV,IAAI,CAAC,MAAO,IAAI,OAAO,IAAI,GAAI,EAC/B,KAAK,IAAI;AAAA,IACd,KAAK;AACH,aAAO,WAAW,GAAG,eAAe,IAAI,QAAQ,QAAQ,EAAE,IAAI;AAAA,IAChE,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAIH,aAAO,eAAe,EAAE;AAAA,IAC1B;AACE,aAAO,gBAAgB,EAAE;AAAA,EAC7B;AACF;AAEA,SAAS,eAAe,OAA4B;AAClD,QAAM,WAAW,CAAC,MAAe,gBAAgB,CAAC,EAAE,KAAK,EAAE,QAAQ,OAAO,KAAK;AAC/E,QAAM,OAAiB,CAAC;AACxB,QAAM,YAAY,MAAM,KAAK,MAAM,iBAAiB,UAAU,CAAC,EAAE;AAAA,IAAQ,CAAC,OACxE,MAAM,KAAK,GAAG,QAAQ,EAAE,IAAI,QAAQ;AAAA,EACtC;AACA,MAAI,UAAU,QAAQ;AACpB,SAAK,KAAK,OAAO,UAAU,KAAK,KAAK,IAAI,IAAI;AAC7C,SAAK,KAAK,OAAO,UAAU,IAAI,MAAM,KAAK,EAAE,KAAK,KAAK,IAAI,IAAI;AAAA,EAChE;AACA,QAAM,WAAW,MAAM,cAAc,OAAO,IACxC,MAAM,KAAK,MAAM,iBAAiB,UAAU,CAAC,IAC7C,MAAM,KAAK,MAAM,iBAAiB,IAAI,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,QAAQ,OAAO,CAAC;AAChF,WAAS,QAAQ,CAAC,OAAO;AACvB,SAAK,KAAK,OAAO,MAAM,KAAK,GAAG,QAAQ,EAAE,IAAI,QAAQ,EAAE,KAAK,KAAK,IAAI,IAAI;AAAA,EAC3E,CAAC;AACD,SAAO,KAAK,KAAK,IAAI;AACvB;AAEA,SAAS,cAAc,QAAqB,SAA0B;AACpE,QAAM,QAAkB,CAAC;AACzB,MAAI,IAAI;AACR,SAAO,WAAW,QAAQ,CAAC,UAAU;AACnC,QAAI,MAAM,aAAa,KAAK,gBAAiB,MAAsB,QAAQ,YAAY,MAAM,MAAM;AACjG,YAAM,SAAS,UAAU,GAAG,GAAG,OAAO;AACtC,YAAM,KAAK,SAAS,gBAAgB,KAAoB,EAAE,KAAK,CAAC;AAAA,IAClE;AAAA,EACF,CAAC;AACD,SAAO,MAAM,KAAK,IAAI;AACxB;AAEA,SAAS,gBAAgB,MAAoB;AAC3C,MAAI,MAAM;AACV,OAAK,WAAW,QAAQ,CAAC,UAAU;AACjC,QAAI,MAAM,aAAa,KAAK,WAAW;AACrC,cAAQ,MAAM,eAAe,IAAI,QAAQ,QAAQ,GAAG;AACpD;AAAA,IACF;AACA,QAAI,MAAM,aAAa,KAAK,aAAc;AAC1C,UAAM,KAAK;AACX,UAAM,MAAM,GAAG,QAAQ,YAAY;AACnC,UAAM,QAAQ,gBAAgB,EAAE;AAChC,YAAQ,KAAK;AAAA,MACX,KAAK;AAAA,MACL,KAAK;AACH,eAAO,MAAM,KAAK,IAAI,KAAK,MAAM,KAAK,CAAC,OAAO;AAC9C;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AACH,eAAO,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,CAAC,MAAM;AAC5C;AAAA,MACF,KAAK;AACH,eAAO,MAAM,QAAQ;AACrB;AAAA,MACF,KAAK;AACH,eAAO;AACP;AAAA,MACF,KAAK,KAAK;AACR,cAAM,OAAO,GAAG,aAAa,MAAM,KAAK;AACxC,eAAO,OAAO,IAAI,KAAK,KAAK,IAAI,MAAM;AACtC;AAAA,MACF;AAAA,MACA,KAAK,OAAO;AAIV,cAAM,MAAM,GAAG,aAAa,KAAK,KAAK;AACtC,cAAM,MAAM,GAAG,aAAa,KAAK,KAAK;AACtC,YAAI,IAAK,QAAO,KAAK,GAAG,KAAK,GAAG;AAChC;AAAA,MACF;AAAA,MACA,KAAK;AACH,eAAO,MAAM,KAAK;AAClB;AAAA,MACF,KAAK;AAAA,MACL,KAAK,QAAQ;AACX,cAAM,QAAQ,GAAG,MAAM,SAAS,GAAG,aAAa,OAAO,KAAK;AAC5D,eAAO,QAAQ,sBAAsB,KAAK,KAAK,KAAK,YAAY;AAChE;AAAA,MACF;AAAA,MACA;AACE,eAAO;AAAA,IACX;AAAA,EACF,CAAC;AACD,SAAO;AACT;AAEA,eAAe,UAAU,IAAiB,OAAe,OAAe,SAAyC;AAC/G,QAAM,aAAa,GAAG,QAAQ;AAC9B,QAAM,OAAO,GAAG,QAAQ;AACxB,QAAM,QAAQ,GAAG,QAAQ;AACzB,MAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,MAAO;AACpC,QAAM,QAAQ,GAAG,QAAQ;AAEzB,WAAS,IAAI,QAAQ;AACrB,MAAI;AACF,UAAM,SAAS,MAAM;AAAA,MACnB,GAAG,QAAQ,UAAU,YAAY,UAAU,IAAI,IAAI,SAAS,QAAQ,MAAM;AAAA,MAC1E,EAAE,SAAS,EAAE,eAAe,UAAU,KAAK,GAAG,EAAE;AAAA,IAClD;AACA,QAAI,CAAC,OAAO,GAAI,OAAM,IAAI,MAAM,eAAe,OAAO,MAAM,EAAE;AAC9D,UAAM,MAAO,MAAM,OAAO,KAAK;AAG/B,UAAM,aAAa,KAAK,MAAM,KAAK,UAAU,IAAI,QAAQ,CAAC,CAAC,CAAC;AAC5D,QAAI,UAAU,QAAW;AAGvB,YAAM,aAAa,OAAO,WAAW,KAAK,MAAM,WAAY,WAAW,KAAK,IAAe;AAC3F,iBAAW,KAAK,IAAI,gBAAgB,YAAY,OAAO,KAAK;AAAA,IAC9D,WAAW,MAAM,SAAS,GAAG,GAAG;AAC9B,mBAAa,YAAY,OAAO,KAAK;AAAA,IACvC,OAAO;AACL,iBAAW,KAAK,IAAI;AAAA,IACtB;AAEA,UAAM,WAAW,MAAM;AAAA,MACrB,GAAG,QAAQ,UAAU,YAAY,UAAU,IAAI,IAAI,SAAS,QAAQ,MAAM;AAAA,MAC1E;AAAA,QACE,QAAQ;AAAA,QACR,SAAS,EAAE,eAAe,UAAU,KAAK,IAAI,gBAAgB,mBAAmB;AAAA,QAChF,MAAM,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AAAA,MAC3C;AAAA,IACF;AACA,QAAI,CAAC,SAAS,GAAI,OAAM,IAAI,MAAM,iBAAiB,SAAS,MAAM,EAAE;AAGpE,QAAI,UAAU,OAAW,IAAG,QAAQ,WAAW;AAC/C,aAAS,IAAI,OAAO;AAAA,EACtB,QAAQ;AACN,aAAS,IAAI,OAAO;AAAA,EACtB;AACF;AAEA,IAAM,QAAQ,oBAAI,QAAkC;AAEpD,SAAS,SAAS,IAAiB,OAA2C;AAC5E,MAAI,OAAO,MAAM,IAAI,EAAE;AACvB,MAAI,CAAC,MAAM;AACT,WAAO,SAAS,cAAc,MAAM;AACpC,SAAK,aAAa,6BAA6B,EAAE;AACjD,aAAS,KAAK,YAAY,IAAI;AAC9B,UAAM,IAAI,IAAI,IAAI;AAAA,EACpB;AACA,QAAM,OAAO,GAAG,sBAAsB;AACtC,OAAK,MAAM,UACT,sBAAsB,KAAK,MAAM,EAAE,WAAW,KAAK,IAAI;AAIzD,MAAI,UAAU,UAAU;AACtB,SAAK,cAAc,SAAS;AAC5B,SAAK,MAAM,aAAa;AACxB,SAAK,MAAM,QAAQ;AAAA,EACrB,WAAW,UAAU,SAAS;AAC5B,SAAK,cAAc,SAAS;AAC5B,SAAK,MAAM,aAAa;AACxB,SAAK,MAAM,QAAQ;AACnB,eAAW,MAAM;AACf,YAAM,OAAO;AACb,YAAM,OAAO,EAAE;AAAA,IACjB,GAAG,IAAI;AAAA,EACT,OAAO;AACL,SAAK,cAAc,SAAS;AAC5B,SAAK,MAAM,aAAa;AACxB,SAAK,MAAM,QAAQ;AAAA,EACrB;AACF;AAEA,SAAS,gBAAgB,SAAgC;AAGvD,QAAM,QAAQ,SAAS,cAAc,QAAQ;AAC7C,QAAM,OAAO;AACb,QAAM,aAAa,8BAA8B,EAAE;AACnD,QAAM,MAAM,UACV;AAIF,QAAM,OAAO,SAAS,cAAc,MAAM;AAC1C,OAAK,cAAc,QAAQ;AAC3B,QAAM,OAAO,SAAS,GAAG,IAAI;AAC7B,QAAM,iBAAiB,cAAc,MAAO,MAAM,MAAM,UAAU,GAAI;AACtE,QAAM,iBAAiB,cAAc,MAAO,MAAM,MAAM,UAAU,IAAK;AAGvE,QAAM,iBAAiB,SAAS,YAAY;AAC5C,WAAS,KAAK,YAAY,KAAK;AACjC;AAGA,SAAS,iBAAiB,OAA+C;AACvE,MAAI;AACF,UAAM,UAAU,MAAM,MAAM,GAAG,EAAE,CAAC;AAClC,QAAI,CAAC,QAAS,QAAO;AACrB,WAAO,KAAK,MAAM,KAAK,QAAQ,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG,CAAC,CAAC;AAAA,EACvE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,eAAqB;AAC5B,QAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,QAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOpB,WAAS,KAAK,YAAY,KAAK;AACjC;","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Replace ONE slice of a larger text field.
|
|
3
|
+
*
|
|
4
|
+
* Used when an element renders a single SEGMENT of a bigger field — e.g. one
|
|
5
|
+
* prose paragraph of a `content` body that is interleaved with `[block:]`
|
|
6
|
+
* embeds — so editing that segment must write back into the full field without
|
|
7
|
+
* clobbering the other segments or the embeds.
|
|
8
|
+
*
|
|
9
|
+
* Safety-first: throws if `original` is not found EXACTLY once, so an ambiguous
|
|
10
|
+
* or stale slice ABORTS the save (surfaced as the error pill) instead of
|
|
11
|
+
* corrupting the whole field. Pure + framework-agnostic; unit-tested.
|
|
7
12
|
*/
|
|
13
|
+
declare function applyFieldSlice(current: string, original: string, next: string): string;
|
|
14
|
+
|
|
8
15
|
/**
|
|
9
16
|
* Labels for the in-editor UI — the rich-text toolbar (bold/italic/underline
|
|
10
17
|
* tooltips, the visible "colour" + "done" buttons, the emoji tooltip) and the
|
|
@@ -16,6 +23,8 @@ interface InlineEditLabels {
|
|
|
16
23
|
bold?: string;
|
|
17
24
|
italic?: string;
|
|
18
25
|
underline?: string;
|
|
26
|
+
orderedList?: string;
|
|
27
|
+
unorderedList?: string;
|
|
19
28
|
color?: string;
|
|
20
29
|
emoji?: string;
|
|
21
30
|
done?: string;
|
|
@@ -34,8 +43,16 @@ interface InlineEditOptions {
|
|
|
34
43
|
storageKey?: string;
|
|
35
44
|
/** Text for the "not connected yet" prompt (a square-pen icon is prepended). Default "Log ind for at redigere". */
|
|
36
45
|
connectLabel?: string;
|
|
37
|
-
/** Text for the "connected" badge —
|
|
46
|
+
/** Text for the "connected + editing" badge — clicking it LEAVES edit mode but keeps the login (icon prepended). Default "Afbryd". */
|
|
38
47
|
disconnectLabel?: string;
|
|
48
|
+
/** Text for the explicit "log out" action on the idle pill — clicking it clears the token (back to the logged-out state). Default "Log ud". */
|
|
49
|
+
logoutLabel?: string;
|
|
50
|
+
/** Show an on-site "connect" pill to LOGGED-OUT visitors (click → connect flow).
|
|
51
|
+
* Default FALSE — customer-safe: a visitor without a token sees NOTHING. Only
|
|
52
|
+
* enable for a site with no real customers (e.g. an internal/first-party site)
|
|
53
|
+
* where a self-contained on-site login entry is wanted. The safe entry for a
|
|
54
|
+
* customer site is always the "Redigér live" button in webhouse.app CMS-admin. */
|
|
55
|
+
connectPrompt?: boolean;
|
|
39
56
|
/** Localised labels for the toolbar + save-status pill. Defaults are Danish. */
|
|
40
57
|
labels?: InlineEditLabels;
|
|
41
58
|
}
|
|
@@ -54,4 +71,4 @@ declare function buildConnectUrl(options: InlineEditOptions, returnUrl: string):
|
|
|
54
71
|
/** Clears the connected token in this browser (e.g. a "log out" action in a site's own /admin panel). */
|
|
55
72
|
declare function disconnect(options: InlineEditOptions): void;
|
|
56
73
|
|
|
57
|
-
export { type InlineEditLabels, type InlineEditOptions, buildConnectUrl, disconnect, getConnectedToken, initInlineEdit };
|
|
74
|
+
export { type InlineEditLabels, type InlineEditOptions, applyFieldSlice, buildConnectUrl, disconnect, getConnectedToken, initInlineEdit };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Replace ONE slice of a larger text field.
|
|
3
|
+
*
|
|
4
|
+
* Used when an element renders a single SEGMENT of a bigger field — e.g. one
|
|
5
|
+
* prose paragraph of a `content` body that is interleaved with `[block:]`
|
|
6
|
+
* embeds — so editing that segment must write back into the full field without
|
|
7
|
+
* clobbering the other segments or the embeds.
|
|
8
|
+
*
|
|
9
|
+
* Safety-first: throws if `original` is not found EXACTLY once, so an ambiguous
|
|
10
|
+
* or stale slice ABORTS the save (surfaced as the error pill) instead of
|
|
11
|
+
* corrupting the whole field. Pure + framework-agnostic; unit-tested.
|
|
7
12
|
*/
|
|
13
|
+
declare function applyFieldSlice(current: string, original: string, next: string): string;
|
|
14
|
+
|
|
8
15
|
/**
|
|
9
16
|
* Labels for the in-editor UI — the rich-text toolbar (bold/italic/underline
|
|
10
17
|
* tooltips, the visible "colour" + "done" buttons, the emoji tooltip) and the
|
|
@@ -16,6 +23,8 @@ interface InlineEditLabels {
|
|
|
16
23
|
bold?: string;
|
|
17
24
|
italic?: string;
|
|
18
25
|
underline?: string;
|
|
26
|
+
orderedList?: string;
|
|
27
|
+
unorderedList?: string;
|
|
19
28
|
color?: string;
|
|
20
29
|
emoji?: string;
|
|
21
30
|
done?: string;
|
|
@@ -34,8 +43,16 @@ interface InlineEditOptions {
|
|
|
34
43
|
storageKey?: string;
|
|
35
44
|
/** Text for the "not connected yet" prompt (a square-pen icon is prepended). Default "Log ind for at redigere". */
|
|
36
45
|
connectLabel?: string;
|
|
37
|
-
/** Text for the "connected" badge —
|
|
46
|
+
/** Text for the "connected + editing" badge — clicking it LEAVES edit mode but keeps the login (icon prepended). Default "Afbryd". */
|
|
38
47
|
disconnectLabel?: string;
|
|
48
|
+
/** Text for the explicit "log out" action on the idle pill — clicking it clears the token (back to the logged-out state). Default "Log ud". */
|
|
49
|
+
logoutLabel?: string;
|
|
50
|
+
/** Show an on-site "connect" pill to LOGGED-OUT visitors (click → connect flow).
|
|
51
|
+
* Default FALSE — customer-safe: a visitor without a token sees NOTHING. Only
|
|
52
|
+
* enable for a site with no real customers (e.g. an internal/first-party site)
|
|
53
|
+
* where a self-contained on-site login entry is wanted. The safe entry for a
|
|
54
|
+
* customer site is always the "Redigér live" button in webhouse.app CMS-admin. */
|
|
55
|
+
connectPrompt?: boolean;
|
|
39
56
|
/** Localised labels for the toolbar + save-status pill. Defaults are Danish. */
|
|
40
57
|
labels?: InlineEditLabels;
|
|
41
58
|
}
|
|
@@ -54,4 +71,4 @@ declare function buildConnectUrl(options: InlineEditOptions, returnUrl: string):
|
|
|
54
71
|
/** Clears the connected token in this browser (e.g. a "log out" action in a site's own /admin panel). */
|
|
55
72
|
declare function disconnect(options: InlineEditOptions): void;
|
|
56
73
|
|
|
57
|
-
export { type InlineEditLabels, type InlineEditOptions, buildConnectUrl, disconnect, getConnectedToken, initInlineEdit };
|
|
74
|
+
export { type InlineEditLabels, type InlineEditOptions, applyFieldSlice, buildConnectUrl, disconnect, getConnectedToken, initInlineEdit };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
|
+
// src/field-slice.ts
|
|
2
|
+
function applyFieldSlice(current, original, next) {
|
|
3
|
+
const first = current.indexOf(original);
|
|
4
|
+
if (first === -1) throw new Error("field-slice: original not found");
|
|
5
|
+
if (current.indexOf(original, first + original.length) !== -1) {
|
|
6
|
+
throw new Error("field-slice: original found more than once (ambiguous)");
|
|
7
|
+
}
|
|
8
|
+
return current.slice(0, first) + next + current.slice(first + original.length);
|
|
9
|
+
}
|
|
10
|
+
|
|
1
11
|
// src/index.ts
|
|
2
12
|
var DEFAULT_LABELS = {
|
|
3
13
|
bold: "Fed",
|
|
4
14
|
italic: "Kursiv",
|
|
5
15
|
underline: "Understreget",
|
|
16
|
+
orderedList: "Nummereret liste",
|
|
17
|
+
unorderedList: "Punktliste",
|
|
6
18
|
color: "Farve",
|
|
7
19
|
emoji: "Inds\xE6t emoji",
|
|
8
20
|
done: "F\xE6rdig",
|
|
@@ -18,6 +30,8 @@ function resolveOptions(options) {
|
|
|
18
30
|
// No emoji in defaults — the square-pen SVG is the icon; some sites ban emoji.
|
|
19
31
|
connectLabel: "Log ind for at redigere",
|
|
20
32
|
disconnectLabel: "Afbryd",
|
|
33
|
+
logoutLabel: "Log ud",
|
|
34
|
+
connectPrompt: false,
|
|
21
35
|
...options,
|
|
22
36
|
labels: { ...DEFAULT_LABELS, ...options.labels ?? {} }
|
|
23
37
|
};
|
|
@@ -26,16 +40,20 @@ async function initInlineEdit(options) {
|
|
|
26
40
|
if (typeof window === "undefined") return;
|
|
27
41
|
const resolved = resolveOptions(options);
|
|
28
42
|
uiLabels = resolved.labels;
|
|
29
|
-
captureTokenFromUrl(resolved);
|
|
43
|
+
const freshFromUrl = captureTokenFromUrl(resolved);
|
|
30
44
|
const enabled = await checkEnabled(resolved);
|
|
31
45
|
if (!enabled) return;
|
|
32
46
|
const token = getConnectedToken(options);
|
|
33
|
-
if (token) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
47
|
+
if (!token) {
|
|
48
|
+
if (resolved.connectPrompt) {
|
|
49
|
+
showConnectPrompt(resolved);
|
|
50
|
+
listenForTokenMessage(resolved);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
38
53
|
}
|
|
54
|
+
setupFields(token, resolved);
|
|
55
|
+
if (freshFromUrl) enterEditMode();
|
|
56
|
+
else showIdlePill();
|
|
39
57
|
}
|
|
40
58
|
function listenForTokenMessage(options) {
|
|
41
59
|
let cmsOrigin;
|
|
@@ -53,7 +71,8 @@ function listenForTokenMessage(options) {
|
|
|
53
71
|
if (document.querySelector("[data-cms-inline-edit-badge]")) return;
|
|
54
72
|
localStorage.setItem(options.storageKey, data.token);
|
|
55
73
|
document.querySelector("[data-cms-inline-edit-connect]")?.remove();
|
|
56
|
-
|
|
74
|
+
setupFields(data.token, options);
|
|
75
|
+
enterEditMode();
|
|
57
76
|
});
|
|
58
77
|
}
|
|
59
78
|
function getConnectedToken(options) {
|
|
@@ -79,10 +98,11 @@ function disconnect(options) {
|
|
|
79
98
|
function captureTokenFromUrl(options) {
|
|
80
99
|
const url = new URL(window.location.href);
|
|
81
100
|
const urlToken = url.searchParams.get(options.tokenParam);
|
|
82
|
-
if (!urlToken) return;
|
|
101
|
+
if (!urlToken) return false;
|
|
83
102
|
localStorage.setItem(options.storageKey, urlToken);
|
|
84
103
|
url.searchParams.delete(options.tokenParam);
|
|
85
104
|
window.history.replaceState({}, "", url.toString());
|
|
105
|
+
return true;
|
|
86
106
|
}
|
|
87
107
|
async function checkEnabled(options) {
|
|
88
108
|
try {
|
|
@@ -100,6 +120,8 @@ function isExpired(token) {
|
|
|
100
120
|
return exp <= Date.now() / 1e3;
|
|
101
121
|
}
|
|
102
122
|
var SQUARE_PEN_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>';
|
|
123
|
+
var UL_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/></svg>';
|
|
124
|
+
var OL_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="10" x2="21" y1="6" y2="6"/><line x1="10" x2="21" y1="12" y2="12"/><line x1="10" x2="21" y1="18" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/></svg>';
|
|
103
125
|
function makeIcon() {
|
|
104
126
|
const icon = document.createElement("span");
|
|
105
127
|
icon.style.cssText = "display:inline-flex;line-height:0;flex:0 0 auto";
|
|
@@ -126,11 +148,58 @@ function showConnectPrompt(options) {
|
|
|
126
148
|
});
|
|
127
149
|
document.body.appendChild(link);
|
|
128
150
|
}
|
|
129
|
-
|
|
151
|
+
var editingActive = false;
|
|
152
|
+
var fieldsWired = false;
|
|
153
|
+
var stateOptions = null;
|
|
154
|
+
function setupFields(token, options) {
|
|
155
|
+
stateOptions = options;
|
|
156
|
+
if (fieldsWired) return;
|
|
130
157
|
injectStyles();
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
158
|
+
document.querySelectorAll("[data-cms-field]").forEach((el) => wireField(el, token, options));
|
|
159
|
+
fieldsWired = true;
|
|
160
|
+
}
|
|
161
|
+
function enterEditMode() {
|
|
162
|
+
if (!stateOptions || editingActive) return;
|
|
163
|
+
editingActive = true;
|
|
164
|
+
removeIdlePill();
|
|
165
|
+
showActiveBadge(stateOptions);
|
|
166
|
+
}
|
|
167
|
+
function exitEditMode() {
|
|
168
|
+
if (!stateOptions || !editingActive) return;
|
|
169
|
+
deactivateRich();
|
|
170
|
+
editingActive = false;
|
|
171
|
+
document.querySelector("[data-cms-inline-edit-badge]")?.remove();
|
|
172
|
+
showIdlePill();
|
|
173
|
+
}
|
|
174
|
+
function removeIdlePill() {
|
|
175
|
+
document.querySelector("[data-cms-inline-edit-idle]")?.remove();
|
|
176
|
+
}
|
|
177
|
+
function showIdlePill() {
|
|
178
|
+
if (!stateOptions) return;
|
|
179
|
+
const options = stateOptions;
|
|
180
|
+
removeIdlePill();
|
|
181
|
+
const wrap = document.createElement("div");
|
|
182
|
+
wrap.setAttribute("data-cms-inline-edit-idle", "");
|
|
183
|
+
wrap.style.cssText = "position:fixed;bottom:16px;left:16px;display:inline-flex;align-items:stretch;z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);border-radius:999px;overflow:hidden;";
|
|
184
|
+
const edit = document.createElement("button");
|
|
185
|
+
edit.type = "button";
|
|
186
|
+
edit.setAttribute("data-cms-inline-edit-enter", "");
|
|
187
|
+
edit.style.cssText = "display:inline-flex;align-items:center;gap:7px;background:#1c2027;color:#fff;font:600 12px system-ui,sans-serif;padding:8px 14px;border:none;cursor:pointer;";
|
|
188
|
+
const editText = document.createElement("span");
|
|
189
|
+
editText.textContent = options.connectLabel;
|
|
190
|
+
edit.append(makeIcon(), editText);
|
|
191
|
+
edit.addEventListener("click", enterEditMode);
|
|
192
|
+
const out = document.createElement("button");
|
|
193
|
+
out.type = "button";
|
|
194
|
+
out.setAttribute("data-cms-inline-edit-logout", "");
|
|
195
|
+
out.textContent = options.logoutLabel;
|
|
196
|
+
out.style.cssText = "background:#141821;color:#9aa4b2;font:600 11px system-ui,sans-serif;padding:8px 12px;border:none;border-left:1px solid rgba(255,255,255,.12);cursor:pointer;";
|
|
197
|
+
out.addEventListener("click", () => {
|
|
198
|
+
disconnect(options);
|
|
199
|
+
removeIdlePill();
|
|
200
|
+
});
|
|
201
|
+
wrap.append(edit, out);
|
|
202
|
+
document.body.appendChild(wrap);
|
|
134
203
|
}
|
|
135
204
|
function wireField(el, token, options) {
|
|
136
205
|
if (el.dataset.cmsRichtext === "true") {
|
|
@@ -142,6 +211,7 @@ function wireField(el, token, options) {
|
|
|
142
211
|
return;
|
|
143
212
|
}
|
|
144
213
|
el.addEventListener("click", (e) => {
|
|
214
|
+
if (!editingActive) return;
|
|
145
215
|
if (el.getAttribute("contenteditable") === "true") return;
|
|
146
216
|
e.preventDefault();
|
|
147
217
|
e.stopPropagation();
|
|
@@ -172,6 +242,7 @@ var richCtx = null;
|
|
|
172
242
|
var richToolbar = null;
|
|
173
243
|
function wireRichField(el, token, options, mode) {
|
|
174
244
|
el.addEventListener("click", (e) => {
|
|
245
|
+
if (!editingActive) return;
|
|
175
246
|
if (richCtx && richCtx.el === el) return;
|
|
176
247
|
e.preventDefault();
|
|
177
248
|
e.stopPropagation();
|
|
@@ -236,6 +307,9 @@ function buildRichToolbar() {
|
|
|
236
307
|
return s;
|
|
237
308
|
};
|
|
238
309
|
t.appendChild(sep());
|
|
310
|
+
t.appendChild(toolbarButton(UL_SVG, uiLabels.unorderedList, () => document.execCommand("insertUnorderedList")));
|
|
311
|
+
t.appendChild(toolbarButton(OL_SVG, uiLabels.orderedList, () => document.execCommand("insertOrderedList")));
|
|
312
|
+
t.appendChild(sep());
|
|
239
313
|
const clrLabel = document.createElement("label");
|
|
240
314
|
clrLabel.style.cssText = "display:flex;align-items:center;gap:5px;color:#9aa4b2;font-size:12px;cursor:pointer;";
|
|
241
315
|
clrLabel.textContent = uiLabels.color;
|
|
@@ -485,6 +559,7 @@ async function saveField(el, value, token, options) {
|
|
|
485
559
|
const slug = el.dataset.cmsSlug;
|
|
486
560
|
const field = el.dataset.cmsField;
|
|
487
561
|
if (!collection || !slug || !field) return;
|
|
562
|
+
const slice = el.dataset.cmsSlice;
|
|
488
563
|
showPill(el, "saving");
|
|
489
564
|
try {
|
|
490
565
|
const getRes = await fetch(
|
|
@@ -494,7 +569,10 @@ async function saveField(el, value, token, options) {
|
|
|
494
569
|
if (!getRes.ok) throw new Error(`GET failed: ${getRes.status}`);
|
|
495
570
|
const doc = await getRes.json();
|
|
496
571
|
const mergedData = JSON.parse(JSON.stringify(doc.data ?? {}));
|
|
497
|
-
if (
|
|
572
|
+
if (slice !== void 0) {
|
|
573
|
+
const currentVal = typeof mergedData[field] === "string" ? mergedData[field] : "";
|
|
574
|
+
mergedData[field] = applyFieldSlice(currentVal, slice, value);
|
|
575
|
+
} else if (field.includes(".")) {
|
|
498
576
|
setDeepField(mergedData, field, value);
|
|
499
577
|
} else {
|
|
500
578
|
mergedData[field] = value;
|
|
@@ -508,6 +586,7 @@ async function saveField(el, value, token, options) {
|
|
|
508
586
|
}
|
|
509
587
|
);
|
|
510
588
|
if (!patchRes.ok) throw new Error(`PATCH failed: ${patchRes.status}`);
|
|
589
|
+
if (slice !== void 0) el.dataset.cmsSlice = value;
|
|
511
590
|
showPill(el, "saved");
|
|
512
591
|
} catch {
|
|
513
592
|
showPill(el, "error");
|
|
@@ -552,10 +631,7 @@ function showActiveBadge(options) {
|
|
|
552
631
|
badge.append(makeIcon(), text);
|
|
553
632
|
badge.addEventListener("mouseenter", () => badge.style.opacity = "1");
|
|
554
633
|
badge.addEventListener("mouseleave", () => badge.style.opacity = ".9");
|
|
555
|
-
badge.addEventListener("click",
|
|
556
|
-
disconnect(options);
|
|
557
|
-
window.location.reload();
|
|
558
|
-
});
|
|
634
|
+
badge.addEventListener("click", exitEditMode);
|
|
559
635
|
document.body.appendChild(badge);
|
|
560
636
|
}
|
|
561
637
|
function decodeJwtPayload(token) {
|
|
@@ -579,6 +655,7 @@ function injectStyles() {
|
|
|
579
655
|
document.head.appendChild(style);
|
|
580
656
|
}
|
|
581
657
|
export {
|
|
658
|
+
applyFieldSlice,
|
|
582
659
|
buildConnectUrl,
|
|
583
660
|
disconnect,
|
|
584
661
|
getConnectedToken,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Browser entry — zero dependencies. Site-wide: activated once per browser\n * via a \"connect\" link (see docs/features/F157-inline-editing.md), then\n * every page load on the site automatically offers click-to-edit on any\n * element carrying data-cms-collection/data-cms-slug/data-cms-field\n * (F129's attribute convention) — no per-document step.\n */\n\n/**\n * Labels for the in-editor UI — the rich-text toolbar (bold/italic/underline\n * tooltips, the visible \"colour\" + \"done\" buttons, the emoji tooltip) and the\n * save-status pill. Every field is optional; unset fields keep the Danish\n * default. Pass an English (or any-locale) set on a non-Danish site so the\n * toolbar and status text match the page language.\n */\nexport interface InlineEditLabels {\n bold?: string;\n italic?: string;\n underline?: string;\n color?: string;\n emoji?: string;\n done?: string;\n saving?: string;\n saved?: string;\n error?: string;\n}\n\nexport interface InlineEditOptions {\n /** Base URL of the CMS API, e.g. \"https://webhouse.app\". */\n cmsBaseUrl: string;\n /** Site id passed as ?site= on every CMS API call, e.g. \"broberg-ai\". */\n siteId: string;\n /** URL query param carrying a freshly-minted token from the connect redirect. Default \"cms_edit\". */\n tokenParam?: string;\n /** localStorage key the token is persisted under — survives across sessions/tabs. Default \"wh-inline-edit-token\". */\n storageKey?: string;\n /** Text for the \"not connected yet\" prompt (a square-pen icon is prepended). Default \"Log ind for at redigere\". */\n connectLabel?: string;\n /** Text for the \"connected\" badge — the whole pill is the exit-edit action (icon prepended). Default \"Afbryd\". */\n disconnectLabel?: string;\n /** Localised labels for the toolbar + save-status pill. Defaults are Danish. */\n labels?: InlineEditLabels;\n}\n\ninterface ResolvedOptions extends Required<Omit<InlineEditOptions, \"labels\">> {\n labels: Required<InlineEditLabels>;\n}\n\nconst DEFAULT_LABELS: Required<InlineEditLabels> = {\n bold: \"Fed\",\n italic: \"Kursiv\",\n underline: \"Understreget\",\n color: \"Farve\",\n emoji: \"Indsæt emoji\",\n done: \"Færdig\",\n saving: \"Gemmer…\",\n saved: \"Gemt ✓\",\n error: \"Fejl — prøv igen\",\n};\n\n// Set once from resolved options in initInlineEdit(); read by the (singleton)\n// toolbar and the save-status pill so their signatures stay unchanged. One edit\n// session per page → a single module-level value is safe.\nlet uiLabels: Required<InlineEditLabels> = DEFAULT_LABELS;\n\nfunction resolveOptions(options: InlineEditOptions): ResolvedOptions {\n return {\n tokenParam: \"cms_edit\",\n storageKey: \"wh-inline-edit-token\",\n // No emoji in defaults — the square-pen SVG is the icon; some sites ban emoji.\n connectLabel: \"Log ind for at redigere\",\n disconnectLabel: \"Afbryd\",\n ...options,\n labels: { ...DEFAULT_LABELS, ...(options.labels ?? {}) },\n };\n}\n\nexport async function initInlineEdit(options: InlineEditOptions): Promise<void> {\n if (typeof window === \"undefined\") return;\n const resolved = resolveOptions(options);\n uiLabels = resolved.labels;\n\n captureTokenFromUrl(resolved);\n\n const enabled = await checkEnabled(resolved);\n if (!enabled) return;\n\n const token = getConnectedToken(options);\n if (token) {\n activateEditMode(token, resolved);\n } else {\n showConnectPrompt(resolved);\n // F158 — the connect flow opens in a popup; it posts the freshly-minted\n // token back to this tab. Arm the (origin-validated) receiver.\n listenForTokenMessage(resolved);\n }\n}\n\n/**\n * F158 — receive the edit token from the connect popup via postMessage.\n * Hard origin check (message MUST come from cmsBaseUrl) + type + site + expiry\n * guards before the token is trusted. On success: persist, swap the connect\n * prompt for edit mode — no page reload.\n */\nfunction listenForTokenMessage(options: ResolvedOptions): void {\n let cmsOrigin: string;\n try {\n cmsOrigin = new URL(options.cmsBaseUrl).origin;\n } catch {\n return;\n }\n window.addEventListener(\"message\", (event: MessageEvent) => {\n if (event.origin !== cmsOrigin) return;\n const data = event.data as { type?: string; token?: string; site?: string } | null;\n if (!data || data.type !== \"wh-inline-edit-token\" || typeof data.token !== \"string\") return;\n if (data.site && data.site !== options.siteId) return;\n if (isExpired(data.token)) return;\n if (document.querySelector(\"[data-cms-inline-edit-badge]\")) return; // already active\n localStorage.setItem(options.storageKey, data.token);\n document.querySelector(\"[data-cms-inline-edit-connect]\")?.remove();\n activateEditMode(data.token, options);\n });\n}\n\n/**\n * A valid (non-expired) token already connected in this browser for this\n * site, or null. Also captures a fresh `?cms_edit=` token from the URL\n * first, so a page can call this directly right after the connect redirect\n * without going through initInlineEdit(). Exposed so a site's own /admin\n * page (or any other tool built on the same connected session) can reuse\n * the same storage/expiry logic instead of re-implementing it.\n */\nexport function getConnectedToken(options: InlineEditOptions): string | null {\n if (typeof window === \"undefined\") return null;\n const resolved = resolveOptions(options);\n captureTokenFromUrl(resolved);\n const token = localStorage.getItem(resolved.storageKey);\n if (!token) return null;\n if (isExpired(token)) {\n localStorage.removeItem(resolved.storageKey);\n return null;\n }\n return token;\n}\n\n/** The URL that mints a fresh 30-day site-scoped token and redirects back to `returnUrl`. */\nexport function buildConnectUrl(options: InlineEditOptions, returnUrl: string): string {\n const resolved = resolveOptions(options);\n return (\n `${resolved.cmsBaseUrl}/admin/inline-edit/connect?site=${encodeURIComponent(resolved.siteId)}` +\n `&return=${encodeURIComponent(returnUrl)}`\n );\n}\n\n/** Clears the connected token in this browser (e.g. a \"log out\" action in a site's own /admin panel). */\nexport function disconnect(options: InlineEditOptions): void {\n if (typeof window === \"undefined\") return;\n localStorage.removeItem(resolveOptions(options).storageKey);\n}\n\n/** Captures a token minted by /admin/inline-edit/connect, then strips it from the URL. */\nfunction captureTokenFromUrl(options: ResolvedOptions): void {\n const url = new URL(window.location.href);\n const urlToken = url.searchParams.get(options.tokenParam);\n if (!urlToken) return;\n localStorage.setItem(options.storageKey, urlToken);\n url.searchParams.delete(options.tokenParam);\n window.history.replaceState({}, \"\", url.toString());\n}\n\nasync function checkEnabled(options: ResolvedOptions): Promise<boolean> {\n try {\n const res = await fetch(`${options.cmsBaseUrl}/api/inline-edit/status?site=${options.siteId}`);\n if (!res.ok) return false;\n const body = (await res.json()) as { enabled?: boolean };\n return body.enabled === true;\n } catch {\n return false;\n }\n}\n\nfunction isExpired(token: string): boolean {\n const claims = decodeJwtPayload(token);\n const exp = typeof claims?.exp === \"number\" ? claims.exp : 0;\n return exp <= Date.now() / 1000;\n}\n\n// Lucide \"square-pen\" (https://lucide.dev/icons/square-pen), MIT. Inline SVG so\n// the pill needs no icon-font/runtime dep and inherits `currentColor`. Emoji are\n// deliberately avoided — some consumer sites ban them outright.\nconst SQUARE_PEN_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<path d=\"M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\"/>' +\n '<path d=\"M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z\"/>' +\n \"</svg>\";\n\nfunction makeIcon(): HTMLSpanElement {\n const icon = document.createElement(\"span\");\n icon.style.cssText = \"display:inline-flex;line-height:0;flex:0 0 auto\";\n icon.innerHTML = SQUARE_PEN_SVG;\n return icon;\n}\n\nfunction showConnectPrompt(options: ResolvedOptions): void {\n const connectUrl = buildConnectUrl(options, window.location.href);\n\n const link = document.createElement(\"a\");\n link.href = connectUrl;\n link.setAttribute(\"data-cms-inline-edit-connect\", \"\");\n link.style.cssText =\n \"position:fixed;bottom:16px;left:16px;background:#1c2027;color:#fff;\" +\n \"font:600 12px system-ui,sans-serif;padding:8px 14px;border-radius:999px;\" +\n \"text-decoration:none;z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);\" +\n \"display:inline-flex;align-items:center;gap:7px;\";\n const text = document.createElement(\"span\");\n text.textContent = options.connectLabel;\n link.append(makeIcon(), text);\n // F158 — open the connect flow in a popup so the CMS confirmation screen can\n // say \"close this window\". A user-gesture window.open is not popup-blocked;\n // if it is blocked anyway, fall back to the same-tab redirect (F157 path).\n link.addEventListener(\"click\", (e) => {\n e.preventDefault();\n const popup = window.open(\n connectUrl,\n \"wh-inline-edit-connect\",\n \"width=460,height=640,menubar=no,toolbar=no,location=yes,status=no\",\n );\n if (!popup) window.location.href = connectUrl;\n });\n document.body.appendChild(link);\n}\n\nfunction activateEditMode(token: string, options: ResolvedOptions): void {\n injectStyles();\n showActiveBadge(options);\n\n const fields = document.querySelectorAll<HTMLElement>(\"[data-cms-field]\");\n fields.forEach((el) => wireField(el, token, options));\n}\n\nfunction wireField(el: HTMLElement, token: string, options: ResolvedOptions): void {\n // Rich fields get the floating formatting toolbar. Two save modes:\n // - data-cms-richtext=\"true\" → save as Markdown (article bodies; cms\n // `richtext` contract renders Markdown via marked).\n // - data-cms-html=\"true\" → save innerHTML VERBATIM (intentional-HTML\n // fields: headings/bios/hero with a branded <em class=\"o\"> accent that\n // has no Markdown equivalent — converting would strip it).\n // Everything else stays a plain single-line contenteditable saving textContent.\n if (el.dataset.cmsRichtext === \"true\") {\n wireRichField(el, token, options, \"markdown\");\n return;\n }\n if (el.dataset.cmsHtml === \"true\") {\n wireRichField(el, token, options, \"html\");\n return;\n }\n\n el.addEventListener(\"click\", (e) => {\n if (el.getAttribute(\"contenteditable\") === \"true\") return;\n // Many editable fields (card titles/blurbs) sit inside a clickable <a>/\n // <button> ancestor (the card itself) — without this, \"click to edit\"\n // would immediately navigate away instead of focusing the field.\n e.preventDefault();\n e.stopPropagation();\n el.dataset.cmsOriginalValue = el.textContent ?? \"\";\n el.setAttribute(\"contenteditable\", \"true\");\n el.focus();\n });\n\n el.addEventListener(\"blur\", () => {\n el.removeAttribute(\"contenteditable\");\n const original = el.dataset.cmsOriginalValue ?? \"\";\n const current = el.textContent ?? \"\";\n if (current.trim() === original.trim()) return;\n void saveField(el, current.trim(), token, options);\n });\n\n el.addEventListener(\"paste\", (e) => {\n e.preventDefault();\n const text = e.clipboardData?.getData(\"text/plain\") ?? \"\";\n document.execCommand(\"insertText\", false, text);\n });\n\n el.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n el.blur();\n }\n });\n}\n\n/* ─── Rich-text mode (article bodies) ──────────────────────────────────────\n * A single editable region + a floating B/I/U · color · emoji · Done toolbar\n * (the Pitch Vault pattern, minus the iframe/postMessage — we run in the live\n * page's own document). On \"Done\" the region's innerHTML is converted back to\n * Markdown (the cms `richtext` contract: fields store Markdown, not HTML) and\n * saved. Only ONE region is active at a time. */\n\ntype RichMode = \"markdown\" | \"html\";\ninterface RichContext {\n el: HTMLElement;\n token: string;\n options: ResolvedOptions;\n originalHtml: string;\n mode: RichMode;\n}\nlet richCtx: RichContext | null = null;\nlet richToolbar: HTMLElement | null = null;\n\nfunction wireRichField(el: HTMLElement, token: string, options: ResolvedOptions, mode: RichMode): void {\n el.addEventListener(\"click\", (e) => {\n if (richCtx && richCtx.el === el) return; // already editing this region\n e.preventDefault();\n e.stopPropagation();\n activateRich(el, token, options, mode);\n });\n}\n\nfunction activateRich(el: HTMLElement, token: string, options: ResolvedOptions, mode: RichMode): void {\n deactivateRich(); // commit any previously-active region first\n richCtx = { el, token, options, originalHtml: el.innerHTML, mode };\n el.setAttribute(\"contenteditable\", \"true\");\n el.classList.add(\"cms-rich-editing\");\n el.focus();\n showRichToolbar();\n // Let the host page react (e.g. pause a rotating hero carousel while editing).\n document.dispatchEvent(new CustomEvent(\"cms-inline-edit:activate\", { detail: { el } }));\n}\n\nfunction deactivateRich(): void {\n if (!richCtx) return;\n const { el, token, options, originalHtml, mode } = richCtx;\n richCtx = null;\n el.removeAttribute(\"contenteditable\");\n el.classList.remove(\"cms-rich-editing\");\n hideRichToolbar();\n if (el.innerHTML !== originalHtml) {\n const value = mode === \"html\" ? el.innerHTML : htmlToMarkdown(el.innerHTML);\n void saveField(el, value, token, options);\n }\n document.dispatchEvent(new CustomEvent(\"cms-inline-edit:deactivate\", { detail: { el } }));\n}\n\nfunction showRichToolbar(): void {\n if (!richToolbar) richToolbar = buildRichToolbar();\n richToolbar.style.display = \"flex\";\n}\n\nfunction hideRichToolbar(): void {\n if (richToolbar) richToolbar.style.display = \"none\";\n hideEmojiPicker();\n}\n\nfunction toolbarButton(label: string, title: string, onDown: () => void): HTMLButtonElement {\n const b = document.createElement(\"button\");\n b.type = \"button\";\n b.innerHTML = label;\n b.title = title;\n b.style.cssText =\n \"background:none;border:1px solid #3a3f4a;color:#fff;min-width:30px;height:30px;\" +\n \"border-radius:6px;cursor:pointer;font-size:14px;padding:0 8px;line-height:1;\";\n b.addEventListener(\"mouseenter\", () => (b.style.background = \"#2a2f38\"));\n b.addEventListener(\"mouseleave\", () => (b.style.background = \"none\"));\n // mousedown + preventDefault so the contenteditable selection isn't lost.\n b.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n onDown();\n });\n return b;\n}\n\nfunction buildRichToolbar(): HTMLElement {\n const t = document.createElement(\"div\");\n t.setAttribute(\"data-cms-inline-edit-toolbar\", \"\");\n t.style.cssText =\n \"position:fixed;top:14px;left:50%;transform:translateX(-50%);\" +\n \"background:#1c2027;border:1px solid #3a3f4a;border-radius:10px;\" +\n \"padding:6px 10px;display:flex;gap:6px;align-items:center;\" +\n \"z-index:2147483647;box-shadow:0 8px 32px rgba(0,0,0,.5);\" +\n \"font-family:system-ui,sans-serif;\";\n\n t.appendChild(toolbarButton(\"<b>B</b>\", uiLabels.bold, () => document.execCommand(\"bold\")));\n t.appendChild(toolbarButton(\"<i>I</i>\", uiLabels.italic, () => document.execCommand(\"italic\")));\n t.appendChild(toolbarButton(\"<u>U</u>\", uiLabels.underline, () => document.execCommand(\"underline\")));\n\n const sep = () => {\n const s = document.createElement(\"div\");\n s.style.cssText = \"width:1px;height:20px;background:#3a3f4a;\";\n return s;\n };\n t.appendChild(sep());\n\n // Text color — execCommand foreColor applies to the current selection.\n const clrLabel = document.createElement(\"label\");\n clrLabel.style.cssText = \"display:flex;align-items:center;gap:5px;color:#9aa4b2;font-size:12px;cursor:pointer;\";\n clrLabel.textContent = uiLabels.color;\n const clr = document.createElement(\"input\");\n clr.type = \"color\";\n clr.style.cssText = \"width:28px;height:24px;border:1px solid #3a3f4a;border-radius:5px;cursor:pointer;padding:1px;background:none;\";\n clr.addEventListener(\"mousedown\", (e) => e.stopPropagation());\n clr.addEventListener(\"input\", () => document.execCommand(\"foreColor\", false, clr.value));\n clrLabel.prepend(clr);\n t.appendChild(clrLabel);\n\n t.appendChild(sep());\n\n const emojiBtn = toolbarButton(\"😀\", uiLabels.emoji, () => toggleEmojiPicker(emojiBtn));\n emojiBtn.style.fontSize = \"16px\";\n t.appendChild(emojiBtn);\n\n t.appendChild(sep());\n\n const done = document.createElement(\"button\");\n done.type = \"button\";\n done.textContent = uiLabels.done;\n done.style.cssText =\n \"background:#00b2ff;border:none;color:#04121c;padding:0 14px;height:30px;\" +\n \"border-radius:6px;cursor:pointer;font-size:12px;font-weight:700;\";\n done.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n deactivateRich();\n });\n t.appendChild(done);\n\n document.body.appendChild(t);\n return t;\n}\n\n/* ─── emoji picker (compact) ────────────────────────────────────────────── */\nconst EMOJIS =\n \"😀 😃 😄 😁 😆 😉 😊 😍 🤩 😎 🤔 🙌 👏 👍 👎 🙏 💪 🔥 ⚡ ✨ 🎉 ✅ ❌ 💯 ⭐ 🚀 💡 📈 📉 🧠 ❤️ 🧡 💛 💚 💙 💜 🇩🇰 🇪🇺\".split(\n \" \",\n );\nlet emojiPicker: HTMLElement | null = null;\nlet savedRange: Range | null = null;\n\nfunction toggleEmojiPicker(anchor: HTMLElement): void {\n if (!emojiPicker) emojiPicker = buildEmojiPicker();\n if (emojiPicker.style.display === \"block\") {\n hideEmojiPicker();\n return;\n }\n const sel = window.getSelection();\n if (sel && sel.rangeCount > 0) savedRange = sel.getRangeAt(0).cloneRange();\n const rect = anchor.getBoundingClientRect();\n emojiPicker.style.top = `${rect.bottom + 6}px`;\n emojiPicker.style.left = `${Math.min(rect.left, window.innerWidth - 240)}px`;\n emojiPicker.style.display = \"block\";\n}\n\nfunction hideEmojiPicker(): void {\n if (emojiPicker) emojiPicker.style.display = \"none\";\n}\n\nfunction buildEmojiPicker(): HTMLElement {\n const p = document.createElement(\"div\");\n p.setAttribute(\"data-cms-inline-edit-toolbar\", \"\");\n p.style.cssText =\n \"position:fixed;z-index:2147483646;background:#1c2027;border:1px solid #3a3f4a;\" +\n \"border-radius:10px;padding:8px;width:230px;max-height:200px;overflow-y:auto;\" +\n \"display:none;box-shadow:0 8px 32px rgba(0,0,0,.5);\";\n const grid = document.createElement(\"div\");\n grid.style.cssText = \"display:flex;flex-wrap:wrap;gap:2px;\";\n EMOJIS.forEach((em) => {\n const b = document.createElement(\"button\");\n b.type = \"button\";\n b.textContent = em;\n b.style.cssText =\n \"background:none;border:none;cursor:pointer;border-radius:5px;width:32px;height:32px;font-size:18px;\";\n b.addEventListener(\"mouseenter\", () => (b.style.background = \"#2a2f38\"));\n b.addEventListener(\"mouseleave\", () => (b.style.background = \"none\"));\n b.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n insertEmoji(em);\n });\n grid.appendChild(b);\n });\n p.appendChild(grid);\n document.body.appendChild(p);\n return p;\n}\n\nfunction insertEmoji(emoji: string): void {\n if (!richCtx) return;\n richCtx.el.focus();\n const sel = window.getSelection();\n if (savedRange && sel) {\n sel.removeAllRanges();\n sel.addRange(savedRange);\n }\n document.execCommand(\"insertText\", false, emoji);\n savedRange = null;\n hideEmojiPicker();\n}\n\n// Click outside the active region (and outside the toolbar) commits the edit.\nif (typeof document !== \"undefined\") {\n document.addEventListener(\n \"mousedown\",\n (e) => {\n if (!richCtx) return;\n const target = e.target as HTMLElement | null;\n if (!target) return;\n if (target.closest(\"[data-cms-inline-edit-toolbar]\")) return;\n if (richCtx.el.contains(target)) return;\n deactivateRich();\n },\n true,\n );\n}\n\n/**\n * Sets a value at a dot-path into a plain-data tree, where a numeric segment\n * indexes into an array (e.g. \"slides.2.eyebrow\" — flagship-style nested\n * content; a flat \"heroEyebrow\" is still just a 1-segment path). Bails\n * silently (no throw) if the path doesn't resolve — a stale/malformed path\n * must never crash the save, just fail to apply.\n */\nfunction setDeepField(data: Record<string, unknown>, path: string, value: string): void {\n const parts = path.split(\".\");\n let obj: any = data;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i] ?? \"\";\n const key: string | number = /^\\d+$/.test(part) ? Number(part) : part;\n if (obj == null || typeof obj !== \"object\" || obj[key] === undefined) return;\n obj = obj[key];\n }\n const last = parts[parts.length - 1] ?? \"\";\n const lastKey: string | number = /^\\d+$/.test(last) ? Number(last) : last;\n if (obj == null || typeof obj !== \"object\") return;\n obj[lastKey] = value;\n}\n\n/**\n * Converts an edited contenteditable region's innerHTML back to Markdown so a\n * rich article body round-trips through the cms `richtext` contract (fields\n * store Markdown, `marked` renders it — NOT HTML). Handles the structures a\n * marked-rendered body + the toolbar produce: headings, paragraphs, emphasis,\n * links, lists, blockquote, code. Formatting with no clean Markdown equivalent\n * (underline, coloured spans) is passed through as inline HTML — `marked`\n * renders inline HTML, so it survives without corrupting the source. Tables and\n * anything unrecognised are passed through as their outerHTML for the same\n * reason: never drop content just because it doesn't map to a Markdown token.\n */\nfunction htmlToMarkdown(html: string): string {\n const container = document.createElement(\"div\");\n container.innerHTML = html;\n return serializeBlockChildren(container).replace(/\\n{3,}/g, \"\\n\\n\").trim() + \"\\n\";\n}\n\nfunction serializeBlockChildren(parent: Node): string {\n const blocks: string[] = [];\n parent.childNodes.forEach((node) => {\n const s = serializeBlock(node).trim();\n if (s) blocks.push(s);\n });\n return blocks.join(\"\\n\\n\");\n}\n\nfunction serializeBlock(node: Node): string {\n if (node.nodeType === Node.TEXT_NODE) return (node.textContent ?? \"\").trim();\n if (node.nodeType !== Node.ELEMENT_NODE) return \"\";\n const el = node as HTMLElement;\n const tag = el.tagName.toLowerCase();\n switch (tag) {\n case \"h1\": return \"# \" + serializeInline(el);\n case \"h2\": return \"## \" + serializeInline(el);\n case \"h3\": return \"### \" + serializeInline(el);\n case \"h4\": return \"#### \" + serializeInline(el);\n case \"h5\": return \"##### \" + serializeInline(el);\n case \"h6\": return \"###### \" + serializeInline(el);\n case \"p\":\n case \"div\":\n return serializeInline(el);\n case \"ul\":\n return serializeList(el, false);\n case \"ol\":\n return serializeList(el, true);\n case \"blockquote\":\n // Serialize the quote's OWN block children (it usually wraps <p>s), then\n // prefix each line with \"> \" — treating it as inline dropped the <p> and\n // left a stray leading space.\n return serializeBlockChildren(el)\n .split(\"\\n\")\n .map((l) => (l ? \"> \" + l : \">\"))\n .join(\"\\n\");\n case \"pre\":\n return \"```\\n\" + (el.textContent ?? \"\").replace(/\\n+$/, \"\") + \"\\n```\";\n case \"hr\":\n return \"---\";\n case \"br\":\n return \"\";\n case \"table\":\n // Rebuild the Markdown pipe table (NOT raw-HTML passthrough) so the\n // consumer's marked renderer re-applies its own table treatment (e.g.\n // broberg's mobile scroll-wrapper) instead of getting a bare <table>.\n return serializeTable(el);\n default:\n return serializeInline(el);\n }\n}\n\nfunction serializeTable(table: HTMLElement): string {\n const cellText = (c: Element) => serializeInline(c).trim().replace(/\\|/g, \"\\\\|\");\n const rows: string[] = [];\n const headCells = Array.from(table.querySelectorAll(\"thead tr\")).flatMap((tr) =>\n Array.from(tr.children).map(cellText),\n );\n if (headCells.length) {\n rows.push(\"| \" + headCells.join(\" | \") + \" |\");\n rows.push(\"| \" + headCells.map(() => \"---\").join(\" | \") + \" |\");\n }\n const bodyRows = table.querySelector(\"tbody\")\n ? Array.from(table.querySelectorAll(\"tbody tr\"))\n : Array.from(table.querySelectorAll(\"tr\")).filter((tr) => !tr.closest(\"thead\"));\n bodyRows.forEach((tr) => {\n rows.push(\"| \" + Array.from(tr.children).map(cellText).join(\" | \") + \" |\");\n });\n return rows.join(\"\\n\");\n}\n\nfunction serializeList(listEl: HTMLElement, ordered: boolean): string {\n const items: string[] = [];\n let n = 1;\n listEl.childNodes.forEach((child) => {\n if (child.nodeType === Node.ELEMENT_NODE && (child as HTMLElement).tagName.toLowerCase() === \"li\") {\n const marker = ordered ? `${n++}. ` : \"- \";\n items.push(marker + serializeInline(child as HTMLElement).trim());\n }\n });\n return items.join(\"\\n\");\n}\n\nfunction serializeInline(node: Node): string {\n let out = \"\";\n node.childNodes.forEach((child) => {\n if (child.nodeType === Node.TEXT_NODE) {\n out += (child.textContent ?? \"\").replace(/\\s+/g, \" \");\n return;\n }\n if (child.nodeType !== Node.ELEMENT_NODE) return;\n const el = child as HTMLElement;\n const tag = el.tagName.toLowerCase();\n const inner = serializeInline(el);\n switch (tag) {\n case \"strong\":\n case \"b\":\n out += inner.trim() ? `**${inner.trim()}**` : \"\";\n break;\n case \"em\":\n case \"i\":\n out += inner.trim() ? `*${inner.trim()}*` : \"\";\n break;\n case \"code\":\n out += \"`\" + inner + \"`\";\n break;\n case \"br\":\n out += \" \\n\";\n break;\n case \"a\": {\n const href = el.getAttribute(\"href\") || \"\";\n out += href ? `[${inner}](${href})` : inner;\n break;\n }\n case \"img\": {\n // Content images embedded in the body () — dropping them\n // would silently delete images from an article on save. Void element,\n // no children, so this must be handled explicitly.\n const src = el.getAttribute(\"src\") || \"\";\n const alt = el.getAttribute(\"alt\") || \"\";\n if (src) out += ``;\n break;\n }\n case \"u\":\n out += `<u>${inner}</u>`; // no Markdown for underline — pass through\n break;\n case \"span\":\n case \"font\": {\n const color = el.style.color || el.getAttribute(\"color\") || \"\";\n out += color ? `<span style=\"color:${color}\">${inner}</span>` : inner;\n break;\n }\n default:\n out += inner;\n }\n });\n return out;\n}\n\nasync function saveField(el: HTMLElement, value: string, token: string, options: ResolvedOptions): Promise<void> {\n const collection = el.dataset.cmsCollection;\n const slug = el.dataset.cmsSlug;\n const field = el.dataset.cmsField;\n if (!collection || !slug || !field) return;\n\n showPill(el, \"saving\");\n try {\n const getRes = await fetch(\n `${options.cmsBaseUrl}/api/cms/${collection}/${slug}?site=${options.siteId}`,\n { headers: { Authorization: `Bearer ${token}` } },\n );\n if (!getRes.ok) throw new Error(`GET failed: ${getRes.status}`);\n const doc = (await getRes.json()) as { data?: Record<string, unknown> };\n // Deep-clone so mutating a nested array/object (dot-path saves) never\n // aliases the fetched doc — same safety whether field is flat or nested.\n const mergedData = JSON.parse(JSON.stringify(doc.data ?? {})) as Record<string, unknown>;\n if (field.includes(\".\")) {\n setDeepField(mergedData, field, value);\n } else {\n mergedData[field] = value;\n }\n\n const patchRes = await fetch(\n `${options.cmsBaseUrl}/api/cms/${collection}/${slug}?site=${options.siteId}`,\n {\n method: \"PATCH\",\n headers: { Authorization: `Bearer ${token}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ data: mergedData }),\n },\n );\n if (!patchRes.ok) throw new Error(`PATCH failed: ${patchRes.status}`);\n showPill(el, \"saved\");\n } catch {\n showPill(el, \"error\");\n }\n}\n\nconst pills = new WeakMap<HTMLElement, HTMLElement>();\n\nfunction showPill(el: HTMLElement, state: \"saving\" | \"saved\" | \"error\"): void {\n let pill = pills.get(el);\n if (!pill) {\n pill = document.createElement(\"span\");\n pill.setAttribute(\"data-cms-inline-edit-pill\", \"\");\n document.body.appendChild(pill);\n pills.set(el, pill);\n }\n const rect = el.getBoundingClientRect();\n pill.style.cssText =\n `position:fixed;top:${rect.top - 26}px;left:${rect.left}px;font:600 11px system-ui,sans-serif;` +\n `padding:3px 9px;border-radius:5px;z-index:2147483647;pointer-events:none;` +\n `box-shadow:0 2px 8px rgba(0,0,0,.25);`;\n\n if (state === \"saving\") {\n pill.textContent = uiLabels.saving;\n pill.style.background = \"#1c2027\";\n pill.style.color = \"#fff\";\n } else if (state === \"saved\") {\n pill.textContent = uiLabels.saved;\n pill.style.background = \"#16a34a\";\n pill.style.color = \"#fff\";\n setTimeout(() => {\n pill?.remove();\n pills.delete(el);\n }, 1500);\n } else {\n pill.textContent = uiLabels.error;\n pill.style.background = \"#dc2626\";\n pill.style.color = \"#fff\";\n }\n}\n\nfunction showActiveBadge(options: ResolvedOptions): void {\n // The connected badge IS the exit-edit action: [icon] \"Afslut redigering\".\n // No \"Redigerer som {name}\" — the whole pill is one click to leave edit mode.\n const badge = document.createElement(\"button\");\n badge.type = \"button\";\n badge.setAttribute(\"data-cms-inline-edit-badge\", \"\");\n badge.style.cssText =\n \"position:fixed;bottom:16px;left:16px;display:inline-flex;align-items:center;gap:7px;\" +\n \"background:#1c2027;color:#fff;font:600 12px system-ui,sans-serif;padding:8px 14px;\" +\n \"border-radius:999px;z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);\" +\n \"border:none;cursor:pointer;opacity:.9;transition:opacity .15s;\";\n const text = document.createElement(\"span\");\n text.textContent = options.disconnectLabel;\n badge.append(makeIcon(), text);\n badge.addEventListener(\"mouseenter\", () => (badge.style.opacity = \"1\"));\n badge.addEventListener(\"mouseleave\", () => (badge.style.opacity = \".9\"));\n badge.addEventListener(\"click\", () => {\n disconnect(options);\n window.location.reload();\n });\n document.body.appendChild(badge);\n}\n\n/** Reads JWT claims for DISPLAY ONLY — never used for authorization decisions. */\nfunction decodeJwtPayload(token: string): Record<string, unknown> | null {\n try {\n const payload = token.split(\".\")[1];\n if (!payload) return null;\n return JSON.parse(atob(payload.replace(/-/g, \"+\").replace(/_/g, \"/\")));\n } catch {\n return null;\n }\n}\n\nfunction injectStyles(): void {\n const style = document.createElement(\"style\");\n style.textContent = `\n [data-cms-field] { outline: 1px dashed transparent; outline-offset: 2px; cursor: text; transition: outline-color .15s; }\n [data-cms-field]:hover { outline-color: rgba(0,178,255,.5); }\n [data-cms-field][contenteditable=\"true\"] { outline: 2px solid #00b2ff; outline-offset: 2px; }\n [data-cms-field][data-cms-richtext=\"true\"] { cursor: text; }\n .cms-rich-editing { outline: 2px solid #00b2ff !important; outline-offset: 6px; border-radius: 4px; }\n `;\n document.head.appendChild(style);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/field-slice.ts","../src/index.ts"],"sourcesContent":["/**\n * Replace ONE slice of a larger text field.\n *\n * Used when an element renders a single SEGMENT of a bigger field — e.g. one\n * prose paragraph of a `content` body that is interleaved with `[block:]`\n * embeds — so editing that segment must write back into the full field without\n * clobbering the other segments or the embeds.\n *\n * Safety-first: throws if `original` is not found EXACTLY once, so an ambiguous\n * or stale slice ABORTS the save (surfaced as the error pill) instead of\n * corrupting the whole field. Pure + framework-agnostic; unit-tested.\n */\nexport function applyFieldSlice(current: string, original: string, next: string): string {\n const first = current.indexOf(original);\n if (first === -1) throw new Error(\"field-slice: original not found\");\n if (current.indexOf(original, first + original.length) !== -1) {\n throw new Error(\"field-slice: original found more than once (ambiguous)\");\n }\n return current.slice(0, first) + next + current.slice(first + original.length);\n}\n","/**\n * Browser entry — zero dependencies. Site-wide: activated once per browser\n * via a \"connect\" link (see docs/features/F157-inline-editing.md), then\n * every page load on the site automatically offers click-to-edit on any\n * element carrying data-cms-collection/data-cms-slug/data-cms-field\n * (F129's attribute convention) — no per-document step.\n */\nimport { applyFieldSlice } from \"./field-slice\";\nexport { applyFieldSlice } from \"./field-slice\";\n\n/**\n * Labels for the in-editor UI — the rich-text toolbar (bold/italic/underline\n * tooltips, the visible \"colour\" + \"done\" buttons, the emoji tooltip) and the\n * save-status pill. Every field is optional; unset fields keep the Danish\n * default. Pass an English (or any-locale) set on a non-Danish site so the\n * toolbar and status text match the page language.\n */\nexport interface InlineEditLabels {\n bold?: string;\n italic?: string;\n underline?: string;\n orderedList?: string;\n unorderedList?: string;\n color?: string;\n emoji?: string;\n done?: string;\n saving?: string;\n saved?: string;\n error?: string;\n}\n\nexport interface InlineEditOptions {\n /** Base URL of the CMS API, e.g. \"https://webhouse.app\". */\n cmsBaseUrl: string;\n /** Site id passed as ?site= on every CMS API call, e.g. \"broberg-ai\". */\n siteId: string;\n /** URL query param carrying a freshly-minted token from the connect redirect. Default \"cms_edit\". */\n tokenParam?: string;\n /** localStorage key the token is persisted under — survives across sessions/tabs. Default \"wh-inline-edit-token\". */\n storageKey?: string;\n /** Text for the \"not connected yet\" prompt (a square-pen icon is prepended). Default \"Log ind for at redigere\". */\n connectLabel?: string;\n /** Text for the \"connected + editing\" badge — clicking it LEAVES edit mode but keeps the login (icon prepended). Default \"Afbryd\". */\n disconnectLabel?: string;\n /** Text for the explicit \"log out\" action on the idle pill — clicking it clears the token (back to the logged-out state). Default \"Log ud\". */\n logoutLabel?: string;\n /** Show an on-site \"connect\" pill to LOGGED-OUT visitors (click → connect flow).\n * Default FALSE — customer-safe: a visitor without a token sees NOTHING. Only\n * enable for a site with no real customers (e.g. an internal/first-party site)\n * where a self-contained on-site login entry is wanted. The safe entry for a\n * customer site is always the \"Redigér live\" button in webhouse.app CMS-admin. */\n connectPrompt?: boolean;\n /** Localised labels for the toolbar + save-status pill. Defaults are Danish. */\n labels?: InlineEditLabels;\n}\n\ninterface ResolvedOptions extends Required<Omit<InlineEditOptions, \"labels\">> {\n labels: Required<InlineEditLabels>;\n}\n\nconst DEFAULT_LABELS: Required<InlineEditLabels> = {\n bold: \"Fed\",\n italic: \"Kursiv\",\n underline: \"Understreget\",\n orderedList: \"Nummereret liste\",\n unorderedList: \"Punktliste\",\n color: \"Farve\",\n emoji: \"Indsæt emoji\",\n done: \"Færdig\",\n saving: \"Gemmer…\",\n saved: \"Gemt ✓\",\n error: \"Fejl — prøv igen\",\n};\n\n// Set once from resolved options in initInlineEdit(); read by the (singleton)\n// toolbar and the save-status pill so their signatures stay unchanged. One edit\n// session per page → a single module-level value is safe.\nlet uiLabels: Required<InlineEditLabels> = DEFAULT_LABELS;\n\nfunction resolveOptions(options: InlineEditOptions): ResolvedOptions {\n return {\n tokenParam: \"cms_edit\",\n storageKey: \"wh-inline-edit-token\",\n // No emoji in defaults — the square-pen SVG is the icon; some sites ban emoji.\n connectLabel: \"Log ind for at redigere\",\n disconnectLabel: \"Afbryd\",\n logoutLabel: \"Log ud\",\n connectPrompt: false,\n ...options,\n labels: { ...DEFAULT_LABELS, ...(options.labels ?? {}) },\n };\n}\n\nexport async function initInlineEdit(options: InlineEditOptions): Promise<void> {\n if (typeof window === \"undefined\") return;\n const resolved = resolveOptions(options);\n uiLabels = resolved.labels;\n\n // Capture BEFORE checkEnabled so we know whether the token JUST arrived from\n // the connect redirect (→ drop straight into edit mode) vs. a normal load\n // with a stored token (→ idle \"Rediger\" pill, don't force edit mode).\n const freshFromUrl = captureTokenFromUrl(resolved);\n\n const enabled = await checkEnabled(resolved);\n if (!enabled) return;\n\n const token = getConnectedToken(options);\n if (!token) {\n // Logged-out visitor / customer. DEFAULT: show NOTHING (customer-safe) —\n // the consumer can now call initInlineEdit() unconditionally without leaking\n // an edit affordance. Only sites that opt in (connectPrompt: true) get the\n // on-site connect pill + its popup token receiver.\n if (resolved.connectPrompt) {\n showConnectPrompt(resolved);\n // F158 — the connect flow opens in a popup; it posts the freshly-minted\n // token back to this tab. Arm the (origin-validated) receiver.\n listenForTokenMessage(resolved);\n }\n return;\n }\n\n // Connected editor. Wire fields once, then pick the entry state: arrived from\n // connect → edit now; normal load with a stored token → idle \"Rediger\" pill\n // (one click enters edit mode; the token persists so \"Rediger\" stays visible\n // on every page while logged in — no trip back to admin).\n setupFields(token, resolved);\n if (freshFromUrl) enterEditMode();\n else showIdlePill();\n}\n\n/**\n * F158 — receive the edit token from the connect popup via postMessage.\n * Hard origin check (message MUST come from cmsBaseUrl) + type + site + expiry\n * guards before the token is trusted. On success: persist, swap the connect\n * prompt for edit mode — no page reload.\n */\nfunction listenForTokenMessage(options: ResolvedOptions): void {\n let cmsOrigin: string;\n try {\n cmsOrigin = new URL(options.cmsBaseUrl).origin;\n } catch {\n return;\n }\n window.addEventListener(\"message\", (event: MessageEvent) => {\n if (event.origin !== cmsOrigin) return;\n const data = event.data as { type?: string; token?: string; site?: string } | null;\n if (!data || data.type !== \"wh-inline-edit-token\" || typeof data.token !== \"string\") return;\n if (data.site && data.site !== options.siteId) return;\n if (isExpired(data.token)) return;\n if (document.querySelector(\"[data-cms-inline-edit-badge]\")) return; // already active\n localStorage.setItem(options.storageKey, data.token);\n document.querySelector(\"[data-cms-inline-edit-connect]\")?.remove();\n // Fresh connect via popup → wire fields + drop straight into edit mode.\n setupFields(data.token, options);\n enterEditMode();\n });\n}\n\n/**\n * A valid (non-expired) token already connected in this browser for this\n * site, or null. Also captures a fresh `?cms_edit=` token from the URL\n * first, so a page can call this directly right after the connect redirect\n * without going through initInlineEdit(). Exposed so a site's own /admin\n * page (or any other tool built on the same connected session) can reuse\n * the same storage/expiry logic instead of re-implementing it.\n */\nexport function getConnectedToken(options: InlineEditOptions): string | null {\n if (typeof window === \"undefined\") return null;\n const resolved = resolveOptions(options);\n captureTokenFromUrl(resolved);\n const token = localStorage.getItem(resolved.storageKey);\n if (!token) return null;\n if (isExpired(token)) {\n localStorage.removeItem(resolved.storageKey);\n return null;\n }\n return token;\n}\n\n/** The URL that mints a fresh 30-day site-scoped token and redirects back to `returnUrl`. */\nexport function buildConnectUrl(options: InlineEditOptions, returnUrl: string): string {\n const resolved = resolveOptions(options);\n return (\n `${resolved.cmsBaseUrl}/admin/inline-edit/connect?site=${encodeURIComponent(resolved.siteId)}` +\n `&return=${encodeURIComponent(returnUrl)}`\n );\n}\n\n/** Clears the connected token in this browser (e.g. a \"log out\" action in a site's own /admin panel). */\nexport function disconnect(options: InlineEditOptions): void {\n if (typeof window === \"undefined\") return;\n localStorage.removeItem(resolveOptions(options).storageKey);\n}\n\n/** Captures a token minted by /admin/inline-edit/connect, then strips it from\n * the URL. Returns true when a FRESH token was just captured from the URL\n * (i.e. this is the connect-redirect landing) so the caller can auto-enter\n * edit mode; false on a normal load where the token comes from localStorage. */\nfunction captureTokenFromUrl(options: ResolvedOptions): boolean {\n const url = new URL(window.location.href);\n const urlToken = url.searchParams.get(options.tokenParam);\n if (!urlToken) return false;\n localStorage.setItem(options.storageKey, urlToken);\n url.searchParams.delete(options.tokenParam);\n window.history.replaceState({}, \"\", url.toString());\n return true;\n}\n\nasync function checkEnabled(options: ResolvedOptions): Promise<boolean> {\n try {\n const res = await fetch(`${options.cmsBaseUrl}/api/inline-edit/status?site=${options.siteId}`);\n if (!res.ok) return false;\n const body = (await res.json()) as { enabled?: boolean };\n return body.enabled === true;\n } catch {\n return false;\n }\n}\n\nfunction isExpired(token: string): boolean {\n const claims = decodeJwtPayload(token);\n const exp = typeof claims?.exp === \"number\" ? claims.exp : 0;\n return exp <= Date.now() / 1000;\n}\n\n// Lucide \"square-pen\" (https://lucide.dev/icons/square-pen), MIT. Inline SVG so\n// the pill needs no icon-font/runtime dep and inherits `currentColor`. Emoji are\n// deliberately avoided — some consumer sites ban them outright.\nconst SQUARE_PEN_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<path d=\"M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\"/>' +\n '<path d=\"M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z\"/>' +\n \"</svg>\";\n\n// Toolbar list-button faces (lucide \"list\" / \"list-ordered\"), inline so no\n// icon-font/runtime dep; stroke inherits the button's white `currentColor`.\nconst UL_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<line x1=\"8\" x2=\"21\" y1=\"6\" y2=\"6\"/><line x1=\"8\" x2=\"21\" y1=\"12\" y2=\"12\"/>' +\n '<line x1=\"8\" x2=\"21\" y1=\"18\" y2=\"18\"/><line x1=\"3\" x2=\"3.01\" y1=\"6\" y2=\"6\"/>' +\n '<line x1=\"3\" x2=\"3.01\" y1=\"12\" y2=\"12\"/><line x1=\"3\" x2=\"3.01\" y1=\"18\" y2=\"18\"/>' +\n \"</svg>\";\n\nconst OL_SVG =\n '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" ' +\n 'fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" ' +\n 'stroke-linejoin=\"round\" aria-hidden=\"true\">' +\n '<line x1=\"10\" x2=\"21\" y1=\"6\" y2=\"6\"/><line x1=\"10\" x2=\"21\" y1=\"12\" y2=\"12\"/>' +\n '<line x1=\"10\" x2=\"21\" y1=\"18\" y2=\"18\"/><path d=\"M4 6h1v4\"/><path d=\"M4 10h2\"/>' +\n '<path d=\"M6 18H4c0-1 2-2 2-3s-1-1.5-2-1\"/>' +\n \"</svg>\";\n\nfunction makeIcon(): HTMLSpanElement {\n const icon = document.createElement(\"span\");\n icon.style.cssText = \"display:inline-flex;line-height:0;flex:0 0 auto\";\n icon.innerHTML = SQUARE_PEN_SVG;\n return icon;\n}\n\nfunction showConnectPrompt(options: ResolvedOptions): void {\n const connectUrl = buildConnectUrl(options, window.location.href);\n\n const link = document.createElement(\"a\");\n link.href = connectUrl;\n link.setAttribute(\"data-cms-inline-edit-connect\", \"\");\n link.style.cssText =\n \"position:fixed;bottom:16px;left:16px;background:#1c2027;color:#fff;\" +\n \"font:600 12px system-ui,sans-serif;padding:8px 14px;border-radius:999px;\" +\n \"text-decoration:none;z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);\" +\n \"display:inline-flex;align-items:center;gap:7px;\";\n const text = document.createElement(\"span\");\n text.textContent = options.connectLabel;\n link.append(makeIcon(), text);\n // F158 — open the connect flow in a popup so the CMS confirmation screen can\n // say \"close this window\". A user-gesture window.open is not popup-blocked;\n // if it is blocked anyway, fall back to the same-tab redirect (F157 path).\n link.addEventListener(\"click\", (e) => {\n e.preventDefault();\n const popup = window.open(\n connectUrl,\n \"wh-inline-edit-connect\",\n \"width=460,height=640,menubar=no,toolbar=no,location=yes,status=no\",\n );\n if (!popup) window.location.href = connectUrl;\n });\n document.body.appendChild(link);\n}\n\n// ─── Connected-editor state machine ────────────────────────────────────────\n// A connected editor (valid token) is ALWAYS in one of two visible states:\n// idle → a \"Rediger\" pill (options.connectLabel); one click enters editing.\n// editing → fields are contenteditable + an \"Afslut redigering\" pill.\n// Leaving edit mode returns to idle and KEEPS the token — the editor stays\n// logged in and can re-enter with one click, on every page, for the token's\n// whole life. Only an explicit \"Log ud\" clears the token (→ logged-out state:\n// no pill at all, same as a customer). Fields are wired ONCE; their click\n// handlers no-op while editingActive is false, so idle is truly inert.\nlet editingActive = false;\nlet fieldsWired = false;\nlet stateOptions: ResolvedOptions | null = null;\n\nfunction setupFields(token: string, options: ResolvedOptions): void {\n stateOptions = options;\n if (fieldsWired) return;\n injectStyles();\n document.querySelectorAll<HTMLElement>(\"[data-cms-field]\").forEach((el) => wireField(el, token, options));\n fieldsWired = true;\n}\n\nfunction enterEditMode(): void {\n if (!stateOptions || editingActive) return;\n editingActive = true;\n removeIdlePill();\n showActiveBadge(stateOptions);\n}\n\nfunction exitEditMode(): void {\n if (!stateOptions || !editingActive) return;\n deactivateRich(); // commit any active rich region first\n editingActive = false;\n document.querySelector(\"[data-cms-inline-edit-badge]\")?.remove();\n showIdlePill();\n}\n\nfunction removeIdlePill(): void {\n document.querySelector(\"[data-cms-inline-edit-idle]\")?.remove();\n}\n\nfunction showIdlePill(): void {\n if (!stateOptions) return;\n const options = stateOptions;\n removeIdlePill();\n const wrap = document.createElement(\"div\");\n wrap.setAttribute(\"data-cms-inline-edit-idle\", \"\");\n wrap.style.cssText =\n \"position:fixed;bottom:16px;left:16px;display:inline-flex;align-items:stretch;\" +\n \"z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);border-radius:999px;overflow:hidden;\";\n // Main action: \"Rediger\" → enter edit mode (no server round-trip; the token\n // already exists locally).\n const edit = document.createElement(\"button\");\n edit.type = \"button\";\n edit.setAttribute(\"data-cms-inline-edit-enter\", \"\");\n edit.style.cssText =\n \"display:inline-flex;align-items:center;gap:7px;background:#1c2027;color:#fff;\" +\n \"font:600 12px system-ui,sans-serif;padding:8px 14px;border:none;cursor:pointer;\";\n const editText = document.createElement(\"span\");\n editText.textContent = options.connectLabel;\n edit.append(makeIcon(), editText);\n edit.addEventListener(\"click\", enterEditMode);\n // Secondary: \"Log ud\" → clear the token (back to the logged-out state).\n const out = document.createElement(\"button\");\n out.type = \"button\";\n out.setAttribute(\"data-cms-inline-edit-logout\", \"\");\n out.textContent = options.logoutLabel;\n out.style.cssText =\n \"background:#141821;color:#9aa4b2;font:600 11px system-ui,sans-serif;padding:8px 12px;\" +\n \"border:none;border-left:1px solid rgba(255,255,255,.12);cursor:pointer;\";\n out.addEventListener(\"click\", () => {\n disconnect(options);\n removeIdlePill();\n });\n wrap.append(edit, out);\n document.body.appendChild(wrap);\n}\n\nfunction wireField(el: HTMLElement, token: string, options: ResolvedOptions): void {\n // Rich fields get the floating formatting toolbar. Two save modes:\n // - data-cms-richtext=\"true\" → save as Markdown (article bodies; cms\n // `richtext` contract renders Markdown via marked).\n // - data-cms-html=\"true\" → save innerHTML VERBATIM (intentional-HTML\n // fields: headings/bios/hero with a branded <em class=\"o\"> accent that\n // has no Markdown equivalent — converting would strip it).\n // Everything else stays a plain single-line contenteditable saving textContent.\n if (el.dataset.cmsRichtext === \"true\") {\n wireRichField(el, token, options, \"markdown\");\n return;\n }\n if (el.dataset.cmsHtml === \"true\") {\n wireRichField(el, token, options, \"html\");\n return;\n }\n\n el.addEventListener(\"click\", (e) => {\n if (!editingActive) return; // idle: the field behaves normally, not editable\n if (el.getAttribute(\"contenteditable\") === \"true\") return;\n // Many editable fields (card titles/blurbs) sit inside a clickable <a>/\n // <button> ancestor (the card itself) — without this, \"click to edit\"\n // would immediately navigate away instead of focusing the field.\n e.preventDefault();\n e.stopPropagation();\n el.dataset.cmsOriginalValue = el.textContent ?? \"\";\n el.setAttribute(\"contenteditable\", \"true\");\n el.focus();\n });\n\n el.addEventListener(\"blur\", () => {\n el.removeAttribute(\"contenteditable\");\n const original = el.dataset.cmsOriginalValue ?? \"\";\n const current = el.textContent ?? \"\";\n if (current.trim() === original.trim()) return;\n void saveField(el, current.trim(), token, options);\n });\n\n el.addEventListener(\"paste\", (e) => {\n e.preventDefault();\n const text = e.clipboardData?.getData(\"text/plain\") ?? \"\";\n document.execCommand(\"insertText\", false, text);\n });\n\n el.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n el.blur();\n }\n });\n}\n\n/* ─── Rich-text mode (article bodies) ──────────────────────────────────────\n * A single editable region + a floating B/I/U · color · emoji · Done toolbar\n * (the Pitch Vault pattern, minus the iframe/postMessage — we run in the live\n * page's own document). On \"Done\" the region's innerHTML is converted back to\n * Markdown (the cms `richtext` contract: fields store Markdown, not HTML) and\n * saved. Only ONE region is active at a time. */\n\ntype RichMode = \"markdown\" | \"html\";\ninterface RichContext {\n el: HTMLElement;\n token: string;\n options: ResolvedOptions;\n originalHtml: string;\n mode: RichMode;\n}\nlet richCtx: RichContext | null = null;\nlet richToolbar: HTMLElement | null = null;\n\nfunction wireRichField(el: HTMLElement, token: string, options: ResolvedOptions, mode: RichMode): void {\n el.addEventListener(\"click\", (e) => {\n if (!editingActive) return; // idle: the field behaves normally, not editable\n if (richCtx && richCtx.el === el) return; // already editing this region\n e.preventDefault();\n e.stopPropagation();\n activateRich(el, token, options, mode);\n });\n}\n\nfunction activateRich(el: HTMLElement, token: string, options: ResolvedOptions, mode: RichMode): void {\n deactivateRich(); // commit any previously-active region first\n richCtx = { el, token, options, originalHtml: el.innerHTML, mode };\n el.setAttribute(\"contenteditable\", \"true\");\n el.classList.add(\"cms-rich-editing\");\n el.focus();\n showRichToolbar();\n // Let the host page react (e.g. pause a rotating hero carousel while editing).\n document.dispatchEvent(new CustomEvent(\"cms-inline-edit:activate\", { detail: { el } }));\n}\n\nfunction deactivateRich(): void {\n if (!richCtx) return;\n const { el, token, options, originalHtml, mode } = richCtx;\n richCtx = null;\n el.removeAttribute(\"contenteditable\");\n el.classList.remove(\"cms-rich-editing\");\n hideRichToolbar();\n if (el.innerHTML !== originalHtml) {\n const value = mode === \"html\" ? el.innerHTML : htmlToMarkdown(el.innerHTML);\n void saveField(el, value, token, options);\n }\n document.dispatchEvent(new CustomEvent(\"cms-inline-edit:deactivate\", { detail: { el } }));\n}\n\nfunction showRichToolbar(): void {\n if (!richToolbar) richToolbar = buildRichToolbar();\n richToolbar.style.display = \"flex\";\n}\n\nfunction hideRichToolbar(): void {\n if (richToolbar) richToolbar.style.display = \"none\";\n hideEmojiPicker();\n}\n\nfunction toolbarButton(label: string, title: string, onDown: () => void): HTMLButtonElement {\n const b = document.createElement(\"button\");\n b.type = \"button\";\n b.innerHTML = label;\n b.title = title;\n b.style.cssText =\n \"background:none;border:1px solid #3a3f4a;color:#fff;min-width:30px;height:30px;\" +\n \"border-radius:6px;cursor:pointer;font-size:14px;padding:0 8px;line-height:1;\";\n b.addEventListener(\"mouseenter\", () => (b.style.background = \"#2a2f38\"));\n b.addEventListener(\"mouseleave\", () => (b.style.background = \"none\"));\n // mousedown + preventDefault so the contenteditable selection isn't lost.\n b.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n onDown();\n });\n return b;\n}\n\nfunction buildRichToolbar(): HTMLElement {\n const t = document.createElement(\"div\");\n t.setAttribute(\"data-cms-inline-edit-toolbar\", \"\");\n t.style.cssText =\n \"position:fixed;top:14px;left:50%;transform:translateX(-50%);\" +\n \"background:#1c2027;border:1px solid #3a3f4a;border-radius:10px;\" +\n \"padding:6px 10px;display:flex;gap:6px;align-items:center;\" +\n \"z-index:2147483647;box-shadow:0 8px 32px rgba(0,0,0,.5);\" +\n \"font-family:system-ui,sans-serif;\";\n\n t.appendChild(toolbarButton(\"<b>B</b>\", uiLabels.bold, () => document.execCommand(\"bold\")));\n t.appendChild(toolbarButton(\"<i>I</i>\", uiLabels.italic, () => document.execCommand(\"italic\")));\n t.appendChild(toolbarButton(\"<u>U</u>\", uiLabels.underline, () => document.execCommand(\"underline\")));\n\n const sep = () => {\n const s = document.createElement(\"div\");\n s.style.cssText = \"width:1px;height:20px;background:#3a3f4a;\";\n return s;\n };\n t.appendChild(sep());\n\n // Bullet + numbered lists — native execCommand, and the Markdown serializer\n // already round-trips <ul>/<ol> to \"- \" / \"1. \" so no save-path change needed.\n t.appendChild(toolbarButton(UL_SVG, uiLabels.unorderedList, () => document.execCommand(\"insertUnorderedList\")));\n t.appendChild(toolbarButton(OL_SVG, uiLabels.orderedList, () => document.execCommand(\"insertOrderedList\")));\n\n t.appendChild(sep());\n\n // Text color — execCommand foreColor applies to the current selection.\n const clrLabel = document.createElement(\"label\");\n clrLabel.style.cssText = \"display:flex;align-items:center;gap:5px;color:#9aa4b2;font-size:12px;cursor:pointer;\";\n clrLabel.textContent = uiLabels.color;\n const clr = document.createElement(\"input\");\n clr.type = \"color\";\n clr.style.cssText = \"width:28px;height:24px;border:1px solid #3a3f4a;border-radius:5px;cursor:pointer;padding:1px;background:none;\";\n clr.addEventListener(\"mousedown\", (e) => e.stopPropagation());\n clr.addEventListener(\"input\", () => document.execCommand(\"foreColor\", false, clr.value));\n clrLabel.prepend(clr);\n t.appendChild(clrLabel);\n\n t.appendChild(sep());\n\n const emojiBtn = toolbarButton(\"😀\", uiLabels.emoji, () => toggleEmojiPicker(emojiBtn));\n emojiBtn.style.fontSize = \"16px\";\n t.appendChild(emojiBtn);\n\n t.appendChild(sep());\n\n const done = document.createElement(\"button\");\n done.type = \"button\";\n done.textContent = uiLabels.done;\n done.style.cssText =\n \"background:#00b2ff;border:none;color:#04121c;padding:0 14px;height:30px;\" +\n \"border-radius:6px;cursor:pointer;font-size:12px;font-weight:700;\";\n done.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n deactivateRich();\n });\n t.appendChild(done);\n\n document.body.appendChild(t);\n return t;\n}\n\n/* ─── emoji picker (compact) ────────────────────────────────────────────── */\nconst EMOJIS =\n \"😀 😃 😄 😁 😆 😉 😊 😍 🤩 😎 🤔 🙌 👏 👍 👎 🙏 💪 🔥 ⚡ ✨ 🎉 ✅ ❌ 💯 ⭐ 🚀 💡 📈 📉 🧠 ❤️ 🧡 💛 💚 💙 💜 🇩🇰 🇪🇺\".split(\n \" \",\n );\nlet emojiPicker: HTMLElement | null = null;\nlet savedRange: Range | null = null;\n\nfunction toggleEmojiPicker(anchor: HTMLElement): void {\n if (!emojiPicker) emojiPicker = buildEmojiPicker();\n if (emojiPicker.style.display === \"block\") {\n hideEmojiPicker();\n return;\n }\n const sel = window.getSelection();\n if (sel && sel.rangeCount > 0) savedRange = sel.getRangeAt(0).cloneRange();\n const rect = anchor.getBoundingClientRect();\n emojiPicker.style.top = `${rect.bottom + 6}px`;\n emojiPicker.style.left = `${Math.min(rect.left, window.innerWidth - 240)}px`;\n emojiPicker.style.display = \"block\";\n}\n\nfunction hideEmojiPicker(): void {\n if (emojiPicker) emojiPicker.style.display = \"none\";\n}\n\nfunction buildEmojiPicker(): HTMLElement {\n const p = document.createElement(\"div\");\n p.setAttribute(\"data-cms-inline-edit-toolbar\", \"\");\n p.style.cssText =\n \"position:fixed;z-index:2147483646;background:#1c2027;border:1px solid #3a3f4a;\" +\n \"border-radius:10px;padding:8px;width:230px;max-height:200px;overflow-y:auto;\" +\n \"display:none;box-shadow:0 8px 32px rgba(0,0,0,.5);\";\n const grid = document.createElement(\"div\");\n grid.style.cssText = \"display:flex;flex-wrap:wrap;gap:2px;\";\n EMOJIS.forEach((em) => {\n const b = document.createElement(\"button\");\n b.type = \"button\";\n b.textContent = em;\n b.style.cssText =\n \"background:none;border:none;cursor:pointer;border-radius:5px;width:32px;height:32px;font-size:18px;\";\n b.addEventListener(\"mouseenter\", () => (b.style.background = \"#2a2f38\"));\n b.addEventListener(\"mouseleave\", () => (b.style.background = \"none\"));\n b.addEventListener(\"mousedown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n insertEmoji(em);\n });\n grid.appendChild(b);\n });\n p.appendChild(grid);\n document.body.appendChild(p);\n return p;\n}\n\nfunction insertEmoji(emoji: string): void {\n if (!richCtx) return;\n richCtx.el.focus();\n const sel = window.getSelection();\n if (savedRange && sel) {\n sel.removeAllRanges();\n sel.addRange(savedRange);\n }\n document.execCommand(\"insertText\", false, emoji);\n savedRange = null;\n hideEmojiPicker();\n}\n\n// Click outside the active region (and outside the toolbar) commits the edit.\nif (typeof document !== \"undefined\") {\n document.addEventListener(\n \"mousedown\",\n (e) => {\n if (!richCtx) return;\n const target = e.target as HTMLElement | null;\n if (!target) return;\n if (target.closest(\"[data-cms-inline-edit-toolbar]\")) return;\n if (richCtx.el.contains(target)) return;\n deactivateRich();\n },\n true,\n );\n}\n\n/**\n * Sets a value at a dot-path into a plain-data tree, where a numeric segment\n * indexes into an array (e.g. \"slides.2.eyebrow\" — flagship-style nested\n * content; a flat \"heroEyebrow\" is still just a 1-segment path). Bails\n * silently (no throw) if the path doesn't resolve — a stale/malformed path\n * must never crash the save, just fail to apply.\n */\nfunction setDeepField(data: Record<string, unknown>, path: string, value: string): void {\n const parts = path.split(\".\");\n let obj: any = data;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i] ?? \"\";\n const key: string | number = /^\\d+$/.test(part) ? Number(part) : part;\n if (obj == null || typeof obj !== \"object\" || obj[key] === undefined) return;\n obj = obj[key];\n }\n const last = parts[parts.length - 1] ?? \"\";\n const lastKey: string | number = /^\\d+$/.test(last) ? Number(last) : last;\n if (obj == null || typeof obj !== \"object\") return;\n obj[lastKey] = value;\n}\n\n/**\n * Converts an edited contenteditable region's innerHTML back to Markdown so a\n * rich article body round-trips through the cms `richtext` contract (fields\n * store Markdown, `marked` renders it — NOT HTML). Handles the structures a\n * marked-rendered body + the toolbar produce: headings, paragraphs, emphasis,\n * links, lists, blockquote, code. Formatting with no clean Markdown equivalent\n * (underline, coloured spans) is passed through as inline HTML — `marked`\n * renders inline HTML, so it survives without corrupting the source. Tables and\n * anything unrecognised are passed through as their outerHTML for the same\n * reason: never drop content just because it doesn't map to a Markdown token.\n */\nfunction htmlToMarkdown(html: string): string {\n const container = document.createElement(\"div\");\n container.innerHTML = html;\n return serializeBlockChildren(container).replace(/\\n{3,}/g, \"\\n\\n\").trim() + \"\\n\";\n}\n\nfunction serializeBlockChildren(parent: Node): string {\n const blocks: string[] = [];\n parent.childNodes.forEach((node) => {\n const s = serializeBlock(node).trim();\n if (s) blocks.push(s);\n });\n return blocks.join(\"\\n\\n\");\n}\n\nfunction serializeBlock(node: Node): string {\n if (node.nodeType === Node.TEXT_NODE) return (node.textContent ?? \"\").trim();\n if (node.nodeType !== Node.ELEMENT_NODE) return \"\";\n const el = node as HTMLElement;\n const tag = el.tagName.toLowerCase();\n switch (tag) {\n case \"h1\": return \"# \" + serializeInline(el);\n case \"h2\": return \"## \" + serializeInline(el);\n case \"h3\": return \"### \" + serializeInline(el);\n case \"h4\": return \"#### \" + serializeInline(el);\n case \"h5\": return \"##### \" + serializeInline(el);\n case \"h6\": return \"###### \" + serializeInline(el);\n case \"p\":\n case \"div\":\n return serializeInline(el);\n case \"ul\":\n return serializeList(el, false);\n case \"ol\":\n return serializeList(el, true);\n case \"blockquote\":\n // Serialize the quote's OWN block children (it usually wraps <p>s), then\n // prefix each line with \"> \" — treating it as inline dropped the <p> and\n // left a stray leading space.\n return serializeBlockChildren(el)\n .split(\"\\n\")\n .map((l) => (l ? \"> \" + l : \">\"))\n .join(\"\\n\");\n case \"pre\":\n return \"```\\n\" + (el.textContent ?? \"\").replace(/\\n+$/, \"\") + \"\\n```\";\n case \"hr\":\n return \"---\";\n case \"br\":\n return \"\";\n case \"table\":\n // Rebuild the Markdown pipe table (NOT raw-HTML passthrough) so the\n // consumer's marked renderer re-applies its own table treatment (e.g.\n // broberg's mobile scroll-wrapper) instead of getting a bare <table>.\n return serializeTable(el);\n default:\n return serializeInline(el);\n }\n}\n\nfunction serializeTable(table: HTMLElement): string {\n const cellText = (c: Element) => serializeInline(c).trim().replace(/\\|/g, \"\\\\|\");\n const rows: string[] = [];\n const headCells = Array.from(table.querySelectorAll(\"thead tr\")).flatMap((tr) =>\n Array.from(tr.children).map(cellText),\n );\n if (headCells.length) {\n rows.push(\"| \" + headCells.join(\" | \") + \" |\");\n rows.push(\"| \" + headCells.map(() => \"---\").join(\" | \") + \" |\");\n }\n const bodyRows = table.querySelector(\"tbody\")\n ? Array.from(table.querySelectorAll(\"tbody tr\"))\n : Array.from(table.querySelectorAll(\"tr\")).filter((tr) => !tr.closest(\"thead\"));\n bodyRows.forEach((tr) => {\n rows.push(\"| \" + Array.from(tr.children).map(cellText).join(\" | \") + \" |\");\n });\n return rows.join(\"\\n\");\n}\n\nfunction serializeList(listEl: HTMLElement, ordered: boolean): string {\n const items: string[] = [];\n let n = 1;\n listEl.childNodes.forEach((child) => {\n if (child.nodeType === Node.ELEMENT_NODE && (child as HTMLElement).tagName.toLowerCase() === \"li\") {\n const marker = ordered ? `${n++}. ` : \"- \";\n items.push(marker + serializeInline(child as HTMLElement).trim());\n }\n });\n return items.join(\"\\n\");\n}\n\nfunction serializeInline(node: Node): string {\n let out = \"\";\n node.childNodes.forEach((child) => {\n if (child.nodeType === Node.TEXT_NODE) {\n out += (child.textContent ?? \"\").replace(/\\s+/g, \" \");\n return;\n }\n if (child.nodeType !== Node.ELEMENT_NODE) return;\n const el = child as HTMLElement;\n const tag = el.tagName.toLowerCase();\n const inner = serializeInline(el);\n switch (tag) {\n case \"strong\":\n case \"b\":\n out += inner.trim() ? `**${inner.trim()}**` : \"\";\n break;\n case \"em\":\n case \"i\":\n out += inner.trim() ? `*${inner.trim()}*` : \"\";\n break;\n case \"code\":\n out += \"`\" + inner + \"`\";\n break;\n case \"br\":\n out += \" \\n\";\n break;\n case \"a\": {\n const href = el.getAttribute(\"href\") || \"\";\n out += href ? `[${inner}](${href})` : inner;\n break;\n }\n case \"img\": {\n // Content images embedded in the body () — dropping them\n // would silently delete images from an article on save. Void element,\n // no children, so this must be handled explicitly.\n const src = el.getAttribute(\"src\") || \"\";\n const alt = el.getAttribute(\"alt\") || \"\";\n if (src) out += ``;\n break;\n }\n case \"u\":\n out += `<u>${inner}</u>`; // no Markdown for underline — pass through\n break;\n case \"span\":\n case \"font\": {\n const color = el.style.color || el.getAttribute(\"color\") || \"\";\n out += color ? `<span style=\"color:${color}\">${inner}</span>` : inner;\n break;\n }\n default:\n out += inner;\n }\n });\n return out;\n}\n\nasync function saveField(el: HTMLElement, value: string, token: string, options: ResolvedOptions): Promise<void> {\n const collection = el.dataset.cmsCollection;\n const slug = el.dataset.cmsSlug;\n const field = el.dataset.cmsField;\n if (!collection || !slug || !field) return;\n const slice = el.dataset.cmsSlice;\n\n showPill(el, \"saving\");\n try {\n const getRes = await fetch(\n `${options.cmsBaseUrl}/api/cms/${collection}/${slug}?site=${options.siteId}`,\n { headers: { Authorization: `Bearer ${token}` } },\n );\n if (!getRes.ok) throw new Error(`GET failed: ${getRes.status}`);\n const doc = (await getRes.json()) as { data?: Record<string, unknown> };\n // Deep-clone so mutating a nested array/object (dot-path saves) never\n // aliases the fetched doc — same safety whether field is flat or nested.\n const mergedData = JSON.parse(JSON.stringify(doc.data ?? {})) as Record<string, unknown>;\n if (slice !== undefined) {\n // Field-slice save: replace just this segment inside the full field value,\n // preserving every other segment + embed. Aborts on a non-unique match.\n const currentVal = typeof mergedData[field] === \"string\" ? (mergedData[field] as string) : \"\";\n mergedData[field] = applyFieldSlice(currentVal, slice, value);\n } else if (field.includes(\".\")) {\n setDeepField(mergedData, field, value);\n } else {\n mergedData[field] = value;\n }\n\n const patchRes = await fetch(\n `${options.cmsBaseUrl}/api/cms/${collection}/${slug}?site=${options.siteId}`,\n {\n method: \"PATCH\",\n headers: { Authorization: `Bearer ${token}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ data: mergedData }),\n },\n );\n if (!patchRes.ok) throw new Error(`PATCH failed: ${patchRes.status}`);\n // The slice we just wrote is now the current text — track it so a second\n // edit of the same segment (no reload) matches against the new value.\n if (slice !== undefined) el.dataset.cmsSlice = value;\n showPill(el, \"saved\");\n } catch {\n showPill(el, \"error\");\n }\n}\n\nconst pills = new WeakMap<HTMLElement, HTMLElement>();\n\nfunction showPill(el: HTMLElement, state: \"saving\" | \"saved\" | \"error\"): void {\n let pill = pills.get(el);\n if (!pill) {\n pill = document.createElement(\"span\");\n pill.setAttribute(\"data-cms-inline-edit-pill\", \"\");\n document.body.appendChild(pill);\n pills.set(el, pill);\n }\n const rect = el.getBoundingClientRect();\n pill.style.cssText =\n `position:fixed;top:${rect.top - 26}px;left:${rect.left}px;font:600 11px system-ui,sans-serif;` +\n `padding:3px 9px;border-radius:5px;z-index:2147483647;pointer-events:none;` +\n `box-shadow:0 2px 8px rgba(0,0,0,.25);`;\n\n if (state === \"saving\") {\n pill.textContent = uiLabels.saving;\n pill.style.background = \"#1c2027\";\n pill.style.color = \"#fff\";\n } else if (state === \"saved\") {\n pill.textContent = uiLabels.saved;\n pill.style.background = \"#16a34a\";\n pill.style.color = \"#fff\";\n setTimeout(() => {\n pill?.remove();\n pills.delete(el);\n }, 1500);\n } else {\n pill.textContent = uiLabels.error;\n pill.style.background = \"#dc2626\";\n pill.style.color = \"#fff\";\n }\n}\n\nfunction showActiveBadge(options: ResolvedOptions): void {\n // The connected badge IS the exit-edit action: [icon] \"Afslut redigering\".\n // No \"Redigerer som {name}\" — the whole pill is one click to leave edit mode.\n const badge = document.createElement(\"button\");\n badge.type = \"button\";\n badge.setAttribute(\"data-cms-inline-edit-badge\", \"\");\n badge.style.cssText =\n \"position:fixed;bottom:16px;left:16px;display:inline-flex;align-items:center;gap:7px;\" +\n \"background:#1c2027;color:#fff;font:600 12px system-ui,sans-serif;padding:8px 14px;\" +\n \"border-radius:999px;z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);\" +\n \"border:none;cursor:pointer;opacity:.9;transition:opacity .15s;\";\n const text = document.createElement(\"span\");\n text.textContent = options.disconnectLabel;\n badge.append(makeIcon(), text);\n badge.addEventListener(\"mouseenter\", () => (badge.style.opacity = \"1\"));\n badge.addEventListener(\"mouseleave\", () => (badge.style.opacity = \".9\"));\n // Clicking the badge LEAVES edit mode but KEEPS the login → back to the idle\n // \"Rediger\" pill (not a logout). Explicit logout lives on the idle pill.\n badge.addEventListener(\"click\", exitEditMode);\n document.body.appendChild(badge);\n}\n\n/** Reads JWT claims for DISPLAY ONLY — never used for authorization decisions. */\nfunction decodeJwtPayload(token: string): Record<string, unknown> | null {\n try {\n const payload = token.split(\".\")[1];\n if (!payload) return null;\n return JSON.parse(atob(payload.replace(/-/g, \"+\").replace(/_/g, \"/\")));\n } catch {\n return null;\n }\n}\n\nfunction injectStyles(): void {\n const style = document.createElement(\"style\");\n style.textContent = `\n [data-cms-field] { outline: 1px dashed transparent; outline-offset: 2px; cursor: text; transition: outline-color .15s; }\n [data-cms-field]:hover { outline-color: rgba(0,178,255,.5); }\n [data-cms-field][contenteditable=\"true\"] { outline: 2px solid #00b2ff; outline-offset: 2px; }\n [data-cms-field][data-cms-richtext=\"true\"] { cursor: text; }\n .cms-rich-editing { outline: 2px solid #00b2ff !important; outline-offset: 6px; border-radius: 4px; }\n `;\n document.head.appendChild(style);\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED