@broberg/cms-inline-edit 0.5.3 → 0.5.5

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.d.ts CHANGED
@@ -52,6 +52,7 @@ interface InlineEditLabels {
52
52
  linkNo?: string;
53
53
  linkEmpty?: string;
54
54
  linkLoading?: string;
55
+ linkCurrent?: string;
55
56
  }
56
57
  interface InlineEditOptions {
57
58
  /** Base URL of the CMS API, e.g. "https://webhouse.app". */
@@ -109,6 +110,51 @@ declare function disconnect(options: InlineEditOptions): void;
109
110
  * MPA consumers (broberg: full reload per navigation re-runs init) never need it.
110
111
  */
111
112
  declare function rescanFields(): void;
113
+ interface LinkablePage {
114
+ collection: string;
115
+ slug: string;
116
+ title: string;
117
+ path: string;
118
+ label: string;
119
+ }
120
+ /**
121
+ * Place a fixed-position popover so it stays fully inside the viewport.
122
+ *
123
+ * The link dialog used to be pinned at `rect.bottom + gap` unconditionally,
124
+ * with `overflow:hidden` and no max-height. On a short laptop screen the
125
+ * dialog's footer — where Gem and Annullér live — sat below the bottom edge
126
+ * with no way to reach it: the dialog could not scroll and the page behind it
127
+ * did not move. The editor could open the dialog and fill it in, but not save.
128
+ * (Reported from production 2026-08-26.)
129
+ *
130
+ * Order matters: the element has to be laid out before its height can be
131
+ * measured, so display is switched on first and the final `top` written after.
132
+ *
133
+ * @param width the popover's own fixed width, used to clamp `left`.
134
+ */
135
+ declare function positionPopover(el: HTMLElement, rect: {
136
+ top: number;
137
+ bottom: number;
138
+ left: number;
139
+ }, gap: number, width: number): void;
140
+ /**
141
+ * Find the page an existing `data-cms-ref` points at.
142
+ *
143
+ * A slug may itself contain a colon, so only the FIRST segment is the
144
+ * collection and everything after it is the slug.
145
+ */
146
+ declare function resolveExistingRef(ref: string | null | undefined, pages: LinkablePage[]): LinkablePage | null;
147
+ /**
148
+ * Render the "currently points at" line for the link being edited.
149
+ *
150
+ * Pure over its arguments so the wording can be pinned by a test. Reading the
151
+ * highlighted row is not enough on its own: it can sit outside the list's 196px
152
+ * window, and a reference to a page that is no longer linkable has no row to
153
+ * highlight at all. Christian, 2026-08-26: "når jeg redigerer et eksisterende
154
+ * link til egen side skal den også vise hvilken vi linker til." A missing page
155
+ * shows its raw reference rather than saying nothing.
156
+ */
157
+ declare function renderCurrentRefLine(box: HTMLElement, ref: string | null | undefined, pages: LinkablePage[]): void;
112
158
  /**
113
159
  * Converts an edited contenteditable region's innerHTML back to Markdown so a
114
160
  * rich article body round-trips through the cms `richtext` contract (fields
@@ -122,4 +168,4 @@ declare function rescanFields(): void;
122
168
  */
123
169
  declare function htmlToMarkdown(html: string): string;
124
170
 
125
- export { type InlineEditLabels, type InlineEditOptions, applyFieldSlice, buildConnectUrl, disconnect, getConnectedToken, htmlToMarkdown, initInlineEdit, rescanFields };
171
+ export { type InlineEditLabels, type InlineEditOptions, applyFieldSlice, buildConnectUrl, disconnect, getConnectedToken, htmlToMarkdown, initInlineEdit, positionPopover, renderCurrentRefLine, rescanFields, resolveExistingRef };
package/dist/index.js CHANGED
@@ -70,7 +70,8 @@ var DEFAULT_LABELS = {
70
70
  linkYes: "Ja",
71
71
  linkNo: "Nej",
72
72
  linkEmpty: "Ingen sider fundet",
73
- linkLoading: "Henter sider\u2026"
73
+ linkLoading: "Henter sider\u2026",
74
+ linkCurrent: "Linker til"
74
75
  };
75
76
  var uiLabels = DEFAULT_LABELS;
76
77
  function resolveOptions(options) {
@@ -467,6 +468,22 @@ async function fetchLinkablePages() {
467
468
  linkPages = body.pages ?? [];
468
469
  return linkPages;
469
470
  }
471
+ function positionPopover(el, rect, gap, width) {
472
+ const MARGIN = 8;
473
+ const vh = window.innerHeight;
474
+ const vw = window.innerWidth;
475
+ el.style.maxHeight = `${Math.max(120, vh - MARGIN * 2)}px`;
476
+ el.style.overflowY = "auto";
477
+ el.style.display = "block";
478
+ const h = el.getBoundingClientRect().height || el.offsetHeight || 0;
479
+ let top = rect.bottom + gap;
480
+ if (top + h > vh - MARGIN) {
481
+ const above = rect.top - gap - h;
482
+ top = above >= MARGIN ? above : vh - MARGIN - h;
483
+ }
484
+ el.style.top = `${Math.max(MARGIN, top)}px`;
485
+ el.style.left = `${Math.max(MARGIN, Math.min(rect.left, vw - width - MARGIN))}px`;
486
+ }
470
487
  function toggleLinkDialog(anchor) {
471
488
  if (linkDialog && linkDialog.style.display === "block") {
472
489
  hideLinkDialog();
@@ -478,9 +495,12 @@ function toggleLinkDialog(anchor) {
478
495
  if (!linkDialog) linkDialog = buildLinkDialog();
479
496
  renderLinkDialog();
480
497
  const rect = anchor.getBoundingClientRect();
481
- linkDialog.style.top = `${rect.bottom + 8}px`;
482
- linkDialog.style.left = `${Math.min(Math.max(8, rect.left - 180), window.innerWidth - 400)}px`;
483
- linkDialog.style.display = "block";
498
+ positionPopover(
499
+ linkDialog,
500
+ { top: rect.top, bottom: rect.bottom, left: rect.left - 180 },
501
+ 8,
502
+ 392
503
+ );
484
504
  }
485
505
  function buildLinkDialog() {
486
506
  const d = document.createElement("div");
@@ -502,7 +522,7 @@ function renderLinkDialog() {
502
522
  const editing = !!linkEditing;
503
523
  const existingRef = linkEditing?.getAttribute("data-cms-ref") ?? "";
504
524
  const onPageTab = !editing || !!existingRef;
505
- 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>`;
525
+ 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="current" data-testid="inline-link-current" style="display:none;margin-top:8px;padding:7px 10px;background:rgba(0,178,255,.08);border:1px solid rgba(0,178,255,.28);border-radius:8px;font-size:11.5px;color:#c9d1dd;line-height:1.45"></div><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>`;
506
526
  const q = (role) => d.querySelector(`[data-role="${role}"]`);
507
527
  const text = q("text");
508
528
  const urlIn = q("url");
@@ -557,6 +577,28 @@ function buildRemoveLink() {
557
577
  wrap.appendChild(btn);
558
578
  return wrap;
559
579
  }
580
+ function resolveExistingRef(ref, pages) {
581
+ if (!ref) return null;
582
+ const [collection, ...rest] = ref.split(":");
583
+ const slug = rest.join(":");
584
+ if (!collection || !slug) return null;
585
+ return pages.find((p) => p.collection === collection && p.slug === slug) ?? null;
586
+ }
587
+ function renderCurrentRefLine(box, ref, pages) {
588
+ if (!ref) {
589
+ box.style.display = "none";
590
+ box.innerHTML = "";
591
+ return;
592
+ }
593
+ const label = escapeHtml(uiLabels.linkCurrent);
594
+ const page = resolveExistingRef(ref, pages);
595
+ box.style.display = "block";
596
+ box.innerHTML = page ? `${label}: <strong>${escapeHtml(page.title)}</strong> <span style="color:#9aa3b2;font-family:ui-monospace,Menlo,monospace">${escapeHtml(page.path)}</span>` : `${label}: <span style="font-family:ui-monospace,Menlo,monospace">${escapeHtml(ref)}</span>`;
597
+ }
598
+ function paintCurrentRef(pages) {
599
+ const box = linkDialog?.querySelector('[data-role="current"]');
600
+ if (box) renderCurrentRefLine(box, linkEditing?.getAttribute("data-cms-ref"), pages ?? []);
601
+ }
560
602
  function renderLinkList(filter) {
561
603
  const d = linkDialog;
562
604
  if (!d) return;
@@ -582,19 +624,21 @@ function renderLinkList(filter) {
582
624
  syncLinkDialog();
583
625
  };
584
626
  list.appendChild(row);
627
+ if (on) row.scrollIntoView?.({ block: "nearest" });
585
628
  });
586
629
  };
630
+ const preselect = (pages) => {
631
+ if (!linkPicked) linkPicked = resolveExistingRef(linkEditing?.getAttribute("data-cms-ref"), pages);
632
+ paintCurrentRef(pages);
633
+ };
587
634
  if (linkPages) {
635
+ preselect(linkPages);
588
636
  paint(linkPages);
589
637
  return;
590
638
  }
591
639
  list.innerHTML = `<div style="padding:10px;font-size:12.5px;color:#9aa3b2">${uiLabels.linkLoading}</div>`;
592
640
  fetchLinkablePages().then((pages) => {
593
- const ref = linkEditing?.getAttribute("data-cms-ref");
594
- if (ref && !linkPicked) {
595
- const [c, ...rest] = ref.split(":");
596
- linkPicked = pages.find((p) => p.collection === c && p.slug === rest.join(":")) ?? null;
597
- }
641
+ preselect(pages);
598
642
  paint(pages);
599
643
  syncLinkDialog();
600
644
  }).catch(() => {
@@ -669,9 +713,7 @@ function toggleEmojiPicker(anchor) {
669
713
  const sel = window.getSelection();
670
714
  if (sel && sel.rangeCount > 0) savedRange = sel.getRangeAt(0).cloneRange();
671
715
  const rect = anchor.getBoundingClientRect();
672
- emojiPicker.style.top = `${rect.bottom + 6}px`;
673
- emojiPicker.style.left = `${Math.min(rect.left, window.innerWidth - 240)}px`;
674
- emojiPicker.style.display = "block";
716
+ positionPopover(emojiPicker, rect, 6, 230);
675
717
  }
676
718
  function hideEmojiPicker() {
677
719
  if (emojiPicker) emojiPicker.style.display = "none";
@@ -804,7 +846,7 @@ function serializeBlockChildren(parent) {
804
846
  const s = serializeBlock(node).trim();
805
847
  if (s) blocks.push(s);
806
848
  } else {
807
- inlineRun += serializeInlineNode(node);
849
+ inlineRun = joinInline(inlineRun, serializeInlineNode(node));
808
850
  }
809
851
  });
810
852
  flushInline();
@@ -884,10 +926,41 @@ function serializeList(listEl, ordered) {
884
926
  function escapeAttr(value) {
885
927
  return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
886
928
  }
929
+ function preservedLinkAttrs(el) {
930
+ const out = [];
931
+ for (const attr of Array.from(el.attributes)) {
932
+ const name = attr.name.toLowerCase();
933
+ if (name === "href") continue;
934
+ if (name.startsWith("on")) continue;
935
+ if (/^javascript:/i.test(attr.value.trim())) continue;
936
+ out.push(`${name}="${escapeAttr(attr.value)}"`);
937
+ }
938
+ return out;
939
+ }
940
+ var HARD_BREAK = " \n";
941
+ function edgeWhitespace(ws) {
942
+ if (!ws) return "";
943
+ return /\n/.test(ws) ? HARD_BREAK : " ";
944
+ }
945
+ function wrapEmphasis(inner, delimiter) {
946
+ const core = inner.trim();
947
+ if (!core) return "";
948
+ const lead = edgeWhitespace(inner.slice(0, inner.length - inner.trimStart().length));
949
+ const trail = edgeWhitespace(inner.slice(inner.trimEnd().length));
950
+ return lead + delimiter + core + delimiter + trail;
951
+ }
952
+ function joinInline(out, piece) {
953
+ if (!piece) return out;
954
+ const outTail = /\s*$/.exec(out)[0];
955
+ const pieceHead = /^\s*/.exec(piece)[0];
956
+ if (!outTail || !pieceHead) return out + piece;
957
+ const merged = /\n/.test(outTail + pieceHead) ? HARD_BREAK : " ";
958
+ return out.slice(0, out.length - outTail.length) + merged + piece.slice(pieceHead.length);
959
+ }
887
960
  function serializeInline(node) {
888
961
  let out = "";
889
962
  node.childNodes.forEach((child) => {
890
- out += serializeInlineNode(child);
963
+ out = joinInline(out, serializeInlineNode(child));
891
964
  });
892
965
  return out;
893
966
  }
@@ -904,11 +977,11 @@ function serializeInlineNode(child) {
904
977
  switch (tag) {
905
978
  case "strong":
906
979
  case "b":
907
- out += inner.trim() ? `**${inner.trim()}**` : "";
980
+ out += wrapEmphasis(inner, "**");
908
981
  break;
909
982
  case "em":
910
983
  case "i":
911
- out += inner.trim() ? `*${inner.trim()}*` : "";
984
+ out += wrapEmphasis(inner, "*");
912
985
  break;
913
986
  case "code":
914
987
  out += "`" + inner + "`";
@@ -918,12 +991,9 @@ function serializeInlineNode(child) {
918
991
  break;
919
992
  case "a": {
920
993
  const href = el.getAttribute("href") || "";
921
- const ref = el.getAttribute("data-cms-ref");
922
- if (ref) {
923
- const attrs = [`href="${escapeAttr(href)}"`, `data-cms-ref="${escapeAttr(ref)}"`];
924
- const label = el.getAttribute("data-cms-ref-label");
925
- if (label) attrs.push(`data-cms-ref-label="${escapeAttr(label)}"`);
926
- out += `<a ${attrs.join(" ")}>${inner}</a>`;
994
+ const extras = preservedLinkAttrs(el);
995
+ if (extras.length > 0) {
996
+ out += `<a ${[`href="${escapeAttr(href)}"`, ...extras].join(" ")}>${inner}</a>`;
927
997
  } else {
928
998
  out += href ? `[${inner}](${href})` : inner;
929
999
  }
@@ -1057,6 +1127,9 @@ export {
1057
1127
  getConnectedToken,
1058
1128
  htmlToMarkdown,
1059
1129
  initInlineEdit,
1060
- rescanFields
1130
+ positionPopover,
1131
+ renderCurrentRefLine,
1132
+ rescanFields,
1133
+ resolveExistingRef
1061
1134
  };
1062
1135
  //# sourceMappingURL=index.js.map