@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 +162 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +62 -1
- package/dist/index.d.ts +62 -1
- package/dist/index.js +157 -18
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
|
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
|
|
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
|
|
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 +=
|
|
1053
|
+
out += wrapEmphasis(inner, "**");
|
|
919
1054
|
break;
|
|
920
1055
|
case "em":
|
|
921
1056
|
case "i":
|
|
922
|
-
out +=
|
|
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
|
-
|
|
1203
|
+
positionPopover,
|
|
1204
|
+
renderCurrentRefLine,
|
|
1205
|
+
rescanFields,
|
|
1206
|
+
resolveExistingRef,
|
|
1207
|
+
setLinkTarget
|
|
1069
1208
|
};
|
|
1070
1209
|
//# sourceMappingURL=index.js.map
|