@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.d.cts CHANGED
@@ -43,6 +43,9 @@ interface InlineEditLabels {
43
43
  linkLiveHint?: string;
44
44
  linkUrlHint?: string;
45
45
  linkUrl?: string;
46
+ linkNewTab?: string;
47
+ linkSchemeless?: string;
48
+ linkSchemelessFix?: string;
46
49
  linkInsert?: string;
47
50
  linkSave?: string;
48
51
  linkCancel?: string;
@@ -52,6 +55,7 @@ interface InlineEditLabels {
52
55
  linkNo?: string;
53
56
  linkEmpty?: string;
54
57
  linkLoading?: string;
58
+ linkCurrent?: string;
55
59
  }
56
60
  interface InlineEditOptions {
57
61
  /** Base URL of the CMS API, e.g. "https://webhouse.app". */
@@ -109,6 +113,63 @@ declare function disconnect(options: InlineEditOptions): void;
109
113
  * MPA consumers (broberg: full reload per navigation re-runs init) never need it.
110
114
  */
111
115
  declare function rescanFields(): void;
116
+ interface LinkablePage {
117
+ collection: string;
118
+ slug: string;
119
+ title: string;
120
+ path: string;
121
+ label: string;
122
+ }
123
+ /**
124
+ * Place a fixed-position popover so it stays fully inside the viewport.
125
+ *
126
+ * The link dialog used to be pinned at `rect.bottom + gap` unconditionally,
127
+ * with `overflow:hidden` and no max-height. On a short laptop screen the
128
+ * dialog's footer — where Gem and Annullér live — sat below the bottom edge
129
+ * with no way to reach it: the dialog could not scroll and the page behind it
130
+ * did not move. The editor could open the dialog and fill it in, but not save.
131
+ * (Reported from production 2026-08-26.)
132
+ *
133
+ * Order matters: the element has to be laid out before its height can be
134
+ * measured, so display is switched on first and the final `top` written after.
135
+ *
136
+ * @param width the popover's own fixed width, used to clamp `left`.
137
+ */
138
+ declare function positionPopover(el: HTMLElement, rect: {
139
+ top: number;
140
+ bottom: number;
141
+ left: number;
142
+ }, gap: number, width: number): void;
143
+ /**
144
+ * Find the page an existing `data-cms-ref` points at.
145
+ *
146
+ * A slug may itself contain a colon, so only the FIRST segment is the
147
+ * collection and everything after it is the slug.
148
+ */
149
+ declare function resolveExistingRef(ref: string | null | undefined, pages: LinkablePage[]): LinkablePage | null;
150
+ /**
151
+ * Render the "currently points at" line for the link being edited.
152
+ *
153
+ * Pure over its arguments so the wording can be pinned by a test. Reading the
154
+ * highlighted row is not enough on its own: it can sit outside the list's 196px
155
+ * window, and a reference to a page that is no longer linkable has no row to
156
+ * highlight at all. Christian, 2026-08-26: "når jeg redigerer et eksisterende
157
+ * link til egen side skal den også vise hvilken vi linker til." A missing page
158
+ * shows its raw reference rather than saying nothing.
159
+ */
160
+ declare function renderCurrentRefLine(box: HTMLElement, ref: string | null | undefined, pages: LinkablePage[]): void;
161
+ /**
162
+ * Open-in-a-new-tab, written as ONE decision.
163
+ *
164
+ * target and rel travel together: a `_blank` link without `rel="noopener"`
165
+ * hands the page it opens a live handle back to ours. Setting one without the
166
+ * other is not a smaller version of this feature, it is a security hole — so
167
+ * the pair is set and cleared in one place rather than at each call site.
168
+ *
169
+ * F157.7 already taught the serializer to emit an anchor carrying extras as
170
+ * inline HTML, so both attributes survive a save.
171
+ */
172
+ declare function setLinkTarget(el: HTMLElement, newTab: boolean): void;
112
173
  /**
113
174
  * Converts an edited contenteditable region's innerHTML back to Markdown so a
114
175
  * rich article body round-trips through the cms `richtext` contract (fields
@@ -122,4 +183,4 @@ declare function rescanFields(): void;
122
183
  */
123
184
  declare function htmlToMarkdown(html: string): string;
124
185
 
125
- export { type InlineEditLabels, type InlineEditOptions, applyFieldSlice, buildConnectUrl, disconnect, getConnectedToken, htmlToMarkdown, initInlineEdit, rescanFields };
186
+ export { type InlineEditLabels, type InlineEditOptions, applyFieldSlice, buildConnectUrl, disconnect, getConnectedToken, htmlToMarkdown, initInlineEdit, positionPopover, renderCurrentRefLine, rescanFields, resolveExistingRef, setLinkTarget };
package/dist/index.d.ts CHANGED
@@ -43,6 +43,9 @@ interface InlineEditLabels {
43
43
  linkLiveHint?: string;
44
44
  linkUrlHint?: string;
45
45
  linkUrl?: string;
46
+ linkNewTab?: string;
47
+ linkSchemeless?: string;
48
+ linkSchemelessFix?: string;
46
49
  linkInsert?: string;
47
50
  linkSave?: string;
48
51
  linkCancel?: string;
@@ -52,6 +55,7 @@ interface InlineEditLabels {
52
55
  linkNo?: string;
53
56
  linkEmpty?: string;
54
57
  linkLoading?: string;
58
+ linkCurrent?: string;
55
59
  }
56
60
  interface InlineEditOptions {
57
61
  /** Base URL of the CMS API, e.g. "https://webhouse.app". */
@@ -109,6 +113,63 @@ declare function disconnect(options: InlineEditOptions): void;
109
113
  * MPA consumers (broberg: full reload per navigation re-runs init) never need it.
110
114
  */
111
115
  declare function rescanFields(): void;
116
+ interface LinkablePage {
117
+ collection: string;
118
+ slug: string;
119
+ title: string;
120
+ path: string;
121
+ label: string;
122
+ }
123
+ /**
124
+ * Place a fixed-position popover so it stays fully inside the viewport.
125
+ *
126
+ * The link dialog used to be pinned at `rect.bottom + gap` unconditionally,
127
+ * with `overflow:hidden` and no max-height. On a short laptop screen the
128
+ * dialog's footer — where Gem and Annullér live — sat below the bottom edge
129
+ * with no way to reach it: the dialog could not scroll and the page behind it
130
+ * did not move. The editor could open the dialog and fill it in, but not save.
131
+ * (Reported from production 2026-08-26.)
132
+ *
133
+ * Order matters: the element has to be laid out before its height can be
134
+ * measured, so display is switched on first and the final `top` written after.
135
+ *
136
+ * @param width the popover's own fixed width, used to clamp `left`.
137
+ */
138
+ declare function positionPopover(el: HTMLElement, rect: {
139
+ top: number;
140
+ bottom: number;
141
+ left: number;
142
+ }, gap: number, width: number): void;
143
+ /**
144
+ * Find the page an existing `data-cms-ref` points at.
145
+ *
146
+ * A slug may itself contain a colon, so only the FIRST segment is the
147
+ * collection and everything after it is the slug.
148
+ */
149
+ declare function resolveExistingRef(ref: string | null | undefined, pages: LinkablePage[]): LinkablePage | null;
150
+ /**
151
+ * Render the "currently points at" line for the link being edited.
152
+ *
153
+ * Pure over its arguments so the wording can be pinned by a test. Reading the
154
+ * highlighted row is not enough on its own: it can sit outside the list's 196px
155
+ * window, and a reference to a page that is no longer linkable has no row to
156
+ * highlight at all. Christian, 2026-08-26: "når jeg redigerer et eksisterende
157
+ * link til egen side skal den også vise hvilken vi linker til." A missing page
158
+ * shows its raw reference rather than saying nothing.
159
+ */
160
+ declare function renderCurrentRefLine(box: HTMLElement, ref: string | null | undefined, pages: LinkablePage[]): void;
161
+ /**
162
+ * Open-in-a-new-tab, written as ONE decision.
163
+ *
164
+ * target and rel travel together: a `_blank` link without `rel="noopener"`
165
+ * hands the page it opens a live handle back to ours. Setting one without the
166
+ * other is not a smaller version of this feature, it is a security hole — so
167
+ * the pair is set and cleared in one place rather than at each call site.
168
+ *
169
+ * F157.7 already taught the serializer to emit an anchor carrying extras as
170
+ * inline HTML, so both attributes survive a save.
171
+ */
172
+ declare function setLinkTarget(el: HTMLElement, newTab: boolean): void;
112
173
  /**
113
174
  * Converts an edited contenteditable region's innerHTML back to Markdown so a
114
175
  * rich article body round-trips through the cms `richtext` contract (fields
@@ -122,4 +183,4 @@ declare function rescanFields(): void;
122
183
  */
123
184
  declare function htmlToMarkdown(html: string): string;
124
185
 
125
- export { type InlineEditLabels, type InlineEditOptions, applyFieldSlice, buildConnectUrl, disconnect, getConnectedToken, htmlToMarkdown, initInlineEdit, rescanFields };
186
+ export { type InlineEditLabels, type InlineEditOptions, applyFieldSlice, buildConnectUrl, disconnect, getConnectedToken, htmlToMarkdown, initInlineEdit, positionPopover, renderCurrentRefLine, rescanFields, resolveExistingRef, setLinkTarget };
package/dist/index.js CHANGED
@@ -8,6 +8,30 @@ function applyFieldSlice(current, original, next) {
8
8
  return current.slice(0, first) + next + current.slice(first + original.length);
9
9
  }
10
10
 
11
+ // src/link-target.ts
12
+ function isSchemeless(raw) {
13
+ const v = (raw || "").trim();
14
+ if (!v) return false;
15
+ if (/^[a-z][a-z0-9+.-]*:/i.test(v)) return false;
16
+ if (v.startsWith("//")) return false;
17
+ if (v.startsWith("/")) return false;
18
+ if (v.startsWith("#")) return false;
19
+ if (v.startsWith("?")) return false;
20
+ return true;
21
+ }
22
+ function withHttps(raw) {
23
+ return `https://${(raw || "").trim().replace(/^\/+/, "")}`;
24
+ }
25
+ function isExternalHost(raw, currentHost) {
26
+ const v = (raw || "").trim();
27
+ if (!/^https?:\/\//i.test(v)) return false;
28
+ try {
29
+ return new URL(v).host.toLowerCase() !== (currentHost || "").toLowerCase();
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
11
35
  // src/token-safe.ts
12
36
  var TEXT_NODE = 3;
13
37
  var ELEMENT_NODE = 1;
@@ -62,6 +86,9 @@ var DEFAULT_LABELS = {
62
86
  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
87
  linkUrlHint: "En fri adresse peger pr\xE6cis d\xE9r, du skriver. Den f\xF8lger <b>ikke</b> med, hvis m\xE5let flytter sig.",
64
88
  linkUrl: "Adresse",
89
+ linkNewTab: "\xC5bn i ny fane",
90
+ linkSchemeless: "<b>{v}</b> l\xE6ses som en side p\xE5 <b>dette</b> site \u2014 ikke som en adresse ude p\xE5 nettet.",
91
+ linkSchemelessFix: "Tilf\xF8j https://",
65
92
  linkInsert: "Inds\xE6t link",
66
93
  linkSave: "Gem \xE6ndring",
67
94
  linkCancel: "Annull\xE9r",
@@ -70,7 +97,8 @@ var DEFAULT_LABELS = {
70
97
  linkYes: "Ja",
71
98
  linkNo: "Nej",
72
99
  linkEmpty: "Ingen sider fundet",
73
- linkLoading: "Henter sider\u2026"
100
+ linkLoading: "Henter sider\u2026",
101
+ linkCurrent: "Linker til"
74
102
  };
75
103
  var uiLabels = DEFAULT_LABELS;
76
104
  function resolveOptions(options) {
@@ -438,6 +466,8 @@ var savedRange = null;
438
466
  var linkDialog = null;
439
467
  var linkPages = null;
440
468
  var linkPicked = null;
469
+ var linkNewTab = false;
470
+ var linkNewTabTouched = false;
441
471
  var linkEditing = null;
442
472
  function hideLinkDialog() {
443
473
  if (linkDialog) linkDialog.style.display = "none";
@@ -467,6 +497,22 @@ async function fetchLinkablePages() {
467
497
  linkPages = body.pages ?? [];
468
498
  return linkPages;
469
499
  }
500
+ function positionPopover(el, rect, gap, width) {
501
+ const MARGIN = 8;
502
+ const vh = window.innerHeight;
503
+ const vw = window.innerWidth;
504
+ el.style.maxHeight = `${Math.max(120, vh - MARGIN * 2)}px`;
505
+ el.style.overflowY = "auto";
506
+ el.style.display = "block";
507
+ const h = el.getBoundingClientRect().height || el.offsetHeight || 0;
508
+ let top = rect.bottom + gap;
509
+ if (top + h > vh - MARGIN) {
510
+ const above = rect.top - gap - h;
511
+ top = above >= MARGIN ? above : vh - MARGIN - h;
512
+ }
513
+ el.style.top = `${Math.max(MARGIN, top)}px`;
514
+ el.style.left = `${Math.max(MARGIN, Math.min(rect.left, vw - width - MARGIN))}px`;
515
+ }
470
516
  function toggleLinkDialog(anchor) {
471
517
  if (linkDialog && linkDialog.style.display === "block") {
472
518
  hideLinkDialog();
@@ -478,9 +524,12 @@ function toggleLinkDialog(anchor) {
478
524
  if (!linkDialog) linkDialog = buildLinkDialog();
479
525
  renderLinkDialog();
480
526
  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";
527
+ positionPopover(
528
+ linkDialog,
529
+ { top: rect.top, bottom: rect.bottom, left: rect.left - 180 },
530
+ 8,
531
+ 392
532
+ );
484
533
  }
485
534
  function buildLinkDialog() {
486
535
  const d = document.createElement("div");
@@ -500,9 +549,13 @@ function renderLinkDialog() {
500
549
  if (!d) return;
501
550
  const L = uiLabels;
502
551
  const editing = !!linkEditing;
552
+ if (!editing) {
553
+ linkNewTab = false;
554
+ linkNewTabTouched = false;
555
+ }
503
556
  const existingRef = linkEditing?.getAttribute("data-cms-ref") ?? "";
504
557
  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>`;
558
+ 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>`;
506
559
  const q = (role) => d.querySelector(`[data-role="${role}"]`);
507
560
  const text = q("text");
508
561
  const urlIn = q("url");
@@ -510,6 +563,8 @@ function renderLinkDialog() {
510
563
  if (editing) {
511
564
  text.value = linkEditing?.getAttribute("data-cms-ref-label") === "auto" ? "" : linkEditing?.textContent ?? "";
512
565
  if (!existingRef) urlIn.value = linkEditing?.getAttribute("href") ?? "";
566
+ linkNewTab = linkEditing?.getAttribute("target") === "_blank";
567
+ linkNewTabTouched = true;
513
568
  q("remove").appendChild(buildRemoveLink());
514
569
  }
515
570
  d.querySelectorAll("[data-tab]").forEach((btn) => {
@@ -529,6 +584,16 @@ function renderLinkDialog() {
529
584
  urlIn.oninput = syncLinkDialog;
530
585
  q("cancel").onclick = hideLinkDialog;
531
586
  q("submit").onclick = applyLink;
587
+ q("newtab").onclick = () => {
588
+ linkNewTab = !linkNewTab;
589
+ linkNewTabTouched = true;
590
+ syncLinkDialog();
591
+ };
592
+ q("add-scheme").onclick = () => {
593
+ urlIn.value = withHttps(urlIn.value);
594
+ urlIn.focus();
595
+ syncLinkDialog();
596
+ };
532
597
  renderLinkList("");
533
598
  syncLinkDialog();
534
599
  }
@@ -557,6 +622,28 @@ function buildRemoveLink() {
557
622
  wrap.appendChild(btn);
558
623
  return wrap;
559
624
  }
625
+ function resolveExistingRef(ref, pages) {
626
+ if (!ref) return null;
627
+ const [collection, ...rest] = ref.split(":");
628
+ const slug = rest.join(":");
629
+ if (!collection || !slug) return null;
630
+ return pages.find((p) => p.collection === collection && p.slug === slug) ?? null;
631
+ }
632
+ function renderCurrentRefLine(box, ref, pages) {
633
+ if (!ref) {
634
+ box.style.display = "none";
635
+ box.innerHTML = "";
636
+ return;
637
+ }
638
+ const label = escapeHtml(uiLabels.linkCurrent);
639
+ const page = resolveExistingRef(ref, pages);
640
+ box.style.display = "block";
641
+ 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>`;
642
+ }
643
+ function paintCurrentRef(pages) {
644
+ const box = linkDialog?.querySelector('[data-role="current"]');
645
+ if (box) renderCurrentRefLine(box, linkEditing?.getAttribute("data-cms-ref"), pages ?? []);
646
+ }
560
647
  function renderLinkList(filter) {
561
648
  const d = linkDialog;
562
649
  if (!d) return;
@@ -582,19 +669,21 @@ function renderLinkList(filter) {
582
669
  syncLinkDialog();
583
670
  };
584
671
  list.appendChild(row);
672
+ if (on) row.scrollIntoView?.({ block: "nearest" });
585
673
  });
586
674
  };
675
+ const preselect = (pages) => {
676
+ if (!linkPicked) linkPicked = resolveExistingRef(linkEditing?.getAttribute("data-cms-ref"), pages);
677
+ paintCurrentRef(pages);
678
+ };
587
679
  if (linkPages) {
680
+ preselect(linkPages);
588
681
  paint(linkPages);
589
682
  return;
590
683
  }
591
684
  list.innerHTML = `<div style="padding:10px;font-size:12.5px;color:#9aa3b2">${uiLabels.linkLoading}</div>`;
592
685
  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
- }
686
+ preselect(pages);
598
687
  paint(pages);
599
688
  syncLinkDialog();
600
689
  }).catch(() => {
@@ -610,6 +699,30 @@ function syncLinkDialog() {
610
699
  q("hint").innerHTML = own ? uiLabels.linkTextOwnHint : onPage ? uiLabels.linkTextAutoHint : uiLabels.linkUrlHint;
611
700
  q("live").style.display = onPage ? "flex" : "none";
612
701
  q("submit").disabled = onPage ? !linkPicked : !q("url").value.trim();
702
+ const raw = q("url").value;
703
+ const doubtful = !onPage && isSchemeless(raw);
704
+ q("schemeless").style.display = doubtful ? "block" : "none";
705
+ if (doubtful) {
706
+ q("schemeless-text").innerHTML = uiLabels.linkSchemeless.replace(
707
+ "{v}",
708
+ escapeHtml(raw.trim())
709
+ );
710
+ }
711
+ if (!onPage && !linkNewTabTouched) {
712
+ linkNewTab = isExternalHost(raw, typeof location !== "undefined" ? location.host : "");
713
+ }
714
+ const box = q("newtab-box");
715
+ if (box) box.setAttribute("style", checkboxCss(linkNewTab));
716
+ q("newtab").setAttribute("aria-pressed", String(linkNewTab));
717
+ }
718
+ function setLinkTarget(el, newTab) {
719
+ if (newTab) {
720
+ el.setAttribute("target", "_blank");
721
+ el.setAttribute("rel", "noopener");
722
+ } else {
723
+ el.removeAttribute("target");
724
+ el.removeAttribute("rel");
725
+ }
613
726
  }
614
727
  function applyLink() {
615
728
  const d = linkDialog;
@@ -621,18 +734,21 @@ function applyLink() {
621
734
  if (!href) return;
622
735
  const ref = onPage && linkPicked ? `${linkPicked.collection}:${linkPicked.slug}` : "";
623
736
  const label = own || (onPage ? linkPicked?.title ?? href : href);
737
+ const applyTarget = (el) => setLinkTarget(el, linkNewTab);
624
738
  if (linkEditing) {
625
739
  linkEditing.setAttribute("href", href);
626
740
  if (ref) linkEditing.setAttribute("data-cms-ref", ref);
627
741
  else linkEditing.removeAttribute("data-cms-ref");
628
742
  if (ref && !own) linkEditing.setAttribute("data-cms-ref-label", "auto");
629
743
  else linkEditing.removeAttribute("data-cms-ref-label");
744
+ applyTarget(linkEditing);
630
745
  linkEditing.textContent = label;
631
746
  hideLinkDialog();
632
747
  return;
633
748
  }
634
749
  const a = document.createElement("a");
635
750
  a.setAttribute("href", href);
751
+ applyTarget(a);
636
752
  if (ref) {
637
753
  a.setAttribute("data-cms-ref", ref);
638
754
  if (!own) a.setAttribute("data-cms-ref-label", "auto");
@@ -657,6 +773,7 @@ function escapeHtml(v) {
657
773
  return v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
658
774
  }
659
775
  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;";
776
+ 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;");
660
777
  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;";
661
778
  var labelCss = () => "font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:#9aa3b2;margin:12px 0 5px;display:block;";
662
779
  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;");
@@ -669,9 +786,7 @@ function toggleEmojiPicker(anchor) {
669
786
  const sel = window.getSelection();
670
787
  if (sel && sel.rangeCount > 0) savedRange = sel.getRangeAt(0).cloneRange();
671
788
  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";
789
+ positionPopover(emojiPicker, rect, 6, 230);
675
790
  }
676
791
  function hideEmojiPicker() {
677
792
  if (emojiPicker) emojiPicker.style.display = "none";
@@ -804,7 +919,7 @@ function serializeBlockChildren(parent) {
804
919
  const s = serializeBlock(node).trim();
805
920
  if (s) blocks.push(s);
806
921
  } else {
807
- inlineRun += serializeInlineNode(node);
922
+ inlineRun = joinInline(inlineRun, serializeInlineNode(node));
808
923
  }
809
924
  });
810
925
  flushInline();
@@ -895,10 +1010,30 @@ function preservedLinkAttrs(el) {
895
1010
  }
896
1011
  return out;
897
1012
  }
1013
+ var HARD_BREAK = " \n";
1014
+ function edgeWhitespace(ws) {
1015
+ if (!ws) return "";
1016
+ return /\n/.test(ws) ? HARD_BREAK : " ";
1017
+ }
1018
+ function wrapEmphasis(inner, delimiter) {
1019
+ const core = inner.trim();
1020
+ if (!core) return "";
1021
+ const lead = edgeWhitespace(inner.slice(0, inner.length - inner.trimStart().length));
1022
+ const trail = edgeWhitespace(inner.slice(inner.trimEnd().length));
1023
+ return lead + delimiter + core + delimiter + trail;
1024
+ }
1025
+ function joinInline(out, piece) {
1026
+ if (!piece) return out;
1027
+ const outTail = /\s*$/.exec(out)[0];
1028
+ const pieceHead = /^\s*/.exec(piece)[0];
1029
+ if (!outTail || !pieceHead) return out + piece;
1030
+ const merged = /\n/.test(outTail + pieceHead) ? HARD_BREAK : " ";
1031
+ return out.slice(0, out.length - outTail.length) + merged + piece.slice(pieceHead.length);
1032
+ }
898
1033
  function serializeInline(node) {
899
1034
  let out = "";
900
1035
  node.childNodes.forEach((child) => {
901
- out += serializeInlineNode(child);
1036
+ out = joinInline(out, serializeInlineNode(child));
902
1037
  });
903
1038
  return out;
904
1039
  }
@@ -915,11 +1050,11 @@ function serializeInlineNode(child) {
915
1050
  switch (tag) {
916
1051
  case "strong":
917
1052
  case "b":
918
- out += inner.trim() ? `**${inner.trim()}**` : "";
1053
+ out += wrapEmphasis(inner, "**");
919
1054
  break;
920
1055
  case "em":
921
1056
  case "i":
922
- out += inner.trim() ? `*${inner.trim()}*` : "";
1057
+ out += wrapEmphasis(inner, "*");
923
1058
  break;
924
1059
  case "code":
925
1060
  out += "`" + inner + "`";
@@ -1065,6 +1200,10 @@ export {
1065
1200
  getConnectedToken,
1066
1201
  htmlToMarkdown,
1067
1202
  initInlineEdit,
1068
- rescanFields
1203
+ positionPopover,
1204
+ renderCurrentRefLine,
1205
+ rescanFields,
1206
+ resolveExistingRef,
1207
+ setLinkTarget
1069
1208
  };
1070
1209
  //# sourceMappingURL=index.js.map