@broberg/cms-inline-edit 0.4.21 → 0.5.1

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.js CHANGED
@@ -50,7 +50,27 @@ var DEFAULT_LABELS = {
50
50
  done: "F\xE6rdig",
51
51
  saving: "Gemmer\u2026",
52
52
  saved: "Gemt \u2713",
53
- error: "Fejl \u2014 pr\xF8v igen"
53
+ error: "Fejl \u2014 pr\xF8v igen",
54
+ link: "Link",
55
+ linkTabPage: "Side p\xE5 sitet",
56
+ linkTabUrl: "Fri adresse",
57
+ linkSearch: "S\xF8g efter en side\u2026",
58
+ linkText: "Linktekst",
59
+ linkTextAuto: "F\xF8lger sidens titel",
60
+ linkTextAutoHint: "<b>Tom = f\xF8lger sidens titel.</b> Omd\xF8bes siden, retter teksten sig selv. Skriver du din egen tekst, bliver den st\xE5ende.",
61
+ linkTextOwnHint: "Linket viser <b>din egen tekst</b>. Den bliver st\xE5ende, ogs\xE5 hvis siden omd\xF8bes.",
62
+ 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.",
63
+ linkUrlHint: "En fri adresse peger pr\xE6cis d\xE9r, du skriver. Den f\xF8lger <b>ikke</b> med, hvis m\xE5let flytter sig.",
64
+ linkUrl: "Adresse",
65
+ linkInsert: "Inds\xE6t link",
66
+ linkSave: "Gem \xE6ndring",
67
+ linkCancel: "Annull\xE9r",
68
+ linkRemove: "Fjern link",
69
+ linkRemoveConfirm: "Fjern?",
70
+ linkYes: "Ja",
71
+ linkNo: "Nej",
72
+ linkEmpty: "Ingen sider fundet",
73
+ linkLoading: "Henter sider\u2026"
54
74
  };
55
75
  var uiLabels = DEFAULT_LABELS;
