@broberg/cms-inline-edit 0.5.5 → 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 +78 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +76 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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;
|
|
@@ -155,6 +158,18 @@ declare function resolveExistingRef(ref: string | null | undefined, pages: Linka
|
|
|
155
158
|
* shows its raw reference rather than saying nothing.
|
|
156
159
|
*/
|
|
157
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;
|
|
158
173
|
/**
|
|
159
174
|
* Converts an edited contenteditable region's innerHTML back to Markdown so a
|
|
160
175
|
* rich article body round-trips through the cms `richtext` contract (fields
|
|
@@ -168,4 +183,4 @@ declare function renderCurrentRefLine(box: HTMLElement, ref: string | null | und
|
|
|
168
183
|
*/
|
|
169
184
|
declare function htmlToMarkdown(html: string): string;
|
|
170
185
|
|
|
171
|
-
export { type InlineEditLabels, type InlineEditOptions, applyFieldSlice, buildConnectUrl, disconnect, getConnectedToken, htmlToMarkdown, initInlineEdit, positionPopover, renderCurrentRefLine, rescanFields, resolveExistingRef };
|
|
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;
|
|
@@ -155,6 +158,18 @@ declare function resolveExistingRef(ref: string | null | undefined, pages: Linka
|
|
|
155
158
|
* shows its raw reference rather than saying nothing.
|
|
156
159
|
*/
|
|
157
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;
|
|
158
173
|
/**
|
|
159
174
|
* Converts an edited contenteditable region's innerHTML back to Markdown so a
|
|
160
175
|
* rich article body round-trips through the cms `richtext` contract (fields
|
|
@@ -168,4 +183,4 @@ declare function renderCurrentRefLine(box: HTMLElement, ref: string | null | und
|
|
|
168
183
|
*/
|
|
169
184
|
declare function htmlToMarkdown(html: string): string;
|
|
170
185
|
|
|
171
|
-
export { type InlineEditLabels, type InlineEditOptions, applyFieldSlice, buildConnectUrl, disconnect, getConnectedToken, htmlToMarkdown, initInlineEdit, positionPopover, renderCurrentRefLine, rescanFields, resolveExistingRef };
|
|
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",
|
|
@@ -439,6 +466,8 @@ var savedRange = null;
|
|
|
439
466
|
var linkDialog = null;
|
|
440
467
|
var linkPages = null;
|
|
441
468
|
var linkPicked = null;
|
|
469
|
+
var linkNewTab = false;
|
|
470
|
+
var linkNewTabTouched = false;
|
|
442
471
|
var linkEditing = null;
|
|
443
472
|
function hideLinkDialog() {
|
|
444
473
|
if (linkDialog) linkDialog.style.display = "none";
|
|
@@ -520,9 +549,13 @@ function renderLinkDialog() {
|
|
|
520
549
|
if (!d) return;
|
|
521
550
|
const L = uiLabels;
|
|
522
551
|
const editing = !!linkEditing;
|
|
552
|
+
if (!editing) {
|
|
553
|
+
linkNewTab = false;
|
|
554
|
+
linkNewTabTouched = false;
|
|
555
|
+
}
|
|
523
556
|
const existingRef = linkEditing?.getAttribute("data-cms-ref") ?? "";
|
|
524
557
|
const onPageTab = !editing || !!existingRef;
|
|
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>`;
|
|
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>`;
|
|
526
559
|
const q = (role) => d.querySelector(`[data-role="${role}"]`);
|
|
527
560
|
const text = q("text");
|
|
528
561
|
const urlIn = q("url");
|
|
@@ -530,6 +563,8 @@ function renderLinkDialog() {
|
|
|
530
563
|
if (editing) {
|
|
531
564
|
text.value = linkEditing?.getAttribute("data-cms-ref-label") === "auto" ? "" : linkEditing?.textContent ?? "";
|
|
532
565
|
if (!existingRef) urlIn.value = linkEditing?.getAttribute("href") ?? "";
|
|
566
|
+
linkNewTab = linkEditing?.getAttribute("target") === "_blank";
|
|
567
|
+
linkNewTabTouched = true;
|
|
533
568
|
q("remove").appendChild(buildRemoveLink());
|
|
534
569
|
}
|
|
535
570
|
d.querySelectorAll("[data-tab]").forEach((btn) => {
|
|
@@ -549,6 +584,16 @@ function renderLinkDialog() {
|
|
|
549
584
|
urlIn.oninput = syncLinkDialog;
|
|
550
585
|
q("cancel").onclick = hideLinkDialog;
|
|
551
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
|
+
};
|
|
552
597
|
renderLinkList("");
|
|
553
598
|
syncLinkDialog();
|
|
554
599
|
}
|
|
@@ -654,6 +699,30 @@ function syncLinkDialog() {
|
|
|
654
699
|
q("hint").innerHTML = own ? uiLabels.linkTextOwnHint : onPage ? uiLabels.linkTextAutoHint : uiLabels.linkUrlHint;
|
|
655
700
|
q("live").style.display = onPage ? "flex" : "none";
|
|
656
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
|
+
}
|
|
657
726
|
}
|
|
658
727
|
function applyLink() {
|
|
659
728
|
const d = linkDialog;
|
|
@@ -665,18 +734,21 @@ function applyLink() {
|
|
|
665
734
|
if (!href) return;
|
|
666
735
|
const ref = onPage && linkPicked ? `${linkPicked.collection}:${linkPicked.slug}` : "";
|
|
667
736
|
const label = own || (onPage ? linkPicked?.title ?? href : href);
|
|
737
|
+
const applyTarget = (el) => setLinkTarget(el, linkNewTab);
|
|
668
738
|
if (linkEditing) {
|
|
669
739
|
linkEditing.setAttribute("href", href);
|
|
670
740
|
if (ref) linkEditing.setAttribute("data-cms-ref", ref);
|
|
671
741
|
else linkEditing.removeAttribute("data-cms-ref");
|
|
672
742
|
if (ref && !own) linkEditing.setAttribute("data-cms-ref-label", "auto");
|
|
673
743
|
else linkEditing.removeAttribute("data-cms-ref-label");
|
|
744
|
+
applyTarget(linkEditing);
|
|
674
745
|
linkEditing.textContent = label;
|
|
675
746
|
hideLinkDialog();
|
|
676
747
|
return;
|
|
677
748
|
}
|
|
678
749
|
const a = document.createElement("a");
|
|
679
750
|
a.setAttribute("href", href);
|
|
751
|
+
applyTarget(a);
|
|
680
752
|
if (ref) {
|
|
681
753
|
a.setAttribute("data-cms-ref", ref);
|
|
682
754
|
if (!own) a.setAttribute("data-cms-ref-label", "auto");
|
|
@@ -701,6 +773,7 @@ function escapeHtml(v) {
|
|
|
701
773
|
return v.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
702
774
|
}
|
|
703
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;");
|
|
704
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;";
|
|
705
778
|
var labelCss = () => "font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:#9aa3b2;margin:12px 0 5px;display:block;";
|
|
706
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;");
|
|
@@ -1130,6 +1203,7 @@ export {
|
|
|
1130
1203
|
positionPopover,
|
|
1131
1204
|
renderCurrentRefLine,
|
|
1132
1205
|
rescanFields,
|
|
1133
|
-
resolveExistingRef
|
|
1206
|
+
resolveExistingRef,
|
|
1207
|
+
setLinkTarget
|
|
1134
1208
|
};
|
|
1135
1209
|
//# sourceMappingURL=index.js.map
|