@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 CHANGED
@@ -23,17 +23,64 @@ __export(index_exports, {
23
23
  applyFieldSlice: () => applyFieldSlice,
24
24
  buildConnectUrl: () => buildConnectUrl,
25
25
  disconnect: () => disconnect,
26
+ extractLinkTargets: () => extractLinkTargets,
26
27
  getConnectedToken: () => getConnectedToken,
27
28
  htmlToMarkdown: () => htmlToMarkdown,
28
29
  initInlineEdit: () => initInlineEdit,
30
+ isBareEmail: () => isBareEmail,
31
+ isDangerousUrl: () => isDangerousUrl,
32
+ isExternalHost: () => isExternalHost,
33
+ isSchemeless: () => isSchemeless,
29
34
  positionPopover: () => positionPopover,
30
35
  renderCurrentRefLine: () => renderCurrentRefLine,
31
36
  rescanFields: () => rescanFields,
32
37
  resolveExistingRef: () => resolveExistingRef,
33
- setLinkTarget: () => setLinkTarget
38
+ setLinkTarget: () => setLinkTarget,
39
+ withHttps: () => withHttps
34
40
  });
35
41
  module.exports = __toCommonJS(index_exports);
36
42
 
43
+ // src/page-tools.ts
44
+ function parseTags(input) {
45
+ const set = /* @__PURE__ */ new Set();
46
+ const ud = [];
47
+ for (const raa of input.split(",")) {
48
+ const t = raa.trim();
49
+ if (!t) continue;
50
+ const noegle = t.toLowerCase();
51
+ if (set.has(noegle)) continue;
52
+ set.add(noegle);
53
+ ud.push(t);
54
+ }
55
+ return ud;
56
+ }
57
+ function mergeTags(eksisterende, nye) {
58
+ const basis = Array.isArray(eksisterende) ? eksisterende.filter((t) => typeof t === "string") : [];
59
+ const kendte = new Set(basis.map((t) => t.toLowerCase()));
60
+ const ud = [...basis];
61
+ for (const t of nye) {
62
+ if (kendte.has(t.toLowerCase())) continue;
63
+ kendte.add(t.toLowerCase());
64
+ ud.push(t);
65
+ }
66
+ return ud;
67
+ }
68
+ function primaryDocRef(ankre) {
69
+ const taeller = /* @__PURE__ */ new Map();
70
+ for (const a of ankre) {
71
+ if (!a.collection || !a.slug || a.collection === "globals") continue;
72
+ const noegle = `${a.collection} ${a.slug}`;
73
+ const post = taeller.get(noegle) ?? { collection: a.collection, slug: a.slug, n: 0 };
74
+ post.n++;
75
+ taeller.set(noegle, post);
76
+ }
77
+ let bedst = null;
78
+ for (const post of taeller.values()) {
79
+ if (!bedst || post.n > bedst.n) bedst = post;
80
+ }
81
+ return bedst ? { collection: bedst.collection, slug: bedst.slug } : null;
82
+ }
83
+
37
84
  // src/field-slice.ts