56
76
  function resolveOptions(options) {
@@ -111,12 +131,18 @@ function getConnectedToken(options) {
111
131
  captureTokenFromUrl(resolved);
112
132
  const token = localStorage.getItem(resolved.storageKey);
113
133
  if (!token) return null;
114
- if (isExpired(token)) {
134
+ if (isExpired(token) || !isForThisSite(token, resolved.siteId)) {
115
135
  localStorage.removeItem(resolved.storageKey);
116
136
  return null;
117
137
  }
118
138
  return token;
119
139
  }
140
+ function isForThisSite(token, siteId) {
141
+ const claims = decodeJwtPayload(token);
142
+ const site = claims?.site;
143
+ if (typeof site !== "string" || !site) return true;
144
+ return site === siteId;
145
+ }
120
146
  function buildConnectUrl(options, returnUrl) {
121
147
  const resolved = resolveOptions(options);
122
148
  return `${resolved.cmsBaseUrl}/admin/inline-edit/connect?site=${encodeURIComponent(resolved.siteId)}&return=${encodeURIComponent(returnUrl)}`;
@@ -152,6 +178,7 @@ function isExpired(token) {
152
178
  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
179
  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
180
  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>';
181
+ 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>';
155
182
  function makeIcon() {
156
183
  const icon = document.createElement("span");
157
184
  icon.style.cssText = "display:inline-flex;line-height:0;flex:0 0 auto";
@@ -310,6 +337,7 @@ function deactivateRich() {
310
337
  el.removeAttribute("contenteditable");
311
338
  el.classList.remove("cms-rich-editing");
312
339
  hideRichToolbar();
340
+ hideLinkDialog();
313
341
  if (el.innerHTML !== originalHtml) {
314
342
  const value = mode === "html" ? el.innerHTML : htmlToMarkdown(el.innerHTML);
315
343
  void saveField(el, value, token, options);
@@ -355,6 +383,10 @@ function buildRichToolbar() {
355
383
  t.appendChild(toolbarButton(UL_SVG, uiLabels.unorderedList, () => document.execCommand("insertUnorderedList")));
356
384
  t.appendChild(toolbarButton(OL_SVG, uiLabels.orderedList, () => document.execCommand("insertOrderedList")));
357
385
  t.appendChild(sep());
386
+ const linkBtn = toolbarButton(LINK_SVG, uiLabels.link, () => toggleLinkDialog(linkBtn));
387
+ linkBtn.setAttribute("data-testid", "inline-toolbar-link");
388
+ t.appendChild(linkBtn);
389
+ t.appendChild(sep());
358
390
  const clrLabel = document.createElement("label");
359
391
  clrLabel.style.cssText = "display:flex;align-items:center;gap:5px;color:#9aa4b2;font-size:12px;cursor:pointer;";
360
392
  clrLabel.textContent = uiLabels.color;
@@ -388,6 +420,231 @@ var EMOJIS = "\u{1F600} \u{1F603} \u{1F604} \u{1F601} \u{1F606} \u{1F609} \u{1F6
388
420
  );
389
421
  var emojiPicker = null;
390
422
  var savedRange = null;
423
+ var linkDialog = null;
424
+ var linkPages = null;
425
+ var linkPicked = null;
426
+ var linkEditing = null;
427
+ function hideLinkDialog() {
428
+ if (linkDialog) linkDialog.style.display = "none";
429
+ linkPicked = null;
430
+ linkEditing = null;
431
+ }
432
+ function anchorAtCaret() {
433
+ const sel = window.getSelection();
434
+ if (!sel || sel.rangeCount === 0) return null;
435
+ let node = sel.getRangeAt(0).startContainer;
436
+ while (node && node !== document.body) {
437
+ if (node.nodeType === Node.ELEMENT_NODE && node.tagName === "A") {
438
+ return node;
439
+ }
440
+ node = node.parentNode;
441
+ }
442
+ return null;
443
+ }
444
+ async function fetchLinkablePages() {
445
+ if (linkPages) return linkPages;
446
+ if (!richCtx) return [];
447
+ const { options, token } = richCtx;
448
+ const url = `${options.cmsBaseUrl}/api/inline-edit/pages?site=${encodeURIComponent(options.siteId)}`;
449
+ const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
450
+ if (!res.ok) throw new Error(`pages ${res.status}`);
451
+ const body = await res.json();
452
+ linkPages = body.pages ?? [];
453
+ return linkPages;
454
+ }
455
+ function toggleLinkDialog(anchor) {
456
+ if (linkDialog && linkDialog.style.display === "block") {
457
+ hideLinkDialog();
458
+ return;
459
+ }
460
+ const sel = window.getSelection();
461
+ if (sel && sel.rangeCount > 0) savedRange = sel.getRangeAt(0).cloneRange();
462
+ linkEditing = anchorAtCaret();
463
+ if (!linkDialog) linkDialog = buildLinkDialog();
464
+ renderLinkDialog();
465
+ const rect = anchor.getBoundingClientRect();
466
+ linkDialog.style.top = `${rect.bottom + 8}px`;
467
+ linkDialog.style.left = `${Math.min(Math.max(8, rect.left - 180), window.innerWidth - 400)}px`;
468
+ linkDialog.style.display = "block";
469
+ }
470
+ function buildLinkDialog() {
471
+ const d = document.createElement("div");
472
+ d.setAttribute("data-cms-inline-edit-toolbar", "");
473
+ d.setAttribute("data-testid", "inline-link-dialog");
474
+ 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;";
475
+ d.addEventListener("mousedown", (e) => {
476
+ const t = e.target;
477
+ if (t.tagName !== "INPUT") e.preventDefault();
478
+ e.stopPropagation();
479
+ });
480
+ document.body.appendChild(d);
481
+ return d;
482
+ }
483
+ function renderLinkDialog() {
484
+ const d = linkDialog;
485
+ if (!d) return;
486
+ const L = uiLabels;
487
+ const editing = !!linkEditing;
488
+ const existingRef = linkEditing?.getAttribute("data-cms-ref") ?? "";
489
+ const onPageTab = !editing || !!existingRef;
490
+ 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>`;
491
+ const q = (role) => d.querySelector(`[data-role="${role}"]`);
492
+ const text = q("text");
493
+ const urlIn = q("url");
494
+ const search = q("search");
495
+ if (editing) {
496
+ text.value = linkEditing?.getAttribute("data-cms-ref-label") === "auto" ? "" : linkEditing?.textContent ?? "";
497
+ if (!existingRef) urlIn.value = linkEditing?.getAttribute("href") ?? "";
498
+ q("remove").appendChild(buildRemoveLink());
499
+ }
500
+ d.querySelectorAll("[data-tab]").forEach((btn) => {
501
+ btn.onclick = () => {
502
+ const page = btn.dataset.tab === "page";
503
+ d.querySelectorAll("[data-tab]").forEach((b) => {
504
+ b.setAttribute("style", tabCss(b.dataset.tab === "page" ? page : !page));
505
+ });
506
+ d.querySelector('[data-pane="page"]').style.display = page ? "block" : "none";
507
+ d.querySelector('[data-pane="url"]').style.display = page ? "none" : "block";
508
+ q("live").style.display = page ? "flex" : "none";
509
+ syncLinkDialog();
510
+ };
511
+ });
512
+ search.oninput = () => renderLinkList(search.value);
513
+ text.oninput = syncLinkDialog;
514
+ urlIn.oninput = syncLinkDialog;
515
+ q("cancel").onclick = hideLinkDialog;
516
+ q("submit").onclick = applyLink;
517
+ renderLinkList("");
518
+ syncLinkDialog();
519
+ }
520
+ function buildRemoveLink() {
521
+ const wrap = document.createElement("div");
522
+ const btn = document.createElement("button");
523
+ btn.type = "button";
524
+ btn.setAttribute("data-testid", "inline-link-remove");
525
+ btn.textContent = uiLabels.linkRemove;
526
+ btn.style.cssText = "background:none;border:none;color:#ff8a8a;font-size:12.5px;cursor:pointer;padding:0 2px;";
527
+ btn.onclick = () => {
528
+ 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>`;
529
+ wrap.querySelector('[data-testid="inline-link-remove-no"]').onclick = () => {
530
+ wrap.innerHTML = "";
531
+ wrap.appendChild(btn);
532
+ };
533
+ wrap.querySelector('[data-testid="inline-link-remove-yes"]').onclick = () => {
534
+ if (linkEditing) {
535
+ const parent = linkEditing.parentNode;
536
+ while (linkEditing.firstChild) parent?.insertBefore(linkEditing.firstChild, linkEditing);
537
+ parent?.removeChild(linkEditing);
538
+ }
539
+ hideLinkDialog();
540
+ };
541
+ };
542
+ wrap.appendChild(btn);
543
+ return wrap;
544
+ }
545
+ function renderLinkList(filter) {
546
+ const d = linkDialog;
547
+ if (!d) return;
548
+ const list = d.querySelector('[data-role="list"]');
549
+ if (!list) return;
550
+ const paint = (pages) => {
551
+ const f = filter.trim().toLowerCase();
552
+ const shown = pages.filter((p) => !f || `${p.title} ${p.path}`.toLowerCase().includes(f));
553
+ if (!shown.length) {
554
+ list.innerHTML = `<div style="padding:10px;font-size:12.5px;color:#9aa3b2">${uiLabels.linkEmpty}</div>`;
555
+ return;
556
+ }
557
+ list.innerHTML = "";
558
+ shown.forEach((p) => {
559
+ const on = linkPicked?.collection === p.collection && linkPicked?.slug === p.slug;
560
+ const row = document.createElement("div");
561
+ row.setAttribute("data-testid", `inline-link-page-${p.collection}-${p.slug}`);
562
+ 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);" : "");
563
+ 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>`;
564
+ row.onclick = () => {
565
+ linkPicked = p;
566
+ renderLinkList(filter);
567
+ syncLinkDialog();
568
+ };
569
+ list.appendChild(row);
570
+ });
571
+ };
572
+ if (linkPages) {
573
+ paint(linkPages);
574
+ return;
575
+ }
576
+ list.innerHTML = `<div style="padding:10px;font-size:12.5px;color:#9aa3b2">${uiLabels.linkLoading}</div>`;
577
+ fetchLinkablePages().then((pages) => {
578
+ const ref = linkEditing?.getAttribute("data-cms-ref");
579
+ if (ref && !linkPicked) {
580
+ const [c, ...rest] = ref.split(":");
581
+ linkPicked = pages.find((p) => p.collection === c && p.slug === rest.join(":")) ?? null;
582
+ }
583
+ paint(pages);
584
+ syncLinkDialog();
585
+ }).catch(() => {
586
+ list.innerHTML = `<div style="padding:10px;font-size:12.5px;color:#ff8a8a">${uiLabels.error}</div>`;
587
+ });
588
+ }
589
+ function syncLinkDialog() {
590
+ const d = linkDialog;
591
+ if (!d) return;
592
+ const q = (role) => d.querySelector(`[data-role="${role}"]`);
593
+ const onPage = d.querySelector('[data-pane="page"]').style.display !== "none";
594
+ const own = q("text").value.trim();
595
+ q("hint").innerHTML = own ? uiLabels.linkTextOwnHint : onPage ? uiLabels.linkTextAutoHint : uiLabels.linkUrlHint;
596
+ q("live").style.display = onPage ? "flex" : "none";
597
+ q("submit").disabled = onPage ? !linkPicked : !q("url").value.trim();
598
+ }
599
+ function applyLink() {
600
+ const d = linkDialog;
601
+ if (!d || !richCtx) return;
602
+ const q = (role) => d.querySelector(`[data-role="${role}"]`);
603
+ const onPage = d.querySelector('[data-pane="page"]').style.display !== "none";
604
+ const own = q("text").value.trim();
605
+ const href = onPage ? linkPicked?.path ?? "" : q("url").value.trim();
606
+ if (!href) return;
607
+ const ref = onPage && linkPicked ? `${linkPicked.collection}:${linkPicked.slug}` : "";
608
+ const label = own || (onPage ? linkPicked?.title ?? href : href);
609
+ if (linkEditing) {
610
+ linkEditing.setAttribute("href", href);
611
+ if (ref) linkEditing.setAttribute("data-cms-ref", ref);
612
+ else linkEditing.removeAttribute("data-cms-ref");
613
+ if (ref && !own) linkEditing.setAttribute("data-cms-ref-label", "auto");
614
+ else linkEditing.removeAttribute("data-cms-ref-label");
615
+ linkEditing.textContent = label;
616
+ hideLinkDialog();
617
+ return;
618
+ }
619
+ const a = document.createElement("a");
620
+ a.setAttribute("href", href);
621
+ if (ref) {
622
+ a.setAttribute("data-cms-ref", ref);
623
+ if (!own) a.setAttribute("data-cms-ref-label", "auto");
624
+ }
625
+ const sel = window.getSelection();
626
+ if (savedRange && sel) {
627
+ sel.removeAllRanges();
628
+ sel.addRange(savedRange);
629
+ const range = sel.getRangeAt(0);
630
+ a.textContent = own || range.toString() || label;
631
+ range.deleteContents();
632
+ range.insertNode(a);
633
+ sel.removeAllRanges();
634
+ } else {
635
+ a.textContent = label;
636
+ richCtx.el.appendChild(a);
637
+ }
638
+ savedRange = null;
639
+ hideLinkDialog();
640
+ }
641
+ function escapeHtml(v) {
642
+ return v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
643
+ }
644
+ 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;";
645
+ 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;";
646
+ var labelCss = () => "font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:#9aa3b2;margin:12px 0 5px;display:block;";
647
+ 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;");
391
648
  function toggleEmojiPicker(anchor) {
392
649
  if (!emojiPicker) emojiPicker = buildEmojiPicker();
393
650
  if (emojiPicker.style.display === "block") {
@@ -497,12 +754,44 @@ function reattachOrphanListItems(container) {
497
754
  }
498
755
  }
499
756
  }
757
+ var BLOCK_TAGS = /* @__PURE__ */ new Set([
758
+ "h1",
759
+ "h2",
760
+ "h3",
761
+ "h4",
762
+ "h5",
763
+ "h6",
764
+ "p",
765
+ "div",
766
+ "ul",
767
+ "ol",
768
+ "li",
769
+ "blockquote",
770
+ "pre",
771
+ "hr",
772
+ "table"
773
+ ]);
774
+ function isBlockNode(node) {
775
+ return node.nodeType === Node.ELEMENT_NODE && BLOCK_TAGS.has(node.tagName.toLowerCase());
776
+ }
500
777
  function serializeBlockChildren(parent) {
501
778
  const blocks = [];
502
- parent.childNodes.forEach((node) => {
503
- const s = serializeBlock(node).trim();
779
+ let inlineRun = "";
780
+ const flushInline = () => {
781
+ const s = inlineRun.trim();
504
782
  if (s) blocks.push(s);
783
+ inlineRun = "";
784
+ };
785
+ parent.childNodes.forEach((node) => {
786
+ if (isBlockNode(node)) {
787
+ flushInline();
788
+ const s = serializeBlock(node).trim();
789
+ if (s) blocks.push(s);
790
+ } else {
791
+ inlineRun += serializeInlineNode(node);
792
+ }
505
793
  });
794
+ flushInline();
506
795
  return blocks.join("\n\n");
507
796
  }
508
797
  function serializeBlock(node) {
@@ -576,14 +865,23 @@ function serializeList(listEl, ordered) {
576
865
  });
577
866
  return items.join("\n");
578
867
  }
868
+ function escapeAttr(value) {
869
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
870
+ }
579
871
  function serializeInline(node) {
580
872
  let out = "";
581
873
  node.childNodes.forEach((child) => {
582
- if (child.nodeType === Node.TEXT_NODE) {
583
- out += (child.textContent ?? "").replace(/\s+/g, " ");
584
- return;
585
- }
586
- if (child.nodeType !== Node.ELEMENT_NODE) return;
874
+ out += serializeInlineNode(child);
875
+ });
876
+ return out;
877
+ }
878
+ function serializeInlineNode(child) {
879
+ if (child.nodeType === Node.TEXT_NODE) {
880
+ return (child.textContent ?? "").replace(/\s+/g, " ");
881
+ }
882
+ if (child.nodeType !== Node.ELEMENT_NODE) return "";
883
+ {
884
+ let out = "";
587
885
  const el = child;
588
886
  const tag = el.tagName.toLowerCase();
589
887
  const inner = serializeInline(el);
@@ -604,7 +902,15 @@ function serializeInline(node) {
604
902
  break;
605
903
  case "a": {
606
904
  const href = el.getAttribute("href") || "";
607
- out += href ? `[${inner}](${href})` : inner;
905
+ const ref = el.getAttribute("data-cms-ref");
906
+ if (ref) {
907
+ const attrs = [`href="${escapeAttr(href)}"`, `data-cms-ref="${escapeAttr(ref)}"`];
908
+ const label = el.getAttribute("data-cms-ref-label");
909
+ if (label) attrs.push(`data-cms-ref-label="${escapeAttr(label)}"`);
910
+ out += `<a ${attrs.join(" ")}>${inner}</a>`;
911
+ } else {
912
+ out += href ? `[${inner}](${href})` : inner;
913
+ }
608
914
  break;
609
915
  }
610
916
  case "img": {
@@ -625,8 +931,8 @@ function serializeInline(node) {
625
931
  default:
626
932
  out += inner;
627
933
  }
628
- });
629
- return out;
934
+ return out;
935
+ }
630
936
  }
631
937
  async function saveField(el, value, token, options) {
632
938
  const collection = el.dataset.cmsCollection;