@broberg/cms-inline-edit 0.6.2 → 0.7.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 +266 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -1
- package/dist/index.d.ts +70 -1
- package/dist/index.js +259 -14
- package/dist/index.js.map +1 -1
- package/dist/server/index.cjs +14 -1
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +18 -1
- package/dist/server/index.d.ts +18 -1
- package/dist/server/index.js +13 -1
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -12,6 +12,75 @@
|
|
|
12
12
|
*/
|
|
13
13
|
declare function applyFieldSlice(current: string, original: string, next: string): string;
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* What kind of address did the editor actually type?
|
|
17
|
+
*
|
|
18
|
+
* The reason this is a module and not three lines inside the dialog: the
|
|
19
|
+
* content scanner (`scripts/scan-schemeless-links.mjs`) has to agree with the
|
|
20
|
+
* editor about what counts as a doubtful address. Two copies of the rule drift,
|
|
21
|
+
* and then the scanner reports a clean site the editor is still producing dead
|
|
22
|
+
* links on.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* True when the browser will read this address as a path on the CURRENT page.
|
|
26
|
+
*
|
|
27
|
+
* `<a href="www.trailmem.com">` is valid HTML: it renders, it is styled, it
|
|
28
|
+
* lights up on hover. The browser just resolves it relative to the page it sits
|
|
29
|
+
* on, so it lands on /some/article/www.trailmem.com. Nothing about the markup
|
|
30
|
+
* looks wrong — only following the link reveals it.
|
|
31
|
+
*
|
|
32
|
+
* This DELIBERATELY does not try to decide whether the author meant an external
|
|
33
|
+
* host. `trailmem.com` and `index.html` are syntactically identical, and a rule
|
|
34
|
+
* that "fixes" the first breaks the second. It reports doubt; it never rewrites.
|
|
35
|
+
*/
|
|
36
|
+
declare function isSchemeless(raw: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* A bare email address — `name@host.tld`, no scheme, no path.
|
|
39
|
+
*
|
|
40
|
+
* Kept deliberately narrow: anything with a slash, a space or a second @ is
|
|
41
|
+
* not one, so an ordinary URL that happens to contain an @ (userinfo, a query)
|
|
42
|
+
* is untouched.
|
|
43
|
+
*/
|
|
44
|
+
declare function isBareEmail(raw: string): boolean;
|
|
45
|
+
/** The one-click repair the dialog offers. Never applied on its own. */
|
|
46
|
+
declare function withHttps(raw: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Does this address leave the site the editor is standing on?
|
|
49
|
+
*
|
|
50
|
+
* Used ONLY to decide how the "open in a new tab" box starts out — and that
|
|
51
|
+
* starting state is visible in the box itself, so a wrong guess is something
|
|
52
|
+
* the editor can see and change, not a silent rewrite.
|
|
53
|
+
*/
|
|
54
|
+
declare function isExternalHost(raw: string, currentHost: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Every link address inside a stored string — HTML anchors and Markdown links.
|
|
57
|
+
*
|
|
58
|
+
* Markdown IMAGE syntax is excluded on purpose: a relative image path
|
|
59
|
+
* (``) is normal and correct, and flagging it would bury
|
|
60
|
+
* the real findings in noise.
|
|
61
|
+
*/
|
|
62
|
+
declare function extractLinkTargets(text: string): Array<{
|
|
63
|
+
syntax: "html" | "markdown";
|
|
64
|
+
value: string;
|
|
65
|
+
}>;
|
|
66
|
+
/**
|
|
67
|
+
* Schemes that must never reach an `href`.
|
|
68
|
+
*
|
|
69
|
+
* `javascript:` in a link is script execution on click, in the SITE's own
|
|
70
|
+
* origin — which is where the editor's own edit-session token lives in
|
|
71
|
+
* localStorage. So one editor planting such a link, and any colleague clicking
|
|
72
|
+
* it, is a token handover. `data:` and `vbscript:` are the same class.
|
|
73
|
+
*
|
|
74
|
+
* The package ALREADY refuses these on every other attribute
|
|
75
|
+
* (preservedLinkAttrs drops `javascript:` values and every `on*` handler) —
|
|
76
|
+
* href was the one that escaped, because it is emitted separately from the
|
|
77
|
+
* attributes it sits beside.
|
|
78
|
+
*
|
|
79
|
+
* Whitespace and control characters are stripped first: browsers ignore them
|
|
80
|
+
* inside a scheme, so `java\tscript:` and ` javascript:` both run.
|
|
81
|
+
*/
|
|
82
|
+
declare function isDangerousUrl(raw: string): boolean;
|
|
83
|
+
|
|
15
84
|
/**
|
|
16
85
|
* Labels for the in-editor UI — the rich-text toolbar (bold/italic/underline
|
|
17
86
|
* tooltips, the visible "colour" + "done" buttons, the emoji tooltip) and the
|
|
@@ -184,4 +253,4 @@ declare function setLinkTarget(el: HTMLElement, newTab: boolean): void;
|
|
|
184
253
|
*/
|
|
185
254
|
declare function htmlToMarkdown(html: string): string;
|
|
186
255
|
|
|
187
|
-
export { type InlineEditLabels, type InlineEditOptions, applyFieldSlice, buildConnectUrl, disconnect, getConnectedToken, htmlToMarkdown, initInlineEdit, positionPopover, renderCurrentRefLine, rescanFields, resolveExistingRef, setLinkTarget };
|
|
256
|
+
export { type InlineEditLabels, type InlineEditOptions, applyFieldSlice, buildConnectUrl, disconnect, extractLinkTargets, getConnectedToken, htmlToMarkdown, initInlineEdit, isBareEmail, isDangerousUrl, isExternalHost, isSchemeless, positionPopover, renderCurrentRefLine, rescanFields, resolveExistingRef, setLinkTarget, withHttps };
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,75 @@
|
|
|
12
12
|
*/
|
|
13
13
|
declare function applyFieldSlice(current: string, original: string, next: string): string;
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* What kind of address did the editor actually type?
|
|
17
|
+
*
|
|
18
|
+
* The reason this is a module and not three lines inside the dialog: the
|
|
19
|
+
* content scanner (`scripts/scan-schemeless-links.mjs`) has to agree with the
|
|
20
|
+
* editor about what counts as a doubtful address. Two copies of the rule drift,
|
|
21
|
+
* and then the scanner reports a clean site the editor is still producing dead
|
|
22
|
+
* links on.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* True when the browser will read this address as a path on the CURRENT page.
|
|
26
|
+
*
|
|
27
|
+
* `<a href="www.trailmem.com">` is valid HTML: it renders, it is styled, it
|
|
28
|
+
* lights up on hover. The browser just resolves it relative to the page it sits
|
|
29
|
+
* on, so it lands on /some/article/www.trailmem.com. Nothing about the markup
|
|
30
|
+
* looks wrong — only following the link reveals it.
|
|
31
|
+
*
|
|
32
|
+
* This DELIBERATELY does not try to decide whether the author meant an external
|
|
33
|
+
* host. `trailmem.com` and `index.html` are syntactically identical, and a rule
|
|
34
|
+
* that "fixes" the first breaks the second. It reports doubt; it never rewrites.
|
|
35
|
+
*/
|
|
36
|
+
declare function isSchemeless(raw: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* A bare email address — `name@host.tld`, no scheme, no path.
|
|
39
|
+
*
|
|
40
|
+
* Kept deliberately narrow: anything with a slash, a space or a second @ is
|
|
41
|
+
* not one, so an ordinary URL that happens to contain an @ (userinfo, a query)
|
|
42
|
+
* is untouched.
|
|
43
|
+
*/
|
|
44
|
+
declare function isBareEmail(raw: string): boolean;
|
|
45
|
+
/** The one-click repair the dialog offers. Never applied on its own. */
|
|
46
|
+
declare function withHttps(raw: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Does this address leave the site the editor is standing on?
|
|
49
|
+
*
|
|
50
|
+
* Used ONLY to decide how the "open in a new tab" box starts out — and that
|
|
51
|
+
* starting state is visible in the box itself, so a wrong guess is something
|
|
52
|
+
* the editor can see and change, not a silent rewrite.
|
|
53
|
+
*/
|
|
54
|
+
declare function isExternalHost(raw: string, currentHost: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Every link address inside a stored string — HTML anchors and Markdown links.
|
|
57
|
+
*
|
|
58
|
+
* Markdown IMAGE syntax is excluded on purpose: a relative image path
|
|
59
|
+
* (``) is normal and correct, and flagging it would bury
|
|
60
|
+
* the real findings in noise.
|
|
61
|
+
*/
|
|
62
|
+
declare function extractLinkTargets(text: string): Array<{
|
|
63
|
+
syntax: "html" | "markdown";
|
|
64
|
+
value: string;
|
|
65
|
+
}>;
|
|
66
|
+
/**
|
|
67
|
+
* Schemes that must never reach an `href`.
|
|
68
|
+
*
|
|
69
|
+
* `javascript:` in a link is script execution on click, in the SITE's own
|
|
70
|
+
* origin — which is where the editor's own edit-session token lives in
|
|
71
|
+
* localStorage. So one editor planting such a link, and any colleague clicking
|
|
72
|
+
* it, is a token handover. `data:` and `vbscript:` are the same class.
|
|
73
|
+
*
|
|
74
|
+
* The package ALREADY refuses these on every other attribute
|
|
75
|
+
* (preservedLinkAttrs drops `javascript:` values and every `on*` handler) —
|
|
76
|
+
* href was the one that escaped, because it is emitted separately from the
|
|
77
|
+
* attributes it sits beside.
|
|
78
|
+
*
|
|
79
|
+
* Whitespace and control characters are stripped first: browsers ignore them
|
|
80
|
+
* inside a scheme, so `java\tscript:` and ` javascript:` both run.
|
|
81
|
+
*/
|
|
82
|
+
declare function isDangerousUrl(raw: string): boolean;
|
|
83
|
+
|
|
15
84
|
/**
|
|
16
85
|
* Labels for the in-editor UI — the rich-text toolbar (bold/italic/underline
|
|
17
86
|
* tooltips, the visible "colour" + "done" buttons, the emoji tooltip) and the
|
|
@@ -184,4 +253,4 @@ declare function setLinkTarget(el: HTMLElement, newTab: boolean): void;
|
|
|
184
253
|
*/
|
|
185
254
|
declare function htmlToMarkdown(html: string): string;
|
|
186
255
|
|
|
187
|
-
export { type InlineEditLabels, type InlineEditOptions, applyFieldSlice, buildConnectUrl, disconnect, getConnectedToken, htmlToMarkdown, initInlineEdit, positionPopover, renderCurrentRefLine, rescanFields, resolveExistingRef, setLinkTarget };
|
|
256
|
+
export { type InlineEditLabels, type InlineEditOptions, applyFieldSlice, buildConnectUrl, disconnect, extractLinkTargets, getConnectedToken, htmlToMarkdown, initInlineEdit, isBareEmail, isDangerousUrl, isExternalHost, isSchemeless, positionPopover, renderCurrentRefLine, rescanFields, resolveExistingRef, setLinkTarget, withHttps };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
|
+
// src/page-tools.ts
|
|
2
|
+
function parseTags(input) {
|
|
3
|
+
const set = /* @__PURE__ */ new Set();
|
|
4
|
+
const ud = [];
|
|
5
|
+
for (const raa of input.split(",")) {
|
|
6
|
+
const t = raa.trim();
|
|
7
|
+
if (!t) continue;
|
|
8
|
+
const noegle = t.toLowerCase();
|
|
9
|
+
if (set.has(noegle)) continue;
|
|
10
|
+
set.add(noegle);
|
|
11
|
+
ud.push(t);
|
|
12
|
+
}
|
|
13
|
+
return ud;
|
|
14
|
+
}
|
|
15
|
+
function mergeTags(eksisterende, nye) {
|
|
16
|
+
const basis = Array.isArray(eksisterende) ? eksisterende.filter((t) => typeof t === "string") : [];
|
|
17
|
+
const kendte = new Set(basis.map((t) => t.toLowerCase()));
|
|
18
|
+
const ud = [...basis];
|
|
19
|
+
for (const t of nye) {
|
|
20
|
+
if (kendte.has(t.toLowerCase())) continue;
|
|
21
|
+
kendte.add(t.toLowerCase());
|
|
22
|
+
ud.push(t);
|
|
23
|
+
}
|
|
24
|
+
return ud;
|
|
25
|
+
}
|
|
26
|
+
function primaryDocRef(ankre) {
|
|
27
|
+
const taeller = /* @__PURE__ */ new Map();
|
|
28
|
+
for (const a of ankre) {
|
|
29
|
+
if (!a.collection || !a.slug || a.collection === "globals") continue;
|
|
30
|
+
const noegle = `${a.collection} ${a.slug}`;
|
|
31
|
+
const post = taeller.get(noegle) ?? { collection: a.collection, slug: a.slug, n: 0 };
|
|
32
|
+
post.n++;
|
|
33
|
+
taeller.set(noegle, post);
|
|
34
|
+
}
|
|
35
|
+
let bedst = null;
|
|
36
|
+
for (const post of taeller.values()) {
|
|
37
|
+
if (!bedst || post.n > bedst.n) bedst = post;
|
|
38
|
+
}
|
|
39
|
+
return bedst ? { collection: bedst.collection, slug: bedst.slug } : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
1
42
|
// src/field-slice.ts
|
|
2
43
|
function applyFieldSlice(current, original, next) {
|
|
3
44
|
const first = current.indexOf(original);
|
|
@@ -12,6 +53,7 @@ function applyFieldSlice(current, original, next) {
|
|
|
12
53
|
function isSchemeless(raw) {
|
|
13
54
|
const v = (raw || "").trim();
|
|
14
55
|
if (!v) return false;
|
|
56
|
+
if (isBareEmail(v)) return false;
|
|
15
57
|
if (/^[a-z][a-z0-9+.-]*:/i.test(v)) return false;
|
|
16
58
|
if (v.startsWith("//")) return false;
|
|
17
59
|
if (v.startsWith("/")) return false;
|
|
@@ -19,6 +61,9 @@ function isSchemeless(raw) {
|
|
|
19
61
|
if (v.startsWith("?")) return false;
|
|
20
62
|
return true;
|
|
21
63
|
}
|
|
64
|
+
function isBareEmail(raw) {
|
|
65
|
+
return /^[^\s@/:?#]+@[^\s@/:?#]+\.[a-z]{2,}$/i.test((raw || "").trim());
|
|
66
|
+
}
|
|
22
67
|
function withHttps(raw) {
|
|
23
68
|
return `https://${(raw || "").trim().replace(/^\/+/, "")}`;
|
|
24
69
|
}
|
|
@@ -31,6 +76,18 @@ function isExternalHost(raw, currentHost) {
|
|
|
31
76
|
return false;
|
|
32
77
|
}
|
|
33
78
|
}
|
|
79
|
+
function extractLinkTargets(text) {
|
|
80
|
+
const out = [];
|
|
81
|
+
if (typeof text !== "string") return out;
|
|
82
|
+
for (const m of text.matchAll(/<a\b[^>]*?\bhref\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>]+))/gi)) {
|
|
83
|
+
out.push({ syntax: "html", value: m[2] ?? m[3] ?? m[4] ?? "" });
|
|
84
|
+
}
|
|
85
|
+
for (const m of text.matchAll(/(!)?\[(?:[^\]\[]|\[[^\]]*\])*\]\(([^)\s]+)/g)) {
|
|
86
|
+
if (m[1]) continue;
|
|
87
|
+
out.push({ syntax: "markdown", value: m[2] ?? "" });
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
34
91
|
function isDangerousUrl(raw) {
|
|
35
92
|
const v = (raw || "").replace(/[\u0000-\u0020]/g, "");
|
|
36
93
|
return /^(?:javascript|data|vbscript):/i.test(v);
|
|
@@ -212,6 +269,7 @@ var SQUARE_PEN_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height=
|
|
|
212
269
|
var UL_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/></svg>';
|
|
213
270
|
var OL_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="10" x2="21" y1="6" y2="6"/><line x1="10" x2="21" y1="12" y2="12"/><line x1="10" x2="21" y1="18" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/></svg>';
|
|
214
271
|
var LINK_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7"/><path d="M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7"/></svg>';
|
|
272
|
+
var WRENCH_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>';
|
|
215
273
|
function makeIcon() {
|
|
216
274
|
const icon = document.createElement("span");
|
|
217
275
|
icon.style.cssText = "display:inline-flex;line-height:0;flex:0 0 auto";
|
|
@@ -563,7 +621,7 @@ function renderLinkDialog() {
|
|
|
563
621
|
}
|
|
564
622
|
const existingRef = linkEditing?.getAttribute("data-cms-ref") ?? "";
|
|
565
623
|
const onPageTab = !editing || !!existingRef;
|
|
566
|
-
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"
|
|
624
|
+
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" style="${noticeCss("schemeless")}"><p data-role="schemeless-text" style="margin:0;font-size:11.5px;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, true)}" disabled>${editing ? L.linkSave : L.linkInsert}</button></div></div>`;
|
|
567
625
|
const q = (role) => d.querySelector(`[data-role="${role}"]`);
|
|
568
626
|
const text = q("text");
|
|
569
627
|
const urlIn = q("url");
|
|
@@ -571,7 +629,7 @@ function renderLinkDialog() {
|
|
|
571
629
|
if (editing) {
|
|
572
630
|
text.value = linkEditing?.getAttribute("data-cms-ref-label") === "auto" ? "" : linkEditing?.textContent ?? "";
|
|
573
631
|
if (!existingRef) urlIn.value = linkEditing?.getAttribute("href") ?? "";
|
|
574
|
-
linkNewTab = linkEditing?.getAttribute("target") === "_blank";
|
|
632
|
+
linkNewTab = (linkEditing?.getAttribute("target") ?? "").trim().toLowerCase() === "_blank";
|
|
575
633
|
linkNewTabTouched = true;
|
|
576
634
|
q("remove").appendChild(buildRemoveLink());
|
|
577
635
|
}
|
|
@@ -706,7 +764,8 @@ function syncLinkDialog() {
|
|
|
706
764
|
const own = q("text").value.trim();
|
|
707
765
|
q("hint").innerHTML = own ? uiLabels.linkTextOwnHint : onPage ? uiLabels.linkTextAutoHint : uiLabels.linkUrlHint;
|
|
708
766
|
q("live").style.display = onPage ? "flex" : "none";
|
|
709
|
-
|
|
767
|
+
const submit = q("submit");
|
|
768
|
+
submit.disabled = onPage ? !linkPicked : !q("url").value.trim();
|
|
710
769
|
const raw = q("url").value;
|
|
711
770
|
const dangerous = !onPage && isDangerousUrl(raw);
|
|
712
771
|
const doubtful = !onPage && !dangerous && isSchemeless(raw);
|
|
@@ -717,34 +776,45 @@ function syncLinkDialog() {
|
|
|
717
776
|
notice.setAttribute("data-variant", "blocked");
|
|
718
777
|
q("schemeless-text").innerHTML = uiLabels.linkBlocked.replace(
|
|
719
778
|
"{v}",
|
|
720
|
-
escapeHtml(raw.trim())
|
|
779
|
+
() => escapeHtml(raw.trim())
|
|
721
780
|
);
|
|
722
|
-
|
|
781
|
+
submit.disabled = true;
|
|
723
782
|
} else if (doubtful) {
|
|
724
783
|
notice.setAttribute("style", noticeCss("schemeless"));
|
|
725
784
|
notice.setAttribute("data-variant", "schemeless");
|
|
726
785
|
q("schemeless-text").innerHTML = uiLabels.linkSchemeless.replace(
|
|
727
786
|
"{v}",
|
|
728
|
-
escapeHtml(raw.trim())
|
|
787
|
+
() => escapeHtml(raw.trim())
|
|
729
788
|
);
|
|
730
789
|
}
|
|
790
|
+
submit.setAttribute("style", btnCss(true, submit.disabled));
|
|
791
|
+
if (!show) {
|
|
792
|
+
notice.removeAttribute("data-variant");
|
|
793
|
+
notice.removeAttribute("data-testid");
|
|
794
|
+
q("schemeless-text").innerHTML = "";
|
|
795
|
+
} else {
|
|
796
|
+
notice.setAttribute("data-testid", dangerous ? "inline-link-blocked" : "inline-link-schemeless");
|
|
797
|
+
}
|
|
731
798
|
notice.style.display = show ? "block" : "none";
|
|
732
799
|
q("schemeless-text").style.color = dangerous ? "#f5c6c6" : "#e8d6b0";
|
|
733
800
|
q("add-scheme").style.display = doubtful ? "" : "none";
|
|
734
|
-
if (!
|
|
735
|
-
linkNewTab = isExternalHost(raw, typeof location !== "undefined" ? location.host : "");
|
|
801
|
+
if (!linkNewTabTouched) {
|
|
802
|
+
linkNewTab = onPage ? false : isExternalHost(raw, typeof location !== "undefined" ? location.host : "");
|
|
736
803
|
}
|
|
737
804
|
const box = q("newtab-box");
|
|
738
805
|
if (box) box.setAttribute("style", checkboxCss(linkNewTab));
|
|
739
806
|
q("newtab").setAttribute("aria-pressed", String(linkNewTab));
|
|
740
807
|
}
|
|
741
808
|
function setLinkTarget(el, newTab) {
|
|
809
|
+
const tokens = (el.getAttribute("rel") ?? "").split(/\s+/).filter(Boolean);
|
|
810
|
+
const kept = tokens.filter((t) => t.toLowerCase() !== "noopener");
|
|
742
811
|
if (newTab) {
|
|
743
812
|
el.setAttribute("target", "_blank");
|
|
744
|
-
el.setAttribute("rel", "noopener");
|
|
813
|
+
el.setAttribute("rel", ["noopener", ...kept].join(" "));
|
|
745
814
|
} else {
|
|
746
815
|
el.removeAttribute("target");
|
|
747
|
-
el.
|
|
816
|
+
if (kept.length) el.setAttribute("rel", kept.join(" "));
|
|
817
|
+
else el.removeAttribute("rel");
|
|
748
818
|
}
|
|
749
819
|
}
|
|
750
820
|
function applyLink() {
|
|
@@ -801,7 +871,11 @@ var noticeCss = (variant) => "display:none;margin-top:8px;padding:8px 10px;borde
|
|
|
801
871
|
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;");
|
|
802
872
|
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;";
|
|
803
873
|
var labelCss = () => "font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:#9aa3b2;margin:12px 0 5px;display:block;";
|
|
804
|
-
var btnCss = (primary) => "height:33px;border-radius:7px;font-size:13px;
|
|
874
|
+
var btnCss = (primary, disabled = false) => "height:33px;border-radius:7px;font-size:13px;padding:0 14px;font-weight:600;font-family:inherit;" + // A disabled button that still looks clickable is the silent state this whole
|
|
875
|
+
// card exists to remove: the editor clicks, nothing happens, and the dialog
|
|
876
|
+
// reads as broken rather than as refusing. It matters more now that `disabled`
|
|
877
|
+
// is what stops a javascript: address being inserted.
|
|
878
|
+
(disabled ? "cursor:not-allowed;opacity:.42;" : "cursor:pointer;") + (primary ? "background:#00b2ff;color:#04121b;border:none;" : "background:none;border:1px solid #3a3f4a;color:#fff;font-weight:500;");
|
|
805
879
|
function toggleEmojiPicker(anchor) {
|
|
806
880
|
if (!emojiPicker) emojiPicker = buildEmojiPicker();
|
|
807
881
|
if (emojiPicker.style.display === "block") {
|
|
@@ -1030,7 +1104,7 @@ function preservedLinkAttrs(el) {
|
|
|
1030
1104
|
const name = attr.name.toLowerCase();
|
|
1031
1105
|
if (name === "href") continue;
|
|
1032
1106
|
if (name.startsWith("on")) continue;
|
|
1033
|
-
if (
|
|
1107
|
+
if (isDangerousUrl(attr.value)) continue;
|
|
1034
1108
|
out.push(`${name}="${escapeAttr(attr.value)}"`);
|
|
1035
1109
|
}
|
|
1036
1110
|
return out;
|
|
@@ -1202,7 +1276,172 @@ function showActiveBadge(options) {
|
|
|
1202
1276
|
badge.addEventListener("mouseenter", () => badge.style.opacity = "1");
|
|
1203
1277
|
badge.addEventListener("mouseleave", () => badge.style.opacity = ".9");
|
|
1204
1278
|
badge.addEventListener("click", exitEditMode);
|
|
1205
|
-
|
|
1279
|
+
badge.style.position = "static";
|
|
1280
|
+
badge.style.borderRadius = "999px 0 0 999px";
|
|
1281
|
+
const raekke = document.createElement("div");
|
|
1282
|
+
raekke.setAttribute("data-cms-inline-edit-toolsrow", "");
|
|
1283
|
+
raekke.style.cssText = "position:fixed;bottom:16px;left:16px;display:inline-flex;align-items:stretch;z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);border-radius:999px;overflow:visible;";
|
|
1284
|
+
const tools = document.createElement("button");
|
|
1285
|
+
tools.type = "button";
|
|
1286
|
+
tools.setAttribute("data-cms-inline-edit-tools", "");
|
|
1287
|
+
tools.setAttribute("data-testid", "inline-edit-tools");
|
|
1288
|
+
tools.setAttribute("aria-label", "Tools");
|
|
1289
|
+
tools.title = "Tools";
|
|
1290
|
+
tools.style.cssText = "display:inline-flex;align-items:center;background:#1c2027;color:#8b93a3;padding:8px 12px;border:none;border-left:1px solid rgba(255,255,255,.12);border-radius:0 999px 999px 0;cursor:pointer;font:600 12px system-ui,sans-serif;";
|
|
1291
|
+
tools.innerHTML = WRENCH_SVG;
|
|
1292
|
+
tools.addEventListener("mouseenter", () => tools.style.color = "#fff");
|
|
1293
|
+
tools.addEventListener("mouseleave", () => tools.style.color = "#8b93a3");
|
|
1294
|
+
tools.addEventListener("click", () => togglePageTools(options, raekke));
|
|
1295
|
+
raekke.append(badge, tools);
|
|
1296
|
+
document.body.appendChild(raekke);
|
|
1297
|
+
}
|
|
1298
|
+
async function hentDoc(collection, slug, token, options) {
|
|
1299
|
+
const res = await fetch(`${options.cmsBaseUrl}/api/cms/${collection}/${slug}?site=${options.siteId}`, {
|
|
1300
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1301
|
+
});
|
|
1302
|
+
if (!res.ok) throw new Error(`GET failed: ${res.status}`);
|
|
1303
|
+
const doc = await res.json();
|
|
1304
|
+
return JSON.parse(JSON.stringify(doc.data ?? {}));
|
|
1305
|
+
}
|
|
1306
|
+
async function gemDoc(collection, slug, data, token, options) {
|
|
1307
|
+
const res = await fetch(`${options.cmsBaseUrl}/api/cms/${collection}/${slug}?site=${options.siteId}`, {
|
|
1308
|
+
method: "PATCH",
|
|
1309
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
1310
|
+
body: JSON.stringify({ data })
|
|
1311
|
+
});
|
|
1312
|
+
if (!res.ok) throw new Error(`PATCH failed: ${res.status}`);
|
|
1313
|
+
}
|
|
1314
|
+
function togglePageTools(options, forankring) {
|
|
1315
|
+
const \u00E5ben = document.querySelector("[data-cms-inline-edit-tools-popup]");
|
|
1316
|
+
if (\u00E5ben) {
|
|
1317
|
+
\u00E5ben.remove();
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
const token = getConnectedToken(options);
|
|
1321
|
+
if (!token) return;
|
|
1322
|
+
const ankre = Array.from(
|
|
1323
|
+
document.querySelectorAll("[data-cms-field]")
|
|
1324
|
+
).map((el) => {
|
|
1325
|
+
const a = {};
|
|
1326
|
+
if (el.dataset.cmsCollection) a.collection = el.dataset.cmsCollection;
|
|
1327
|
+
if (el.dataset.cmsSlug) a.slug = el.dataset.cmsSlug;
|
|
1328
|
+
return a;
|
|
1329
|
+
});
|
|
1330
|
+
const ref = primaryDocRef(ankre);
|
|
1331
|
+
if (!ref) return;
|
|
1332
|
+
const popup = document.createElement("div");
|
|
1333
|
+
popup.setAttribute("data-cms-inline-edit-tools-popup", "");
|
|
1334
|
+
popup.setAttribute("data-testid", "page-tools-popup");
|
|
1335
|
+
popup.style.cssText = "position:fixed;bottom:56px;left:16px;z-index:2147483647;background:#1c2027;border:1px solid #3a3f4a;border-radius:12px;padding:10px;min-width:230px;box-shadow:0 8px 32px rgba(0,0,0,.5);font-family:system-ui,sans-serif;color:#fff;display:flex;flex-direction:column;gap:8px;";
|
|
1336
|
+
const featRow = document.createElement("button");
|
|
1337
|
+
featRow.type = "button";
|
|
1338
|
+
featRow.setAttribute("data-testid", "page-tools-featured");
|
|
1339
|
+
featRow.style.cssText = "display:flex;align-items:center;gap:8px;background:rgba(255,255,255,.04);border:1px solid #3a3f4a;border-radius:9px;padding:8px 10px;font:600 12px system-ui,sans-serif;color:#fff;cursor:pointer;text-align:left;";
|
|
1340
|
+
featRow.textContent = "\u2605 Featured";
|
|
1341
|
+
featRow.disabled = true;
|
|
1342
|
+
popup.appendChild(featRow);
|
|
1343
|
+
const visFeatured = (til) => {
|
|
1344
|
+
featRow.style.borderColor = til ? "#00b2ff" : "#3a3f4a";
|
|
1345
|
+
featRow.style.color = til ? "#40c8ff" : "#fff";
|
|
1346
|
+
featRow.style.background = til ? "rgba(0,178,255,.13)" : "rgba(255,255,255,.04)";
|
|
1347
|
+
};
|
|
1348
|
+
let featured = false;
|
|
1349
|
+
void hentDoc(ref.collection, ref.slug, token, options).then((data) => {
|
|
1350
|
+
featured = data.featured === true;
|
|
1351
|
+
visFeatured(featured);
|
|
1352
|
+
featRow.disabled = false;
|
|
1353
|
+
}).catch(() => {
|
|
1354
|
+
featRow.textContent = "\u2605 Featured \u2014 kunne ikke l\xE6ses";
|
|
1355
|
+
});
|
|
1356
|
+
featRow.addEventListener("click", async () => {
|
|
1357
|
+
featRow.disabled = true;
|
|
1358
|
+
try {
|
|
1359
|
+
const data = await hentDoc(ref.collection, ref.slug, token, options);
|
|
1360
|
+
data.featured = !(data.featured === true);
|
|
1361
|
+
await gemDoc(ref.collection, ref.slug, data, token, options);
|
|
1362
|
+
const frisk = await hentDoc(ref.collection, ref.slug, token, options);
|
|
1363
|
+
featured = frisk.featured === true;
|
|
1364
|
+
visFeatured(featured);
|
|
1365
|
+
} catch {
|
|
1366
|
+
featRow.style.borderColor = "#f3522c";
|
|
1367
|
+
} finally {
|
|
1368
|
+
featRow.disabled = false;
|
|
1369
|
+
}
|
|
1370
|
+
});
|
|
1371
|
+
const tagRow = document.createElement("div");
|
|
1372
|
+
tagRow.style.cssText = "display:flex;align-items:center;gap:6px;";
|
|
1373
|
+
const plus = document.createElement("button");
|
|
1374
|
+
plus.type = "button";
|
|
1375
|
+
plus.setAttribute("data-testid", "page-tools-tags-plus");
|
|
1376
|
+
plus.style.cssText = "background:rgba(255,255,255,.04);border:1px solid #3a3f4a;border-radius:9px;padding:8px 12px;color:#fff;font:600 12px system-ui,sans-serif;cursor:pointer;";
|
|
1377
|
+
plus.textContent = "# Tags +";
|
|
1378
|
+
const felt = document.createElement("input");
|
|
1379
|
+
felt.type = "text";
|
|
1380
|
+
felt.placeholder = "tag1, tag2, tag3";
|
|
1381
|
+
felt.setAttribute("data-testid", "page-tools-tags-input");
|
|
1382
|
+
felt.style.cssText = "flex:1;min-width:0;display:none;background:#12151a;border:1px solid #3a3f4a;border-radius:8px;padding:7px 9px;color:#fff;font:12px system-ui,sans-serif;outline:none;";
|
|
1383
|
+
const gem = document.createElement("button");
|
|
1384
|
+
gem.type = "button";
|
|
1385
|
+
gem.setAttribute("data-testid", "page-tools-tags-gem");
|
|
1386
|
+
gem.style.cssText = "display:none;background:#00b2ff;border:none;border-radius:8px;padding:7px 11px;color:#04222f;font:700 12px system-ui,sans-serif;cursor:pointer;";
|
|
1387
|
+
gem.textContent = "Gem";
|
|
1388
|
+
plus.addEventListener("click", () => {
|
|
1389
|
+
plus.style.display = "none";
|
|
1390
|
+
felt.style.display = "block";
|
|
1391
|
+
gem.style.display = "inline-block";
|
|
1392
|
+
felt.focus();
|
|
1393
|
+
});
|
|
1394
|
+
const gemTags = async () => {
|
|
1395
|
+
const nye = parseTags(felt.value);
|
|
1396
|
+
if (!nye.length) return;
|
|
1397
|
+
gem.disabled = true;
|
|
1398
|
+
gem.textContent = "\u2026";
|
|
1399
|
+
try {
|
|
1400
|
+
const data = await hentDoc(ref.collection, ref.slug, token, options);
|
|
1401
|
+
data.tags = mergeTags(data.tags, nye);
|
|
1402
|
+
await gemDoc(ref.collection, ref.slug, data, token, options);
|
|
1403
|
+
const frisk = await hentDoc(ref.collection, ref.slug, token, options);
|
|
1404
|
+
const holdt = Array.isArray(frisk.tags) && nye.every((t) => frisk.tags.some((e) => String(e).toLowerCase() === t.toLowerCase()));
|
|
1405
|
+
gem.textContent = holdt ? "Gemt \u2713" : "Fejl";
|
|
1406
|
+
if (holdt) {
|
|
1407
|
+
felt.value = "";
|
|
1408
|
+
setTimeout(() => {
|
|
1409
|
+
gem.textContent = "Gem";
|
|
1410
|
+
gem.disabled = false;
|
|
1411
|
+
}, 1200);
|
|
1412
|
+
} else {
|
|
1413
|
+
gem.disabled = false;
|
|
1414
|
+
}
|
|
1415
|
+
} catch {
|
|
1416
|
+
gem.textContent = "Fejl";
|
|
1417
|
+
gem.disabled = false;
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
gem.addEventListener("click", () => void gemTags());
|
|
1421
|
+
felt.addEventListener("keydown", (e) => {
|
|
1422
|
+
if (e.key === "Enter") {
|
|
1423
|
+
e.preventDefault();
|
|
1424
|
+
void gemTags();
|
|
1425
|
+
}
|
|
1426
|
+
});
|
|
1427
|
+
tagRow.append(plus, felt, gem);
|
|
1428
|
+
popup.appendChild(tagRow);
|
|
1429
|
+
const lukP\u00E5Klik = (e) => {
|
|
1430
|
+
if (!popup.contains(e.target) && !forankring.contains(e.target)) luk();
|
|
1431
|
+
};
|
|
1432
|
+
const lukP\u00E5Esc = (e) => {
|
|
1433
|
+
if (e.key === "Escape") luk();
|
|
1434
|
+
};
|
|
1435
|
+
const luk = () => {
|
|
1436
|
+
popup.remove();
|
|
1437
|
+
document.removeEventListener("mousedown", lukP\u00E5Klik);
|
|
1438
|
+
document.removeEventListener("keydown", lukP\u00E5Esc);
|
|
1439
|
+
};
|
|
1440
|
+
setTimeout(() => {
|
|
1441
|
+
document.addEventListener("mousedown", lukP\u00E5Klik);
|
|
1442
|
+
document.addEventListener("keydown", lukP\u00E5Esc);
|
|
1443
|
+
}, 0);
|
|
1444
|
+
document.body.appendChild(popup);
|
|
1206
1445
|
}
|
|
1207
1446
|
function decodeJwtPayload(token) {
|
|
1208
1447
|
try {
|
|
@@ -1228,13 +1467,19 @@ export {
|
|
|
1228
1467
|
applyFieldSlice,
|
|
1229
1468
|
buildConnectUrl,
|
|
1230
1469
|
disconnect,
|
|
1470
|
+
extractLinkTargets,
|
|
1231
1471
|
getConnectedToken,
|
|
1232
1472
|
htmlToMarkdown,
|
|
1233
1473
|
initInlineEdit,
|
|
1474
|
+
isBareEmail,
|
|
1475
|
+
isDangerousUrl,
|
|
1476
|
+
isExternalHost,
|
|
1477
|
+
isSchemeless,
|
|
1234
1478
|
positionPopover,
|
|
1235
1479
|
renderCurrentRefLine,
|
|
1236
1480
|
rescanFields,
|
|
1237
1481
|
resolveExistingRef,
|
|
1238
|
-
setLinkTarget
|
|
1482
|
+
setLinkTarget,
|
|
1483
|
+
withHttps
|
|
1239
1484
|
};
|
|
1240
1485
|
//# sourceMappingURL=index.js.map
|