@broberg/cms-inline-edit 0.4.20 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -0
- package/dist/index.cjs +376 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -1
- package/dist/index.d.ts +34 -1
- package/dist/index.js +375 -13
- package/dist/index.js.map +1 -1
- package/dist/server/index.cjs +29 -0
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +10 -1
- package/dist/server/index.d.ts +10 -1
- package/dist/server/index.js +28 -0
- package/dist/server/index.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -36,3 +36,56 @@ import { saveInlineEditField, verifyEditSession } from "@broberg/cms-inline-edit
|
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
See `docs/features/F157-inline-editing.md` in the [@webhouse/cms](https://github.com/webhousecode/cms) repo for the full design.
|
|
39
|
+
|
|
40
|
+
## Links to a page (live references)
|
|
41
|
+
|
|
42
|
+
The toolbar's link button offers a **free URL** or **a page on the site**. A page
|
|
43
|
+
link stores a reference next to a real, working href:
|
|
44
|
+
|
|
45
|
+
```html
|
|
46
|
+
<a href="/da/om-sanne" data-cms-ref="sider:om-sanne" data-cms-ref-label="auto">Om Sanne</a>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Render it through `resolveCmsLinks()` and the link re-points itself when the page
|
|
50
|
+
moves or is renamed — nothing rewrites stored content:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { resolveCmsLinks } from "@broberg/cms-inline-edit/server";
|
|
54
|
+
|
|
55
|
+
const html = resolveCmsLinks(renderMarkdown(doc.body), (collection, slug) => {
|
|
56
|
+
const page = findPage(collection, slug);
|
|
57
|
+
return page ? { url: page.path, title: page.title } : null;
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`data-cms-ref-label="auto"` (set when the editor left the link text empty) also
|
|
62
|
+
follows the page's current title. An unknown reference keeps the href it has, so
|
|
63
|
+
a deleted page degrades to the last known link rather than a dead one — and a
|
|
64
|
+
site that never calls the resolver still ships working links.
|
|
65
|
+
|
|
66
|
+
The picker's page list comes from `GET /api/inline-edit/pages?site=<id>`
|
|
67
|
+
(published documents only).
|
|
68
|
+
|
|
69
|
+
## Markup in a PLAIN field — use `data-cms-token`
|
|
70
|
+
|
|
71
|
+
**Which save path a fix covers matters.** The serializer fixes in 0.4.21
|
|
72
|
+
(orphaned `<li>`) and 0.4.23 (top-level inline formatting, dropped images) live
|
|
73
|
+
in the **rich** path — a field marked `data-cms-richtext="true"`, serialised
|
|
74
|
+
through `htmlToMarkdown`.
|
|
75
|
+
|
|
76
|
+
A **plain** field is different: it saves `el.textContent`, and `textContent` has
|
|
77
|
+
no asterisks. So Markdown markup inside a plain field is lost on save, and
|
|
78
|
+
**0.4.23 does not change that.** If you are carrying a `data-cms-token`
|
|
79
|
+
workaround for markup in a plain field, KEEP IT after upgrading — it is not dead
|
|
80
|
+
code (reported by the sanneandersen session, 2026-08-17).
|
|
81
|
+
|
|
82
|
+
The supported way to keep markup in a plain field is `data-cms-token` on the
|
|
83
|
+
formatted segment, carrying the raw source:
|
|
84
|
+
|
|
85
|
+
```html
|
|
86
|
+
<span data-cms-token="**Alt fra Blad**"><strong>Alt fra Blad</strong></span>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`serializeTokenSafe()` round-trips those segments verbatim, so the stored value
|
|
90
|
+
keeps its `**…**` while the page shows the rendered form. Use the rich path
|
|
91
|
+
(`data-cms-richtext="true"`) when a field should support formatting generally.
|
package/dist/index.cjs
CHANGED
|
@@ -24,6 +24,7 @@ __export(index_exports, {
|
|
|
24
24
|
buildConnectUrl: () => buildConnectUrl,
|
|
25
25
|
disconnect: () => disconnect,
|
|
26
26
|
getConnectedToken: () => getConnectedToken,
|
|
27
|
+
htmlToMarkdown: () => htmlToMarkdown,
|
|
27
28
|
initInlineEdit: () => initInlineEdit,
|
|
28
29
|
rescanFields: () => rescanFields
|
|
29
30
|
});
|
|
@@ -39,6 +40,36 @@ function applyFieldSlice(current, original, next) {
|
|
|
39
40
|
return current.slice(0, first) + next + current.slice(first + original.length);
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
// src/token-safe.ts
|
|
44
|
+
var TEXT_NODE = 3;
|
|
45
|
+
var ELEMENT_NODE = 1;
|
|
46
|
+
function hasTokenChips(el) {
|
|
47
|
+
return el.querySelector("[data-cms-token]") !== null;
|
|
48
|
+
}
|
|
49
|
+
function serializeTokenSafe(el) {
|
|
50
|
+
let out = "";
|
|
51
|
+
el.childNodes.forEach((node) => {
|
|
52
|
+
if (node.nodeType === TEXT_NODE) {
|
|
53
|
+
out += node.textContent ?? "";
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (node.nodeType !== ELEMENT_NODE) return;
|
|
57
|
+
const element = node;
|
|
58
|
+
const token = element.getAttribute("data-cms-token");
|
|
59
|
+
if (token) {
|
|
60
|
+
out += token;
|
|
61
|
+
} else {
|
|
62
|
+
out += serializeTokenSafe(element);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
function lockTokenChips(el) {
|
|
68
|
+
el.querySelectorAll("[data-cms-token]").forEach((chip) => {
|
|
69
|
+
chip.setAttribute("contenteditable", "false");
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
42
73
|
// src/index.ts
|
|
43
74
|
var DEFAULT_LABELS = {
|
|
44
75
|
bold: "Fed",
|
|
@@ -51,7 +82,27 @@ var DEFAULT_LABELS = {
|
|
|
51
82
|
done: "F\xE6rdig",
|
|
52
83
|
saving: "Gemmer\u2026",
|
|
53
84
|
saved: "Gemt \u2713",
|
|
54
|
-
error: "Fejl \u2014 pr\xF8v igen"
|
|
85
|
+
error: "Fejl \u2014 pr\xF8v igen",
|
|
86
|
+
link: "Link",
|
|
87
|
+
linkTabPage: "Side p\xE5 sitet",
|
|
88
|
+
linkTabUrl: "Fri adresse",
|
|
89
|
+
linkSearch: "S\xF8g efter en side\u2026",
|
|
90
|
+
linkText: "Linktekst",
|
|
91
|
+
linkTextAuto: "F\xF8lger sidens titel",
|
|
92
|
+
linkTextAutoHint: "<b>Tom = f\xF8lger sidens titel.</b> Omd\xF8bes siden, retter teksten sig selv. Skriver du din egen tekst, bliver den st\xE5ende.",
|
|
93
|
+
linkTextOwnHint: "Linket viser <b>din egen tekst</b>. Den bliver st\xE5ende, ogs\xE5 hvis siden omd\xF8bes.",
|
|
94
|
+
linkLiveHint: "Linket peger p\xE5 <b>siden</b> \u2014 ikke p\xE5 en adresse. Flyttes siden, eller f\xE5r den et nyt navn, retter linket sig selv.",
|
|
95
|
+
linkUrlHint: "En fri adresse peger pr\xE6cis d\xE9r, du skriver. Den f\xF8lger <b>ikke</b> med, hvis m\xE5let flytter sig.",
|
|
96
|
+
linkUrl: "Adresse",
|
|
97
|
+
linkInsert: "Inds\xE6t link",
|
|
98
|
+
linkSave: "Gem \xE6ndring",
|
|
99
|
+
linkCancel: "Annull\xE9r",
|
|
100
|
+
linkRemove: "Fjern link",
|
|
101
|
+
linkRemoveConfirm: "Fjern?",
|
|
102
|
+
linkYes: "Ja",
|
|
103
|
+
linkNo: "Nej",
|
|
104
|
+
linkEmpty: "Ingen sider fundet",
|
|
105
|
+
linkLoading: "Henter sider\u2026"
|
|
55
106
|
};
|
|
56
107
|
var uiLabels = DEFAULT_LABELS;
|
|
57
108
|
function resolveOptions(options) {
|
|
@@ -153,6 +204,7 @@ function isExpired(token) {
|
|
|
153
204
|
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>';
|
|
154
205
|
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>';
|
|
155
206
|
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>';
|
|
207
|
+
var LINK_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="M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7"/><path d="M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7"/></svg>';
|
|
156
208
|
function makeIcon() {
|
|
157
209
|
const icon = document.createElement("span");
|
|
158
210
|
icon.style.cssText = "display:inline-flex;line-height:0;flex:0 0 auto";
|
|
@@ -259,14 +311,16 @@ function wireField(el, token, options) {
|
|
|
259
311
|
if (el.getAttribute("contenteditable") === "true") return;
|
|
260
312
|
e.preventDefault();
|
|
261
313
|
e.stopPropagation();
|
|
262
|
-
|
|
314
|
+
const tokenSafe = hasTokenChips(el);
|
|
315
|
+
el.dataset.cmsOriginalValue = tokenSafe ? serializeTokenSafe(el) : el.textContent ?? "";
|
|
263
316
|
el.setAttribute("contenteditable", "true");
|
|
317
|
+
if (tokenSafe) lockTokenChips(el);
|
|
264
318
|
el.focus();
|
|
265
319
|
});
|
|
266
320
|
el.addEventListener("blur", () => {
|
|
267
321
|
el.removeAttribute("contenteditable");
|
|
268
322
|
const original = el.dataset.cmsOriginalValue ?? "";
|
|
269
|
-
const current = el.textContent ?? "";
|
|
323
|
+
const current = hasTokenChips(el) ? serializeTokenSafe(el) : el.textContent ?? "";
|
|
270
324
|
if (current.trim() === original.trim()) return;
|
|
271
325
|
void saveField(el, current.trim(), token, options);
|
|
272
326
|
});
|
|
@@ -309,6 +363,7 @@ function deactivateRich() {
|
|
|
309
363
|
el.removeAttribute("contenteditable");
|
|
310
364
|
el.classList.remove("cms-rich-editing");
|
|
311
365
|
hideRichToolbar();
|
|
366
|
+
hideLinkDialog();
|
|
312
367
|
if (el.innerHTML !== originalHtml) {
|
|
313
368
|
const value = mode === "html" ? el.innerHTML : htmlToMarkdown(el.innerHTML);
|
|
314
369
|
void saveField(el, value, token, options);
|
|
@@ -354,6 +409,10 @@ function buildRichToolbar() {
|
|
|
354
409
|
t.appendChild(toolbarButton(UL_SVG, uiLabels.unorderedList, () => document.execCommand("insertUnorderedList")));
|
|
355
410
|
t.appendChild(toolbarButton(OL_SVG, uiLabels.orderedList, () => document.execCommand("insertOrderedList")));
|
|
356
411
|
t.appendChild(sep());
|
|
412
|
+
const linkBtn = toolbarButton(LINK_SVG, uiLabels.link, () => toggleLinkDialog(linkBtn));
|
|
413
|
+
linkBtn.setAttribute("data-testid", "inline-toolbar-link");
|
|
414
|
+
t.appendChild(linkBtn);
|
|
415
|
+
t.appendChild(sep());
|
|
357
416
|
const clrLabel = document.createElement("label");
|
|
358
417
|
clrLabel.style.cssText = "display:flex;align-items:center;gap:5px;color:#9aa4b2;font-size:12px;cursor:pointer;";
|
|
359
418
|
clrLabel.textContent = uiLabels.color;
|
|
@@ -387,6 +446,231 @@ var EMOJIS = "\u{1F600} \u{1F603} \u{1F604} \u{1F601} \u{1F606} \u{1F609} \u{1F6
|
|
|
387
446
|
);
|
|
388
447
|
var emojiPicker = null;
|
|
389
448
|
var savedRange = null;
|
|
449
|
+
var linkDialog = null;
|
|
450
|
+
var linkPages = null;
|
|
451
|
+
var linkPicked = null;
|
|
452
|
+
var linkEditing = null;
|
|
453
|
+
function hideLinkDialog() {
|
|
454
|
+
if (linkDialog) linkDialog.style.display = "none";
|
|
455
|
+
linkPicked = null;
|
|
456
|
+
linkEditing = null;
|
|
457
|
+
}
|
|
458
|
+
function anchorAtCaret() {
|
|
459
|
+
const sel = window.getSelection();
|
|
460
|
+
if (!sel || sel.rangeCount === 0) return null;
|
|
461
|
+
let node = sel.getRangeAt(0).startContainer;
|
|
462
|
+
while (node && node !== document.body) {
|
|
463
|
+
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === "A") {
|
|
464
|
+
return node;
|
|
465
|
+
}
|
|
466
|
+
node = node.parentNode;
|
|
467
|
+
}
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
async function fetchLinkablePages() {
|
|
471
|
+
if (linkPages) return linkPages;
|
|
472
|
+
if (!richCtx) return [];
|
|
473
|
+
const { options, token } = richCtx;
|
|
474
|
+
const url = `${options.cmsBaseUrl}/api/inline-edit/pages?site=${encodeURIComponent(options.siteId)}`;
|
|
475
|
+
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
476
|
+
if (!res.ok) throw new Error(`pages ${res.status}`);
|
|
477
|
+
const body = await res.json();
|
|
478
|
+
linkPages = body.pages ?? [];
|
|
479
|
+
return linkPages;
|
|
480
|
+
}
|
|
481
|
+
function toggleLinkDialog(anchor) {
|
|
482
|
+
if (linkDialog && linkDialog.style.display === "block") {
|
|
483
|
+
hideLinkDialog();
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const sel = window.getSelection();
|
|
487
|
+
if (sel && sel.rangeCount > 0) savedRange = sel.getRangeAt(0).cloneRange();
|
|
488
|
+
linkEditing = anchorAtCaret();
|
|
489
|
+
if (!linkDialog) linkDialog = buildLinkDialog();
|
|
490
|
+
renderLinkDialog();
|
|
491
|
+
const rect = anchor.getBoundingClientRect();
|
|
492
|
+
linkDialog.style.top = `${rect.bottom + 8}px`;
|
|
493
|
+
linkDialog.style.left = `${Math.min(Math.max(8, rect.left - 180), window.innerWidth - 400)}px`;
|
|
494
|
+
linkDialog.style.display = "block";
|
|
495
|
+
}
|
|
496
|
+
function buildLinkDialog() {
|
|
497
|
+
const d = document.createElement("div");
|
|
498
|
+
d.setAttribute("data-cms-inline-edit-toolbar", "");
|
|
499
|
+
d.setAttribute("data-testid", "inline-link-dialog");
|
|
500
|
+
d.style.cssText = "position:fixed;z-index:2147483646;background:#1c2027;border:1px solid #3a3f4a;border-radius:10px;width:392px;display:none;box-shadow:0 8px 32px rgba(0,0,0,.5);font-family:system-ui,sans-serif;color:#fff;overflow:hidden;";
|
|
501
|
+
d.addEventListener("mousedown", (e) => {
|
|
502
|
+
const t = e.target;
|
|
503
|
+
if (t.tagName !== "INPUT") e.preventDefault();
|
|
504
|
+
e.stopPropagation();
|
|
505
|
+
});
|
|
506
|
+
document.body.appendChild(d);
|
|
507
|
+
return d;
|
|
508
|
+
}
|
|
509
|
+
function renderLinkDialog() {
|
|
510
|
+
const d = linkDialog;
|
|
511
|
+
if (!d) return;
|
|
512
|
+
const L = uiLabels;
|
|
513
|
+
const editing = !!linkEditing;
|
|
514
|
+
const existingRef = linkEditing?.getAttribute("data-cms-ref") ?? "";
|
|
515
|
+
const onPageTab = !editing || !!existingRef;
|
|
516
|
+
d.innerHTML = `<div style="display:flex;gap:4px;padding:8px 8px 0"><button type="button" data-testid="inline-link-tab-page" data-tab="page" style="${tabCss(onPageTab)}">${L.linkTabPage}</button><button type="button" data-testid="inline-link-tab-url" data-tab="url" style="${tabCss(!onPageTab)}">${L.linkTabUrl}</button></div><div style="padding:10px 12px 12px"><div data-pane="page" style="display:${onPageTab ? "block" : "none"}"><input data-testid="inline-link-search" data-role="search" placeholder="${L.linkSearch}" style="${fieldCss()}"><div data-role="list" data-testid="inline-link-list" style="margin-top:8px;max-height:196px;overflow-y:auto;border:1px solid #3a3f4a;border-radius:8px;background:#141821"></div></div><div data-pane="url" style="display:${onPageTab ? "none" : "block"}"><label style="${labelCss()}">${L.linkUrl}</label><input data-testid="inline-link-url" data-role="url" placeholder="https://\u2026" style="${fieldCss()}"></div><label style="${labelCss()}">${L.linkText}</label><input data-testid="inline-link-text" data-role="text" placeholder="${L.linkTextAuto}" style="${fieldCss()}"><p data-role="hint" style="font-size:11.5px;color:#9aa3b2;margin:6px 0 0;line-height:1.5"></p><div data-role="live" style="display:flex;gap:8px;margin-top:12px;padding:9px 10px;background:rgba(0,178,255,.08);border:1px solid rgba(0,178,255,.28);border-radius:8px"><p style="margin:0;font-size:11.5px;color:#c9d1dd;line-height:1.55">${L.linkLiveHint}</p></div><div style="display:flex;gap:8px;align-items:center;margin-top:14px"><div data-role="remove"></div><div style="flex:1"></div><button type="button" data-testid="inline-link-cancel" data-role="cancel" style="${btnCss(false)}">${L.linkCancel}</button><button type="button" data-testid="inline-link-submit" data-role="submit" style="${btnCss(true)}" disabled>${editing ? L.linkSave : L.linkInsert}</button></div></div>`;
|
|
517
|
+
const q = (role) => d.querySelector(`[data-role="${role}"]`);
|
|
518
|
+
const text = q("text");
|
|
519
|
+
const urlIn = q("url");
|
|
520
|
+
const search = q("search");
|
|
521
|
+
if (editing) {
|
|
522
|
+
text.value = linkEditing?.getAttribute("data-cms-ref-label") === "auto" ? "" : linkEditing?.textContent ?? "";
|
|
523
|
+
if (!existingRef) urlIn.value = linkEditing?.getAttribute("href") ?? "";
|
|
524
|
+
q("remove").appendChild(buildRemoveLink());
|
|
525
|
+
}
|
|
526
|
+
d.querySelectorAll("[data-tab]").forEach((btn) => {
|
|
527
|
+
btn.onclick = () => {
|
|
528
|
+
const page = btn.dataset.tab === "page";
|
|
529
|
+
d.querySelectorAll("[data-tab]").forEach((b) => {
|
|
530
|
+
b.setAttribute("style", tabCss(b.dataset.tab === "page" ? page : !page));
|
|
531
|
+
});
|
|
532
|
+
d.querySelector('[data-pane="page"]').style.display = page ? "block" : "none";
|
|
533
|
+
d.querySelector('[data-pane="url"]').style.display = page ? "none" : "block";
|
|
534
|
+
q("live").style.display = page ? "flex" : "none";
|
|
535
|
+
syncLinkDialog();
|
|
536
|
+
};
|
|
537
|
+
});
|
|
538
|
+
search.oninput = () => renderLinkList(search.value);
|
|
539
|
+
text.oninput = syncLinkDialog;
|
|
540
|
+
urlIn.oninput = syncLinkDialog;
|
|
541
|
+
q("cancel").onclick = hideLinkDialog;
|
|
542
|
+
q("submit").onclick = applyLink;
|
|
543
|
+
renderLinkList("");
|
|
544
|
+
syncLinkDialog();
|
|
545
|
+
}
|
|
546
|
+
function buildRemoveLink() {
|
|
547
|
+
const wrap = document.createElement("div");
|
|
548
|
+
const btn = document.createElement("button");
|
|
549
|
+
btn.type = "button";
|
|
550
|
+
btn.setAttribute("data-testid", "inline-link-remove");
|
|
551
|
+
btn.textContent = uiLabels.linkRemove;
|
|
552
|
+
btn.style.cssText = "background:none;border:none;color:#ff8a8a;font-size:12.5px;cursor:pointer;padding:0 2px;";
|
|
553
|
+
btn.onclick = () => {
|
|
554
|
+
wrap.innerHTML = `<span style="font-size:11.5px;color:#ff8a8a;font-weight:500;padding:0 2px">${uiLabels.linkRemoveConfirm}</span><button type="button" data-testid="inline-link-remove-yes" style="font-size:11px;padding:2px 8px;border-radius:4px;border:none;background:#c0392b;color:#fff;cursor:pointer;line-height:1.4;margin-left:6px">${uiLabels.linkYes}</button><button type="button" data-testid="inline-link-remove-no" style="font-size:11px;padding:2px 8px;border-radius:4px;border:1px solid #3a3f4a;background:none;color:#fff;cursor:pointer;line-height:1.4;margin-left:6px">${uiLabels.linkNo}</button>`;
|
|
555
|
+
wrap.querySelector('[data-testid="inline-link-remove-no"]').onclick = () => {
|
|
556
|
+
wrap.innerHTML = "";
|
|
557
|
+
wrap.appendChild(btn);
|
|
558
|
+
};
|
|
559
|
+
wrap.querySelector('[data-testid="inline-link-remove-yes"]').onclick = () => {
|
|
560
|
+
if (linkEditing) {
|
|
561
|
+
const parent = linkEditing.parentNode;
|
|
562
|
+
while (linkEditing.firstChild) parent?.insertBefore(linkEditing.firstChild, linkEditing);
|
|
563
|
+
parent?.removeChild(linkEditing);
|
|
564
|
+
}
|
|
565
|
+
hideLinkDialog();
|
|
566
|
+
};
|
|
567
|
+
};
|
|
568
|
+
wrap.appendChild(btn);
|
|
569
|
+
return wrap;
|
|
570
|
+
}
|
|
571
|
+
function renderLinkList(filter) {
|
|
572
|
+
const d = linkDialog;
|
|
573
|
+
if (!d) return;
|
|
574
|
+
const list = d.querySelector('[data-role="list"]');
|
|
575
|
+
if (!list) return;
|
|
576
|
+
const paint = (pages) => {
|
|
577
|
+
const f = filter.trim().toLowerCase();
|
|
578
|
+
const shown = pages.filter((p) => !f || `${p.title} ${p.path}`.toLowerCase().includes(f));
|
|
579
|
+
if (!shown.length) {
|
|
580
|
+
list.innerHTML = `<div style="padding:10px;font-size:12.5px;color:#9aa3b2">${uiLabels.linkEmpty}</div>`;
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
list.innerHTML = "";
|
|
584
|
+
shown.forEach((p) => {
|
|
585
|
+
const on = linkPicked?.collection === p.collection && linkPicked?.slug === p.slug;
|
|
586
|
+
const row = document.createElement("div");
|
|
587
|
+
row.setAttribute("data-testid", `inline-link-page-${p.collection}-${p.slug}`);
|
|
588
|
+
row.style.cssText = "display:flex;align-items:center;gap:10px;padding:8px 10px;cursor:pointer;border-bottom:1px solid #232833;" + (on ? "background:rgba(0,178,255,.14);" : "");
|
|
589
|
+
row.innerHTML = `<div style="min-width:0;flex:1"><div style="font-size:13.5px;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escapeHtml(p.title)}</div><div style="font-size:11.5px;color:#9aa3b2;font-family:ui-monospace,Menlo,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escapeHtml(p.path)}</div></div><span style="flex:none;font-size:10px;text-transform:uppercase;color:#9aa3b2;border:1px solid #3a3f4a;border-radius:4px;padding:2px 6px;white-space:nowrap">${escapeHtml(p.label)}</span>`;
|
|
590
|
+
row.onclick = () => {
|
|
591
|
+
linkPicked = p;
|
|
592
|
+
renderLinkList(filter);
|
|
593
|
+
syncLinkDialog();
|
|
594
|
+
};
|
|
595
|
+
list.appendChild(row);
|
|
596
|
+
});
|
|
597
|
+
};
|
|
598
|
+
if (linkPages) {
|
|
599
|
+
paint(linkPages);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
list.innerHTML = `<div style="padding:10px;font-size:12.5px;color:#9aa3b2">${uiLabels.linkLoading}</div>`;
|
|
603
|
+
fetchLinkablePages().then((pages) => {
|
|
604
|
+
const ref = linkEditing?.getAttribute("data-cms-ref");
|
|
605
|
+
if (ref && !linkPicked) {
|
|
606
|
+
const [c, ...rest] = ref.split(":");
|
|
607
|
+
linkPicked = pages.find((p) => p.collection === c && p.slug === rest.join(":")) ?? null;
|
|
608
|
+
}
|
|
609
|
+
paint(pages);
|
|
610
|
+
syncLinkDialog();
|
|
611
|
+
}).catch(() => {
|
|
612
|
+
list.innerHTML = `<div style="padding:10px;font-size:12.5px;color:#ff8a8a">${uiLabels.error}</div>`;
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
function syncLinkDialog() {
|
|
616
|
+
const d = linkDialog;
|
|
617
|
+
if (!d) return;
|
|
618
|
+
const q = (role) => d.querySelector(`[data-role="${role}"]`);
|
|
619
|
+
const onPage = d.querySelector('[data-pane="page"]').style.display !== "none";
|
|
620
|
+
const own = q("text").value.trim();
|
|
621
|
+
q("hint").innerHTML = own ? uiLabels.linkTextOwnHint : onPage ? uiLabels.linkTextAutoHint : uiLabels.linkUrlHint;
|
|
622
|
+
q("live").style.display = onPage ? "flex" : "none";
|
|
623
|
+
q("submit").disabled = onPage ? !linkPicked : !q("url").value.trim();
|
|
624
|
+
}
|
|
625
|
+
function applyLink() {
|
|
626
|
+
const d = linkDialog;
|
|
627
|
+
if (!d || !richCtx) return;
|
|
628
|
+
const q = (role) => d.querySelector(`[data-role="${role}"]`);
|
|
629
|
+
const onPage = d.querySelector('[data-pane="page"]').style.display !== "none";
|
|
630
|
+
const own = q("text").value.trim();
|
|
631
|
+
const href = onPage ? linkPicked?.path ?? "" : q("url").value.trim();
|
|
632
|
+
if (!href) return;
|
|
633
|
+
const ref = onPage && linkPicked ? `${linkPicked.collection}:${linkPicked.slug}` : "";
|
|
634
|
+
const label = own || (onPage ? linkPicked?.title ?? href : href);
|
|
635
|
+
if (linkEditing) {
|
|
636
|
+
linkEditing.setAttribute("href", href);
|
|
637
|
+
if (ref) linkEditing.setAttribute("data-cms-ref", ref);
|
|
638
|
+
else linkEditing.removeAttribute("data-cms-ref");
|
|
639
|
+
if (ref && !own) linkEditing.setAttribute("data-cms-ref-label", "auto");
|
|
640
|
+
else linkEditing.removeAttribute("data-cms-ref-label");
|
|
641
|
+
linkEditing.textContent = label;
|
|
642
|
+
hideLinkDialog();
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const a = document.createElement("a");
|
|
646
|
+
a.setAttribute("href", href);
|
|
647
|
+
if (ref) {
|
|
648
|
+
a.setAttribute("data-cms-ref", ref);
|
|
649
|
+
if (!own) a.setAttribute("data-cms-ref-label", "auto");
|
|
650
|
+
}
|
|
651
|
+
const sel = window.getSelection();
|
|
652
|
+
if (savedRange && sel) {
|
|
653
|
+
sel.removeAllRanges();
|
|
654
|
+
sel.addRange(savedRange);
|
|
655
|
+
const range = sel.getRangeAt(0);
|
|
656
|
+
a.textContent = own || range.toString() || label;
|
|
657
|
+
range.deleteContents();
|
|
658
|
+
range.insertNode(a);
|
|
659
|
+
sel.removeAllRanges();
|
|
660
|
+
} else {
|
|
661
|
+
a.textContent = label;
|
|
662
|
+
richCtx.el.appendChild(a);
|
|
663
|
+
}
|
|
664
|
+
savedRange = null;
|
|
665
|
+
hideLinkDialog();
|
|
666
|
+
}
|
|
667
|
+
function escapeHtml(v) {
|
|
668
|
+
return v.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
669
|
+
}
|
|
670
|
+
var tabCss = (on) => "flex:1;background:" + (on ? "#2a2f38" : "none") + ";border:1px solid " + (on ? "#3a3f4a" : "transparent") + ";color:" + (on ? "#fff" : "#9aa3b2") + ";height:32px;border-radius:7px;font-size:13px;cursor:pointer;font-weight:500;font-family:inherit;";
|
|
671
|
+
var fieldCss = () => "width:100%;background:#141821;border:1px solid #3a3f4a;color:#fff;height:34px;border-radius:7px;padding:0 10px;font-size:13.5px;outline:none;font-family:inherit;box-sizing:border-box;";
|
|
672
|
+
var labelCss = () => "font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:#9aa3b2;margin:12px 0 5px;display:block;";
|
|
673
|
+
var btnCss = (primary) => "height:33px;border-radius:7px;font-size:13px;cursor:pointer;padding:0 14px;font-weight:600;font-family:inherit;" + (primary ? "background:#00b2ff;color:#04121b;border:none;" : "background:none;border:1px solid #3a3f4a;color:#fff;font-weight:500;");
|
|
390
674
|
function toggleEmojiPicker(anchor) {
|
|
391
675
|
if (!emojiPicker) emojiPicker = buildEmojiPicker();
|
|
392
676
|
if (emojiPicker.style.display === "block") {
|
|
@@ -470,14 +754,70 @@ function setDeepField(data, path, value) {
|
|
|
470
754
|
function htmlToMarkdown(html) {
|
|
471
755
|
const container = document.createElement("div");
|
|
472
756
|
container.innerHTML = html;
|
|
757
|
+
reattachOrphanListItems(container);
|
|
473
758
|
return serializeBlockChildren(container).replace(/\n{3,}/g, "\n\n").trim() + "\n";
|
|
474
759
|
}
|
|
760
|
+
function isList(el) {
|
|
761
|
+
if (!el) return false;
|
|
762
|
+
const tag = el.tagName.toLowerCase();
|
|
763
|
+
return tag === "ul" || tag === "ol";
|
|
764
|
+
}
|
|
765
|
+
function reattachOrphanListItems(container) {
|
|
766
|
+
const orphans = Array.from(container.children).filter(
|
|
767
|
+
(c) => c.tagName.toLowerCase() === "li"
|
|
768
|
+
);
|
|
769
|
+
for (const li of orphans) {
|
|
770
|
+
const prev = li.previousElementSibling;
|
|
771
|
+
const next = li.nextElementSibling;
|
|
772
|
+
if (isList(prev)) {
|
|
773
|
+
prev.appendChild(li);
|
|
774
|
+
} else if (isList(next)) {
|
|
775
|
+
next.insertBefore(li, next.firstChild);
|
|
776
|
+
} else {
|
|
777
|
+
const list = container.ownerDocument.createElement("ul");
|
|
778
|
+
li.replaceWith(list);
|
|
779
|
+
list.appendChild(li);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
var BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
784
|
+
"h1",
|
|
785
|
+
"h2",
|
|
786
|
+
"h3",
|
|
787
|
+
"h4",
|
|
788
|
+
"h5",
|
|
789
|
+
"h6",
|
|
790
|
+
"p",
|
|
791
|
+
"div",
|
|
792
|
+
"ul",
|
|
793
|
+
"ol",
|
|
794
|
+
"li",
|
|
795
|
+
"blockquote",
|
|
796
|
+
"pre",
|
|
797
|
+
"hr",
|
|
798
|
+
"table"
|
|
799
|
+
]);
|
|
800
|
+
function isBlockNode(node) {
|
|
801
|
+
return node.nodeType === Node.ELEMENT_NODE && BLOCK_TAGS.has(node.tagName.toLowerCase());
|
|
802
|
+
}
|
|
475
803
|
function serializeBlockChildren(parent) {
|
|
476
804
|
const blocks = [];
|
|
477
|
-
|
|
478
|
-
|
|
805
|
+
let inlineRun = "";
|
|
806
|
+
const flushInline = () => {
|
|
807
|
+
const s = inlineRun.trim();
|
|
479
808
|
if (s) blocks.push(s);
|
|
809
|
+
inlineRun = "";
|
|
810
|
+
};
|
|
811
|
+
parent.childNodes.forEach((node) => {
|
|
812
|
+
if (isBlockNode(node)) {
|
|
813
|
+
flushInline();
|
|
814
|
+
const s = serializeBlock(node).trim();
|
|
815
|
+
if (s) blocks.push(s);
|
|
816
|
+
} else {
|
|
817
|
+
inlineRun += serializeInlineNode(node);
|
|
818
|
+
}
|
|
480
819
|
});
|
|
820
|
+
flushInline();
|
|
481
821
|
return blocks.join("\n\n");
|
|
482
822
|
}
|
|
483
823
|
function serializeBlock(node) {
|
|
@@ -505,6 +845,11 @@ function serializeBlock(node) {
|
|
|
505
845
|
return serializeList(el, false);
|
|
506
846
|
case "ol":
|
|
507
847
|
return serializeList(el, true);
|
|
848
|
+
// Backstop for an orphaned <li> that reattachOrphanListItems could not
|
|
849
|
+
// place (e.g. nested inside another block). Without this it falls through
|
|
850
|
+
// to the inline default and loses its marker — silent content loss.
|
|
851
|
+
case "li":
|
|
852
|
+
return "- " + serializeInline(el).trim();
|
|
508
853
|
case "blockquote":
|
|
509
854
|
return serializeBlockChildren(el).split("\n").map((l) => l ? "> " + l : ">").join("\n");
|
|
510
855
|
case "pre":
|
|
@@ -546,14 +891,23 @@ function serializeList(listEl, ordered) {
|
|
|
546
891
|
});
|
|
547
892
|
return items.join("\n");
|
|
548
893
|
}
|
|
894
|
+
function escapeAttr(value) {
|
|
895
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
896
|
+
}
|
|
549
897
|
function serializeInline(node) {
|
|
550
898
|
let out = "";
|
|
551
899
|
node.childNodes.forEach((child) => {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
900
|
+
out += serializeInlineNode(child);
|
|
901
|
+
});
|
|
902
|
+
return out;
|
|
903
|
+
}
|
|
904
|
+
function serializeInlineNode(child) {
|
|
905
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
906
|
+
return (child.textContent ?? "").replace(/\s+/g, " ");
|
|
907
|
+
}
|
|
908
|
+
if (child.nodeType !== Node.ELEMENT_NODE) return "";
|
|
909
|
+
{
|
|
910
|
+
let out = "";
|
|
557
911
|
const el = child;
|
|
558
912
|
const tag = el.tagName.toLowerCase();
|
|
559
913
|
const inner = serializeInline(el);
|
|
@@ -574,7 +928,15 @@ function serializeInline(node) {
|
|
|
574
928
|
break;
|
|
575
929
|
case "a": {
|
|
576
930
|
const href = el.getAttribute("href") || "";
|
|
577
|
-
|
|
931
|
+
const ref = el.getAttribute("data-cms-ref");
|
|
932
|
+
if (ref) {
|
|
933
|
+
const attrs = [`href="${escapeAttr(href)}"`, `data-cms-ref="${escapeAttr(ref)}"`];
|
|
934
|
+
const label = el.getAttribute("data-cms-ref-label");
|
|
935
|
+
if (label) attrs.push(`data-cms-ref-label="${escapeAttr(label)}"`);
|
|
936
|
+
out += `<a ${attrs.join(" ")}>${inner}</a>`;
|
|
937
|
+
} else {
|
|
938
|
+
out += href ? `[${inner}](${href})` : inner;
|
|
939
|
+
}
|
|
578
940
|
break;
|
|
579
941
|
}
|
|
580
942
|
case "img": {
|
|
@@ -595,8 +957,8 @@ function serializeInline(node) {
|
|
|
595
957
|
default:
|
|
596
958
|
out += inner;
|
|
597
959
|
}
|
|
598
|
-
|
|
599
|
-
|
|
960
|
+
return out;
|
|
961
|
+
}
|
|
600
962
|
}
|
|
601
963
|
async function saveField(el, value, token, options) {
|
|
602
964
|
const collection = el.dataset.cmsCollection;
|
|
@@ -704,6 +1066,7 @@ function injectStyles() {
|
|
|
704
1066
|
buildConnectUrl,
|
|
705
1067
|
disconnect,
|
|
706
1068
|
getConnectedToken,
|
|
1069
|
+
htmlToMarkdown,
|
|
707
1070
|
initInlineEdit,
|
|
708
1071
|
rescanFields
|
|
709
1072
|
});
|