@broberg/cms-inline-edit 0.4.21 → 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 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
@@ -82,7 +82,27 @@ var DEFAULT_LABELS = {
82
82
  done: "F\xE6rdig",
83
83
  saving: "Gemmer\u2026",
84
84
  saved: "Gemt \u2713",
85
- 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"
86
106
  };
87
107
  var uiLabels = DEFAULT_LABELS;
88
108
  function resolveOptions(options) {
@@ -184,6 +204,7 @@ function isExpired(token) {
184
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>';
185
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>';
186
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>';
187
208
  function makeIcon() {
188
209
  const icon = document.createElement("span");
189
210
  icon.style.cssText = "display:inline-flex;line-height:0;flex:0 0 auto";
@@ -342,6 +363,7 @@ function deactivateRich() {
342
363
  el.removeAttribute("contenteditable");
343
364
  el.classList.remove("cms-rich-editing");
344
365
  hideRichToolbar();
366
+ hideLinkDialog();
345
367
  if (el.innerHTML !== originalHtml) {
346
368
  const value = mode === "html" ? el.innerHTML : htmlToMarkdown(el.innerHTML);
347
369
  void saveField(el, value, token, options);
@@ -387,6 +409,10 @@ function buildRichToolbar() {
387
409
  t.appendChild(toolbarButton(UL_SVG, uiLabels.unorderedList, () => document.execCommand("insertUnorderedList")));
388
410
  t.appendChild(toolbarButton(OL_SVG, uiLabels.orderedList, () => document.execCommand("insertOrderedList")));
389
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());
390
416
  const clrLabel = document.createElement("label");
391
417
  clrLabel.style.cssText = "display:flex;align-items:center;gap:5px;color:#9aa4b2;font-size:12px;cursor:pointer;";
392
418
  clrLabel.textContent = uiLabels.color;
@@ -420,6 +446,231 @@ var EMOJIS = "\u{1F600} \u{1F603} \u{1F604} \u{1F601} \u{1F606} \u{1F609} \u{1F6
420
446
  );
421
447
  var emojiPicker = null;
422
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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;");
423
674
  function toggleEmojiPicker(anchor) {
424
675
  if (!emojiPicker) emojiPicker = buildEmojiPicker();
425
676
  if (emojiPicker.style.display === "block") {
@@ -529,12 +780,44 @@ function reattachOrphanListItems(container) {
529
780
  }
530
781
  }
531
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
+ }
532
803
  function serializeBlockChildren(parent) {
533
804
  const blocks = [];
534
- parent.childNodes.forEach((node) => {
535
- const s = serializeBlock(node).trim();
805
+ let inlineRun = "";
806
+ const flushInline = () => {
807
+ const s = inlineRun.trim();
536
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
+ }
537
819
  });
820
+ flushInline();
538
821
  return blocks.join("\n\n");
539
822
  }
540
823
  function serializeBlock(node) {
@@ -608,14 +891,23 @@ function serializeList(listEl, ordered) {
608
891
  });
609
892
  return items.join("\n");
610
893
  }
894
+ function escapeAttr(value) {
895
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
896
+ }
611
897
  function serializeInline(node) {
612
898
  let out = "";
613
899
  node.childNodes.forEach((child) => {
614
- if (child.nodeType === Node.TEXT_NODE) {
615
- out += (child.textContent ?? "").replace(/\s+/g, " ");
616
- return;
617
- }
618
- if (child.nodeType !== Node.ELEMENT_NODE) return;
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 = "";
619
911
  const el = child;
620
912
  const tag = el.tagName.toLowerCase();
621
913
  const inner = serializeInline(el);
@@ -636,7 +928,15 @@ function serializeInline(node) {
636
928
  break;
637
929
  case "a": {
638
930
  const href = el.getAttribute("href") || "";
639
- out += href ? `[${inner}](${href})` : inner;
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
+ }
640
940
  break;
641
941
  }
642
942
  case "img": {
@@ -657,8 +957,8 @@ function serializeInline(node) {
657
957
  default:
658
958
  out += inner;
659
959
  }
660
- });
661
- return out;
960
+ return out;
961
+ }
662
962
  }
663
963
  async function saveField(el, value, token, options) {
664
964
  const collection = el.dataset.cmsCollection;