@broberg/cms-inline-edit 0.5.4 → 0.6.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/dist/index.cjs CHANGED
@@ -26,7 +26,11 @@ __export(index_exports, {
26
26
  getConnectedToken: () => getConnectedToken,
27
27
  htmlToMarkdown: () => htmlToMarkdown,
28
28
  initInlineEdit: () => initInlineEdit,
29
- rescanFields: () => rescanFields
29
+ positionPopover: () => positionPopover,
30
+ renderCurrentRefLine: () => renderCurrentRefLine,
31
+ rescanFields: () => rescanFields,
32
+ resolveExistingRef: () => resolveExistingRef,
33
+ setLinkTarget: () => setLinkTarget
30
34
  });
31
35
  module.exports = __toCommonJS(index_exports);
32
36
 
@@ -40,6 +44,30 @@ function applyFieldSlice(current, original, next) {
40
44
  return current.slice(0, first) + next + current.slice(first + original.length);
41
45
  }
42
46
 
47
+ // src/link-target.ts
48
+ function isSchemeless(raw) {
49
+ const v = (raw || "").trim();
50
+ if (!v) return false;
51
+ if (/^[a-z][a-z0-9+.-]*:/i.test(v)) return false;
52
+ if (v.startsWith("//")) return false;
53
+ if (v.startsWith("/")) return false;
54
+ if (v.startsWith("#")) return false;
55
+ if (v.startsWith("?")) return false;
56
+ return true;
57
+ }
58
+ function withHttps(raw) {
59
+ return `https://${(raw || "").trim().replace(/^\/+/, "")}`;
60
+ }
61
+ function isExternalHost(raw, currentHost) {
62
+ const v = (raw || "").trim();
63
+ if (!/^https?:\/\//i.test(v)) return false;
64
+ try {
65
+ return new URL(v).host.toLowerCase() !== (currentHost || "").toLowerCase();
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
43
71
  // src/token-safe.ts
44
72
  var TEXT_NODE = 3;
45
73
  var ELEMENT_NODE = 1;
@@ -94,6 +122,9 @@ var DEFAULT_LABELS = {
94
122
  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
123
  linkUrlHint: "En fri adresse peger pr\xE6cis d\xE9r, du skriver. Den f\xF8lger <b>ikke</b> med, hvis m\xE5let flytter sig.",
96
124
  linkUrl: "Adresse",
125
+ linkNewTab: "\xC5bn i ny fane",
126
+ linkSchemeless: "<b>{v}</b> l\xE6ses som en side p\xE5 <b>dette</b> site \u2014 ikke som en adresse ude p\xE5 nettet.",
127
+ linkSchemelessFix: "Tilf\xF8j https://",
97
128
  linkInsert: "Inds\xE6t link",
98
129
  linkSave: "Gem \xE6ndring",
99
130
  linkCancel: "Annull\xE9r",
@@ -102,7 +133,8 @@ var DEFAULT_LABELS = {
102
133
  linkYes: "Ja",
103
134
  linkNo: "Nej",
104
135
  linkEmpty: "Ingen sider fundet",
105
- linkLoading: "Henter sider\u2026"
136
+ linkLoading: "Henter sider\u2026",
137
+ linkCurrent: "Linker til"
106
138
  };
107
139
  var uiLabels = DEFAULT_LABELS;
108
140
  function resolveOptions(options) {
@@ -470,6 +502,8 @@ var savedRange = null;
470
502
  var linkDialog = null;
471
503
  var linkPages = null;
472
504
  var linkPicked = null;
505
+ var linkNewTab = false;
506
+ var linkNewTabTouched = false;
473
507
  var linkEditing = null;
474
508
  function hideLinkDialog() {
475
509
  if (linkDialog) linkDialog.style.display = "none";
@@ -499,6 +533,22 @@ async function fetchLinkablePages() {
499
533
  linkPages = body.pages ?? [];
500
534
  return linkPages;
501
535
  }
536
+ function positionPopover(el, rect, gap, width) {
537
+ const MARGIN = 8;
538
+ const vh = window.innerHeight;
539
+ const vw = window.innerWidth;
540
+ el.style.maxHeight = `${Math.max(120, vh - MARGIN * 2)}px`;
541
+ el.style.overflowY = "auto";
542
+ el.style.display = "block";
543
+ const h = el.getBoundingClientRect().height || el.offsetHeight || 0;
544
+ let top = rect.bottom + gap;
545
+ if (top + h > vh - MARGIN) {
546
+ const above = rect.top - gap - h;
547
+ top = above >= MARGIN ? above : vh - MARGIN - h;
548
+ }
549
+ el.style.top = `${Math.max(MARGIN, top)}px`;
550
+ el.style.left = `${Math.max(MARGIN, Math.min(rect.left, vw - width - MARGIN))}px`;
551
+ }
502
552
  function toggleLinkDialog(anchor) {
503
553
  if (linkDialog && linkDialog.style.display === "block") {
504
554
  hideLinkDialog();
@@ -510,9 +560,12 @@ function toggleLinkDialog(anchor) {
510
560
  if (!linkDialog) linkDialog = buildLinkDialog();
511
561
  renderLinkDialog();
512
562
  const rect = anchor.getBoundingClientRect();
513
- linkDialog.style.top = `${rect.bottom + 8}px`;
514
- linkDialog.style.left = `${Math.min(Math.max(8, rect.left - 180), window.innerWidth - 400)}px`;
515
- linkDialog.style.display = "block";
563
+ positionPopover(
564
+ linkDialog,
565
+ { top: rect.top, bottom: rect.bottom, left: rect.left - 180 },
566
+ 8,
567
+ 392
568
+ );
516
569
  }
517
570
  function buildLinkDialog() {
518
571
  const d = document.createElement("div");
@@ -532,9 +585,13 @@ function renderLinkDialog() {
532
585
  if (!d) return;
533
586
  const L = uiLabels;
534
587
  const editing = !!linkEditing;
588
+ if (!editing) {
589
+ linkNewTab = false;
590
+ linkNewTabTouched = false;
591
+ }
535
592
  const existingRef = linkEditing?.getAttribute("data-cms-ref") ?? "";
536
593
  const onPageTab = !editing || !!existingRef;
537
- 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>`;
594
+ 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 data-role="schemeless" data-testid="inline-link-schemeless" style="display:none;margin-top:8px;padding:8px 10px;background:rgba(255,176,32,.10);border:1px solid rgba(255,176,32,.34);border-radius:8px"><p data-role="schemeless-text" style="margin:0;font-size:11.5px;color:#e8d6b0;line-height:1.5"></p><button type="button" data-testid="inline-link-add-scheme" data-role="add-scheme" style="${btnCss(false)};margin-top:7px;height:26px;font-size:12px">${L.linkSchemelessFix}</button></div></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><button type="button" data-role="newtab" data-testid="inline-link-newtab" aria-pressed="false" style="display:flex;align-items:center;gap:8px;margin-top:12px;background:none;border:none;padding:0;cursor:pointer;font-family:inherit;font-size:12.5px;color:#c9d1dd"><span data-role="newtab-box" style="${checkboxCss(false)}"></span><span>${L.linkNewTab}</span></button><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>`;
538
595
  const q = (role) => d.querySelector(`[data-role="${role}"]`);
539
596
  const text = q("text");
540
597
  const urlIn = q("url");
@@ -542,6 +599,8 @@ function renderLinkDialog() {
542
599
  if (editing) {
543
600
  text.value = linkEditing?.getAttribute("data-cms-ref-label") === "auto" ? "" : linkEditing?.textContent ?? "";
544
601
  if (!existingRef) urlIn.value = linkEditing?.getAttribute("href") ?? "";
602
+ linkNewTab = linkEditing?.getAttribute("target") === "_blank";
603
+ linkNewTabTouched = true;
545
604
  q("remove").appendChild(buildRemoveLink());
546
605
  }
547
606
  d.querySelectorAll("[data-tab]").forEach((btn) => {
@@ -561,6 +620,16 @@ function renderLinkDialog() {
561
620
  urlIn.oninput = syncLinkDialog;
562
621
  q("cancel").onclick = hideLinkDialog;
563
622
  q("submit").onclick = applyLink;
623
+ q("newtab").onclick = () => {
624
+ linkNewTab = !linkNewTab;
625
+ linkNewTabTouched = true;
626
+ syncLinkDialog();
627
+ };
628
+ q("add-scheme").onclick = () => {
629
+ urlIn.value = withHttps(urlIn.value);
630
+ urlIn.focus();
631
+ syncLinkDialog();
632
+ };
564
633
  renderLinkList("");
565
634
  syncLinkDialog();
566
635
  }
@@ -589,6 +658,28 @@ function buildRemoveLink() {
589
658
  wrap.appendChild(btn);
590
659
  return wrap;
591
660
  }
661
+ function resolveExistingRef(ref, pages) {
662
+ if (!ref) return null;
663
+ const [collection, ...rest] = ref.split(":");
664
+ const slug = rest.join(":");
665
+ if (!collection || !slug) return null;
666
+ return pages.find((p) => p.collection === collection && p.slug === slug) ?? null;
667
+ }
668
+ function renderCurrentRefLine(box, ref, pages) {
669
+ if (!ref) {
670
+ box.style.display = "none";
671
+ box.innerHTML = "";
672
+ return;
673
+ }
674
+ const label = escapeHtml(uiLabels.linkCurrent);
675
+ const page = resolveExistingRef(ref, pages);
676
+ box.style.display = "block";
677
+ 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>`;
678
+ }
679
+ function paintCurrentRef(pages) {
680
+ const box = linkDialog?.querySelector('[data-role="current"]');
681
+ if (box) renderCurrentRefLine(box, linkEditing?.getAttribute("data-cms-ref"), pages ?? []);
682
+ }
592
683
  function renderLinkList(filter) {
593
684
  const d = linkDialog;
594
685
  if (!d) return;
@@ -614,19 +705,21 @@ function renderLinkList(filter) {
614
705
  syncLinkDialog();
615
706
  };
616
707
  list.appendChild(row);
708
+ if (on) row.scrollIntoView?.({ block: "nearest" });
617
709
  });
618
710
  };
711
+ const preselect = (pages) => {
712
+ if (!linkPicked) linkPicked = resolveExistingRef(linkEditing?.getAttribute("data-cms-ref"), pages);
713
+ paintCurrentRef(pages);
714
+ };
619
715
  if (linkPages) {
716
+ preselect(linkPages);
620
717
  paint(linkPages);
621
718
  return;
622
719
  }
623
720
  list.innerHTML = `<div style="padding:10px;font-size:12.5px;color:#9aa3b2">${uiLabels.linkLoading}</div>`;
624
721
  fetchLinkablePages().then((pages) => {
625
- const ref = linkEditing?.getAttribute("data-cms-ref");
626
- if (ref && !linkPicked) {
627
- const [c, ...rest] = ref.split(":");
628
- linkPicked = pages.find((p) => p.collection === c && p.slug === rest.join(":")) ?? null;
629
- }
722
+ preselect(pages);
630
723
  paint(pages);
631
724
  syncLinkDialog();
632
725
  }).catch(() => {
@@ -642,6 +735,30 @@ function syncLinkDialog() {
642
735
  q("hint").innerHTML = own ? uiLabels.linkTextOwnHint : onPage ? uiLabels.linkTextAutoHint : uiLabels.linkUrlHint;
643
736
  q("live").style.display = onPage ? "flex" : "none";
644
737
  q("submit").disabled = onPage ? !linkPicked : !q("url").value.trim();
738
+ const raw = q("url").value;
739
+ const doubtful = !onPage && isSchemeless(raw);
740
+ q("schemeless").style.display = doubtful ? "block" : "none";
741
+ if (doubtful) {
742
+ q("schemeless-text").innerHTML = uiLabels.linkSchemeless.replace(
743
+ "{v}",
744
+ escapeHtml(raw.trim())
745
+ );
746
+ }
747
+ if (!onPage && !linkNewTabTouched) {
748
+ linkNewTab = isExternalHost(raw, typeof location !== "undefined" ? location.host : "");
749
+ }
750
+ const box = q("newtab-box");
751
+ if (box) box.setAttribute("style", checkboxCss(linkNewTab));
752
+ q("newtab").setAttribute("aria-pressed", String(linkNewTab));
753
+ }
754
+ function setLinkTarget(el, newTab) {
755
+ if (newTab) {
756
+ el.setAttribute("target", "_blank");
757
+ el.setAttribute("rel", "noopener");
758
+ } else {
759
+ el.removeAttribute("target");
760
+ el.removeAttribute("rel");
761
+ }
645
762
  }
646
763
  function applyLink() {
647
764
  const d = linkDialog;
@@ -653,18 +770,21 @@ function applyLink() {
653
770
  if (!href) return;
654
771
  const ref = onPage && linkPicked ? `${linkPicked.collection}:${linkPicked.slug}` : "";
655
772
  const label = own || (onPage ? linkPicked?.title ?? href : href);
773
+ const applyTarget = (el) => setLinkTarget(el, linkNewTab);
656
774
  if (linkEditing) {
657
775
  linkEditing.setAttribute("href", href);
658
776
  if (ref) linkEditing.setAttribute("data-cms-ref", ref);
659
777
  else linkEditing.removeAttribute("data-cms-ref");
660
778
  if (ref && !own) linkEditing.setAttribute("data-cms-ref-label", "auto");
661
779
  else linkEditing.removeAttribute("data-cms-ref-label");
780
+ applyTarget(linkEditing);
662
781
  linkEditing.textContent = label;
663
782
  hideLinkDialog();
664
783
  return;
665
784
  }
666
785
  const a = document.createElement("a");
667
786
  a.setAttribute("href", href);
787
+ applyTarget(a);
668
788
  if (ref) {
669
789
  a.setAttribute("data-cms-ref", ref);
670
790
  if (!own) a.setAttribute("data-cms-ref-label", "auto");
@@ -689,6 +809,7 @@ function escapeHtml(v) {
689
809
  return v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
690
810
  }
691
811
  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;";
812
+ var checkboxCss = (on) => "width:16px;height:16px;flex:0 0 16px;border-radius:4px;box-sizing:border-box;display:inline-block;" + (on ? `background:#00b2ff;border:1px solid #00b2ff;background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path d='M4 8.5l2.6 2.6L12 5.8' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/></svg>");background-size:100% 100%;` : "background:#141821;border:1px solid #3a3f4a;");
692
813
  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;";
693
814
  var labelCss = () => "font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:#9aa3b2;margin:12px 0 5px;display:block;";
694
815
  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;");
@@ -701,9 +822,7 @@ function toggleEmojiPicker(anchor) {
701
822
  const sel = window.getSelection();
702
823
  if (sel && sel.rangeCount > 0) savedRange = sel.getRangeAt(0).cloneRange();
703
824
  const rect = anchor.getBoundingClientRect();
704
- emojiPicker.style.top = `${rect.bottom + 6}px`;
705
- emojiPicker.style.left = `${Math.min(rect.left, window.innerWidth - 240)}px`;
706
- emojiPicker.style.display = "block";
825
+ positionPopover(emojiPicker, rect, 6, 230);
707
826
  }
708
827
  function hideEmojiPicker() {
709
828
  if (emojiPicker) emojiPicker.style.display = "none";
@@ -836,7 +955,7 @@ function serializeBlockChildren(parent) {
836
955
  const s = serializeBlock(node).trim();
837
956
  if (s) blocks.push(s);
838
957
  } else {
839
- inlineRun += serializeInlineNode(node);
958
+ inlineRun = joinInline(inlineRun, serializeInlineNode(node));
840
959
  }
841
960
  });
842
961
  flushInline();
@@ -927,10 +1046,30 @@ function preservedLinkAttrs(el) {
927
1046
  }
928
1047
  return out;
929
1048
  }
1049
+ var HARD_BREAK = " \n";
1050
+ function edgeWhitespace(ws) {
1051
+ if (!ws) return "";
1052
+ return /\n/.test(ws) ? HARD_BREAK : " ";
1053
+ }
1054
+ function wrapEmphasis(inner, delimiter) {
1055
+ const core = inner.trim();
1056
+ if (!core) return "";
1057
+ const lead = edgeWhitespace(inner.slice(0, inner.length - inner.trimStart().length));
1058
+ const trail = edgeWhitespace(inner.slice(inner.trimEnd().length));
1059
+ return lead + delimiter + core + delimiter + trail;
1060
+ }
1061
+ function joinInline(out, piece) {
1062
+ if (!piece) return out;
1063
+ const outTail = /\s*$/.exec(out)[0];
1064
+ const pieceHead = /^\s*/.exec(piece)[0];
1065
+ if (!outTail || !pieceHead) return out + piece;
1066
+ const merged = /\n/.test(outTail + pieceHead) ? HARD_BREAK : " ";
1067
+ return out.slice(0, out.length - outTail.length) + merged + piece.slice(pieceHead.length);
1068
+ }
930
1069
  function serializeInline(node) {
931
1070
  let out = "";
932
1071
  node.childNodes.forEach((child) => {
933
- out += serializeInlineNode(child);
1072
+ out = joinInline(out, serializeInlineNode(child));
934
1073
  });
935
1074
  return out;
936
1075
  }
@@ -947,11 +1086,11 @@ function serializeInlineNode(child) {
947
1086
  switch (tag) {
948
1087
  case "strong":
949
1088
  case "b":
950
- out += inner.trim() ? `**${inner.trim()}**` : "";
1089
+ out += wrapEmphasis(inner, "**");
951
1090
  break;
952
1091
  case "em":
953
1092
  case "i":
954
- out += inner.trim() ? `*${inner.trim()}*` : "";
1093
+ out += wrapEmphasis(inner, "*");
955
1094
  break;
956
1095
  case "code":
957
1096
  out += "`" + inner + "`";
@@ -1098,6 +1237,10 @@ function injectStyles() {
1098
1237
  getConnectedToken,
1099
1238
  htmlToMarkdown,
1100
1239
  initInlineEdit,
1101
- rescanFields
1240
+ positionPopover,
1241
+ renderCurrentRefLine,
1242
+ rescanFields,
1243
+ resolveExistingRef,
1244
+ setLinkTarget
1102
1245
  });
1103
1246
  //# sourceMappingURL=index.cjs.map