38
85
  function applyFieldSlice(current, original, next) {
39
86
  const first = current.indexOf(original);
@@ -48,6 +95,7 @@ function applyFieldSlice(current, original, next) {
48
95
  function isSchemeless(raw) {
49
96
  const v = (raw || "").trim();
50
97
  if (!v) return false;
98
+ if (isBareEmail(v)) return false;
51
99
  if (/^[a-z][a-z0-9+.-]*:/i.test(v)) return false;
52
100
  if (v.startsWith("//")) return false;
53
101
  if (v.startsWith("/")) return false;
@@ -55,6 +103,9 @@ function isSchemeless(raw) {
55
103
  if (v.startsWith("?")) return false;
56
104
  return true;
57
105
  }
106
+ function isBareEmail(raw) {
107
+ return /^[^\s@/:?#]+@[^\s@/:?#]+\.[a-z]{2,}$/i.test((raw || "").trim());
108
+ }
58
109
  function withHttps(raw) {
59
110
  return `https://${(raw || "").trim().replace(/^\/+/, "")}`;
60
111
  }
@@ -67,6 +118,18 @@ function isExternalHost(raw, currentHost) {
67
118
  return false;
68
119
  }
69
120
  }
121
+ function extractLinkTargets(text) {
122
+ const out = [];
123
+ if (typeof text !== "string") return out;
124
+ for (const m of text.matchAll(/<a\b[^>]*?\bhref\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>]+))/gi)) {
125
+ out.push({ syntax: "html", value: m[2] ?? m[3] ?? m[4] ?? "" });
126
+ }
127
+ for (const m of text.matchAll(/(!)?\[(?:[^\]\[]|\[[^\]]*\])*\]\(([^)\s]+)/g)) {
128
+ if (m[1]) continue;
129
+ out.push({ syntax: "markdown", value: m[2] ?? "" });
130
+ }
131
+ return out;
132
+ }
70
133
  function isDangerousUrl(raw) {
71
134
  const v = (raw || "").replace(/[\u0000-\u0020]/g, "");
72
135
  return /^(?:javascript|data|vbscript):/i.test(v);
@@ -248,6 +311,7 @@ var SQUARE_PEN_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height=
248
311
  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>';
249
312
  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>';
250
313
  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>';
314
+ 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>';
251
315
  function makeIcon() {
252
316
  const icon = document.createElement("span");
253
317
  icon.style.cssText = "display:inline-flex;line-height:0;flex:0 0 auto";
@@ -599,7 +663,7 @@ function renderLinkDialog() {
599
663
  }
600
664
  const existingRef = linkEditing?.getAttribute("data-cms-ref") ?? "";
601
665
  const onPageTab = !editing || !!existingRef;
602
- 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="${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)}" disabled>${editing ? L.linkSave : L.linkInsert}</button></div></div>`;
666
+ 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>`;
603
667
  const q = (role) => d.querySelector(`[data-role="${role}"]`);
604
668
  const text = q("text");
605
669
  const urlIn = q("url");
@@ -607,7 +671,7 @@ function renderLinkDialog() {
607
671
  if (editing) {
608
672
  text.value = linkEditing?.getAttribute("data-cms-ref-label") === "auto" ? "" : linkEditing?.textContent ?? "";
609
673
  if (!existingRef) urlIn.value = linkEditing?.getAttribute("href") ?? "";
610
- linkNewTab = linkEditing?.getAttribute("target") === "_blank";
674
+ linkNewTab = (linkEditing?.getAttribute("target") ?? "").trim().toLowerCase() === "_blank";
611
675
  linkNewTabTouched = true;
612
676
  q("remove").appendChild(buildRemoveLink());
613
677
  }
@@ -742,7 +806,8 @@ function syncLinkDialog() {
742
806
  const own = q("text").value.trim();
743
807
  q("hint").innerHTML = own ? uiLabels.linkTextOwnHint : onPage ? uiLabels.linkTextAutoHint : uiLabels.linkUrlHint;
744
808
  q("live").style.display = onPage ? "flex" : "none";
745
- q("submit").disabled = onPage ? !linkPicked : !q("url").value.trim();
809
+ const submit = q("submit");
810
+ submit.disabled = onPage ? !linkPicked : !q("url").value.trim();
746
811
  const raw = q("url").value;
747
812
  const dangerous = !onPage && isDangerousUrl(raw);
748
813
  const doubtful = !onPage && !dangerous && isSchemeless(raw);
@@ -753,34 +818,45 @@ function syncLinkDialog() {
753
818
  notice.setAttribute("data-variant", "blocked");
754
819
  q("schemeless-text").innerHTML = uiLabels.linkBlocked.replace(
755
820
  "{v}",
756
- escapeHtml(raw.trim())
821
+ () => escapeHtml(raw.trim())
757
822
  );
758
- q("submit").disabled = true;
823
+ submit.disabled = true;
759
824
  } else if (doubtful) {
760
825
  notice.setAttribute("style", noticeCss("schemeless"));
761
826
  notice.setAttribute("data-variant", "schemeless");
762
827
  q("schemeless-text").innerHTML = uiLabels.linkSchemeless.replace(
763
828
  "{v}",
764
- escapeHtml(raw.trim())
829
+ () => escapeHtml(raw.trim())
765
830
  );
766
831
  }
832
+ submit.setAttribute("style", btnCss(true, submit.disabled));
833
+ if (!show) {
834
+ notice.removeAttribute("data-variant");
835
+ notice.removeAttribute("data-testid");
836
+ q("schemeless-text").innerHTML = "";
837
+ } else {
838
+ notice.setAttribute("data-testid", dangerous ? "inline-link-blocked" : "inline-link-schemeless");
839
+ }
767
840
  notice.style.display = show ? "block" : "none";
768
841
  q("schemeless-text").style.color = dangerous ? "#f5c6c6" : "#e8d6b0";
769
842
  q("add-scheme").style.display = doubtful ? "" : "none";
770
- if (!onPage && !linkNewTabTouched) {
771
- linkNewTab = isExternalHost(raw, typeof location !== "undefined" ? location.host : "");
843
+ if (!linkNewTabTouched) {
844
+ linkNewTab = onPage ? false : isExternalHost(raw, typeof location !== "undefined" ? location.host : "");
772
845
  }
773
846
  const box = q("newtab-box");
774
847
  if (box) box.setAttribute("style", checkboxCss(linkNewTab));
775
848
  q("newtab").setAttribute("aria-pressed", String(linkNewTab));
776
849
  }
777
850
  function setLinkTarget(el, newTab) {
851
+ const tokens = (el.getAttribute("rel") ?? "").split(/\s+/).filter(Boolean);
852
+ const kept = tokens.filter((t) => t.toLowerCase() !== "noopener");
778
853
  if (newTab) {
779
854
  el.setAttribute("target", "_blank");
780
- el.setAttribute("rel", "noopener");
855
+ el.setAttribute("rel", ["noopener", ...kept].join(" "));
781
856
  } else {
782
857
  el.removeAttribute("target");
783
- el.removeAttribute("rel");
858
+ if (kept.length) el.setAttribute("rel", kept.join(" "));
859
+ else el.removeAttribute("rel");
784
860
  }
785
861
  }
786
862
  function applyLink() {
@@ -837,7 +913,11 @@ var noticeCss = (variant) => "display:none;margin-top:8px;padding:8px 10px;borde
837
913
  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;");
838
914
  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;";
839
915
  var labelCss = () => "font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:#9aa3b2;margin:12px 0 5px;display:block;";
840
- 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;");
916
+ 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
917
+ // card exists to remove: the editor clicks, nothing happens, and the dialog
918
+ // reads as broken rather than as refusing. It matters more now that `disabled`
919
+ // is what stops a javascript: address being inserted.
920
+ (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;");
841
921
  function toggleEmojiPicker(anchor) {
842
922
  if (!emojiPicker) emojiPicker = buildEmojiPicker();
843
923
  if (emojiPicker.style.display === "block") {
@@ -1066,7 +1146,7 @@ function preservedLinkAttrs(el) {
1066
1146
  const name = attr.name.toLowerCase();
1067
1147
  if (name === "href") continue;
1068
1148
  if (name.startsWith("on")) continue;
1069
- if (/^javascript:/i.test(attr.value.trim())) continue;
1149
+ if (isDangerousUrl(attr.value)) continue;
1070
1150
  out.push(`${name}="${escapeAttr(attr.value)}"`);
1071
1151
  }
1072
1152
  return out;
@@ -1238,7 +1318,172 @@ function showActiveBadge(options) {
1238
1318
  badge.addEventListener("mouseenter", () => badge.style.opacity = "1");
1239
1319
  badge.addEventListener("mouseleave", () => badge.style.opacity = ".9");
1240
1320
  badge.addEventListener("click", exitEditMode);
1241
- document.body.appendChild(badge);
1321
+ badge.style.position = "static";
1322
+ badge.style.borderRadius = "999px 0 0 999px";
1323
+ const raekke = document.createElement("div");
1324
+ raekke.setAttribute("data-cms-inline-edit-toolsrow", "");
1325
+ 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;";
1326
+ const tools = document.createElement("button");
1327
+ tools.type = "button";
1328
+ tools.setAttribute("data-cms-inline-edit-tools", "");
1329
+ tools.setAttribute("data-testid", "inline-edit-tools");
1330
+ tools.setAttribute("aria-label", "Tools");
1331
+ tools.title = "Tools";
1332
+ 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;";
1333
+ tools.innerHTML = WRENCH_SVG;
1334
+ tools.addEventListener("mouseenter", () => tools.style.color = "#fff");
1335
+ tools.addEventListener("mouseleave", () => tools.style.color = "#8b93a3");
1336
+ tools.addEventListener("click", () => togglePageTools(options, raekke));
1337
+ raekke.append(badge, tools);
1338
+ document.body.appendChild(raekke);
1339
+ }
1340
+ async function hentDoc(collection, slug, token, options) {
1341
+ const res = await fetch(`${options.cmsBaseUrl}/api/cms/${collection}/${slug}?site=${options.siteId}`, {
1342
+ headers: { Authorization: `Bearer ${token}` }
1343
+ });
1344
+ if (!res.ok) throw new Error(`GET failed: ${res.status}`);
1345
+ const doc = await res.json();
1346
+ return JSON.parse(JSON.stringify(doc.data ?? {}));
1347
+ }
1348
+ async function gemDoc(collection, slug, data, token, options) {
1349
+ const res = await fetch(`${options.cmsBaseUrl}/api/cms/${collection}/${slug}?site=${options.siteId}`, {
1350
+ method: "PATCH",
1351
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
1352
+ body: JSON.stringify({ data })
1353
+ });
1354
+ if (!res.ok) throw new Error(`PATCH failed: ${res.status}`);
1355
+ }
1356
+ function togglePageTools(options, forankring) {
1357
+ const \u00E5ben = document.querySelector("[data-cms-inline-edit-tools-popup]");
1358
+ if (\u00E5ben) {
1359
+ \u00E5ben.remove();
1360
+ return;
1361
+ }
1362
+ const token = getConnectedToken(options);
1363
+ if (!token) return;
1364
+ const ankre = Array.from(
1365
+ document.querySelectorAll("[data-cms-field]")
1366
+ ).map((el) => {
1367
+ const a = {};
1368
+ if (el.dataset.cmsCollection) a.collection = el.dataset.cmsCollection;
1369
+ if (el.dataset.cmsSlug) a.slug = el.dataset.cmsSlug;
1370
+ return a;
1371
+ });
1372
+ const ref = primaryDocRef(ankre);
1373
+ if (!ref) return;
1374
+ const popup = document.createElement("div");
1375
+ popup.setAttribute("data-cms-inline-edit-tools-popup", "");
1376
+ popup.setAttribute("data-testid", "page-tools-popup");
1377
+ 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;";
1378
+ const featRow = document.createElement("button");
1379
+ featRow.type = "button";
1380
+ featRow.setAttribute("data-testid", "page-tools-featured");
1381
+ 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;";
1382
+ featRow.textContent = "\u2605 Featured";
1383
+ featRow.disabled = true;
1384
+ popup.appendChild(featRow);
1385
+ const visFeatured = (til) => {
1386
+ featRow.style.borderColor = til ? "#00b2ff" : "#3a3f4a";
1387
+ featRow.style.color = til ? "#40c8ff" : "#fff";
1388
+ featRow.style.background = til ? "rgba(0,178,255,.13)" : "rgba(255,255,255,.04)";
1389
+ };
1390
+ let featured = false;
1391
+ void hentDoc(ref.collection, ref.slug, token, options).then((data) => {
1392
+ featured = data.featured === true;
1393
+ visFeatured(featured);
1394
+ featRow.disabled = false;
1395
+ }).catch(() => {
1396
+ featRow.textContent = "\u2605 Featured \u2014 kunne ikke l\xE6ses";
1397
+ });
1398
+ featRow.addEventListener("click", async () => {
1399
+ featRow.disabled = true;
1400
+ try {
1401
+ const data = await hentDoc(ref.collection, ref.slug, token, options);
1402
+ data.featured = !(data.featured === true);
1403
+ await gemDoc(ref.collection, ref.slug, data, token, options);
1404
+ const frisk = await hentDoc(ref.collection, ref.slug, token, options);
1405
+ featured = frisk.featured === true;
1406
+ visFeatured(featured);
1407
+ } catch {
1408
+ featRow.style.borderColor = "#f3522c";
1409
+ } finally {
1410
+ featRow.disabled = false;
1411
+ }
1412
+ });
1413
+ const tagRow = document.createElement("div");
1414
+ tagRow.style.cssText = "display:flex;align-items:center;gap:6px;";
1415
+ const plus = document.createElement("button");
1416
+ plus.type = "button";
1417
+ plus.setAttribute("data-testid", "page-tools-tags-plus");
1418
+ 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;";
1419
+ plus.textContent = "# Tags +";
1420
+ const felt = document.createElement("input");
1421
+ felt.type = "text";
1422
+ felt.placeholder = "tag1, tag2, tag3";
1423
+ felt.setAttribute("data-testid", "page-tools-tags-input");
1424
+ 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;";
1425
+ const gem = document.createElement("button");
1426
+ gem.type = "button";
1427
+ gem.setAttribute("data-testid", "page-tools-tags-gem");
1428
+ 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;";
1429
+ gem.textContent = "Gem";
1430
+ plus.addEventListener("click", () => {
1431
+ plus.style.display = "none";
1432
+ felt.style.display = "block";
1433
+ gem.style.display = "inline-block";
1434
+ felt.focus();
1435
+ });
1436
+ const gemTags = async () => {
1437
+ const nye = parseTags(felt.value);
1438
+ if (!nye.length) return;
1439
+ gem.disabled = true;
1440
+ gem.textContent = "\u2026";
1441
+ try {
1442
+ const data = await hentDoc(ref.collection, ref.slug, token, options);
1443
+ data.tags = mergeTags(data.tags, nye);
1444
+ await gemDoc(ref.collection, ref.slug, data, token, options);
1445
+ const frisk = await hentDoc(ref.collection, ref.slug, token, options);
1446
+ const holdt = Array.isArray(frisk.tags) && nye.every((t) => frisk.tags.some((e) => String(e).toLowerCase() === t.toLowerCase()));
1447
+ gem.textContent = holdt ? "Gemt \u2713" : "Fejl";
1448
+ if (holdt) {
1449
+ felt.value = "";
1450
+ setTimeout(() => {
1451
+ gem.textContent = "Gem";
1452
+ gem.disabled = false;
1453
+ }, 1200);
1454
+ } else {
1455
+ gem.disabled = false;
1456
+ }
1457
+ } catch {
1458
+ gem.textContent = "Fejl";
1459
+ gem.disabled = false;
1460
+ }
1461
+ };
1462
+ gem.addEventListener("click", () => void gemTags());
1463
+ felt.addEventListener("keydown", (e) => {
1464
+ if (e.key === "Enter") {
1465
+ e.preventDefault();
1466
+ void gemTags();
1467
+ }
1468
+ });
1469
+ tagRow.append(plus, felt, gem);
1470
+ popup.appendChild(tagRow);
1471
+ const lukP\u00E5Klik = (e) => {
1472
+ if (!popup.contains(e.target) && !forankring.contains(e.target)) luk();
1473
+ };
1474
+ const lukP\u00E5Esc = (e) => {
1475
+ if (e.key === "Escape") luk();
1476
+ };
1477
+ const luk = () => {
1478
+ popup.remove();
1479
+ document.removeEventListener("mousedown", lukP\u00E5Klik);
1480
+ document.removeEventListener("keydown", lukP\u00E5Esc);
1481
+ };
1482
+ setTimeout(() => {
1483
+ document.addEventListener("mousedown", lukP\u00E5Klik);
1484
+ document.addEventListener("keydown", lukP\u00E5Esc);
1485
+ }, 0);
1486
+ document.body.appendChild(popup);
1242
1487
  }
1243
1488
  function decodeJwtPayload(token) {
1244
1489
  try {
@@ -1265,13 +1510,19 @@ function injectStyles() {
1265
1510
  applyFieldSlice,
1266
1511
  buildConnectUrl,
1267
1512
  disconnect,
1513
+ extractLinkTargets,
1268
1514
  getConnectedToken,
1269
1515
  htmlToMarkdown,
1270
1516
  initInlineEdit,
1517
+ isBareEmail,
1518
+ isDangerousUrl,
1519
+ isExternalHost,
1520
+ isSchemeless,
1271
1521
  positionPopover,
1272
1522
  renderCurrentRefLine,
1273
1523
  rescanFields,
1274
1524
  resolveExistingRef,
1275
- setLinkTarget
1525
+ setLinkTarget,
1526
+ withHttps
1276
1527
  });
1277
1528
  //# sourceMappingURL=index.cjs.map