@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.cjs +101 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +47 -1
- package/dist/index.d.ts +47 -1
- package/dist/index.js +97 -24
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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, "&").replace(/"/g, """).replace(/</g, "<");
|
|
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
|
|
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 +=
|
|
980
|
+
out += wrapEmphasis(inner, "**");
|
|
908
981
|
break;
|
|
909
982
|
case "em":
|
|
910
983
|
case "i":
|
|
911
|
-
out +=
|
|
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
|
|
922
|
-
if (
|
|
923
|
-
|
|
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
|
-
|
|
1130
|
+
positionPopover,
|
|
1131
|
+
renderCurrentRefLine,
|
|
1132
|
+
rescanFields,
|
|
1133
|
+
resolveExistingRef
|
|
1061
1134
|
};
|
|
1062
1135
|
//# sourceMappingURL=index.js.map
|