@broberg/cms-inline-edit 0.6.0 → 0.6.2

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.d.cts CHANGED
@@ -46,6 +46,7 @@ interface InlineEditLabels {
46
46
  linkNewTab?: string;
47
47
  linkSchemeless?: string;
48
48
  linkSchemelessFix?: string;
49
+ linkBlocked?: string;
49
50
  linkInsert?: string;
50
51
  linkSave?: string;
51
52
  linkCancel?: string;
package/dist/index.d.ts CHANGED
@@ -46,6 +46,7 @@ interface InlineEditLabels {
46
46
  linkNewTab?: string;
47
47
  linkSchemeless?: string;
48
48
  linkSchemelessFix?: string;
49
+ linkBlocked?: string;
49
50
  linkInsert?: string;
50
51
  linkSave?: string;
51
52
  linkCancel?: string;
package/dist/index.js CHANGED
@@ -31,6 +31,10 @@ function isExternalHost(raw, currentHost) {
31
31
  return false;
32
32
  }
33
33
  }
34
+ function isDangerousUrl(raw) {
35
+ const v = (raw || "").replace(/[\u0000-\u0020]/g, "");
36
+ return /^(?:javascript|data|vbscript):/i.test(v);
37
+ }
34
38
 
35
39
  // src/token-safe.ts
36
40
  var TEXT_NODE = 3;
@@ -89,6 +93,7 @@ var DEFAULT_LABELS = {
89
93
  linkNewTab: "\xC5bn i ny fane",
90
94
  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
95
  linkSchemelessFix: "Tilf\xF8j https://",
96
+ linkBlocked: "<b>{v}</b> kan ikke bruges som link \u2014 adresser af den type kan k\xF8re kode p\xE5 sitet.",
92
97
  linkInsert: "Inds\xE6t link",
93
98
  linkSave: "Gem \xE6ndring",
94
99
  linkCancel: "Annull\xE9r",
@@ -291,10 +296,12 @@ function showIdlePill() {
291
296
  removeIdlePill();
292
297
  const wrap = document.createElement("div");
293
298
  wrap.setAttribute("data-cms-inline-edit-idle", "");
299
+ wrap.setAttribute("data-testid", "inline-edit-idle");
294
300
  wrap.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:hidden;";
295
301
  const edit = document.createElement("button");
296
302
  edit.type = "button";
297
303
  edit.setAttribute("data-cms-inline-edit-enter", "");
304
+ edit.setAttribute("data-testid", "inline-edit-enter");
298
305
  edit.style.cssText = "display:inline-flex;align-items:center;gap:7px;background:#1c2027;color:#fff;font:600 12px system-ui,sans-serif;padding:8px 14px;border:none;cursor:pointer;";
299
306
  const editText = document.createElement("span");
300
307
  editText.textContent = options.connectLabel;
@@ -303,6 +310,7 @@ function showIdlePill() {
303
310
  const out = document.createElement("button");
304
311
  out.type = "button";
305
312
  out.setAttribute("data-cms-inline-edit-logout", "");
313
+ out.setAttribute("data-testid", "inline-edit-logout");
306
314
  out.textContent = options.logoutLabel;
307
315
  out.style.cssText = "background:#141821;color:#9aa4b2;font:600 11px system-ui,sans-serif;padding:8px 12px;border:none;border-left:1px solid rgba(255,255,255,.12);cursor:pointer;";
308
316
  out.addEventListener("click", () => {
@@ -555,7 +563,7 @@ function renderLinkDialog() {
555
563
  }
556
564
  const existingRef = linkEditing?.getAttribute("data-cms-ref") ?? "";
557
565
  const onPageTab = !editing || !!existingRef;
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>`;
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" 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>`;
559
567
  const q = (role) => d.querySelector(`[data-role="${role}"]`);
560
568
  const text = q("text");
561
569
  const urlIn = q("url");
@@ -700,14 +708,29 @@ function syncLinkDialog() {
700
708
  q("live").style.display = onPage ? "flex" : "none";
701
709
  q("submit").disabled = onPage ? !linkPicked : !q("url").value.trim();
702
710
  const raw = q("url").value;
703
- const doubtful = !onPage && isSchemeless(raw);
704
- q("schemeless").style.display = doubtful ? "block" : "none";
705
- if (doubtful) {
711
+ const dangerous = !onPage && isDangerousUrl(raw);
712
+ const doubtful = !onPage && !dangerous && isSchemeless(raw);
713
+ const notice = q("schemeless");
714
+ const show = dangerous || doubtful;
715
+ if (dangerous) {
716
+ notice.setAttribute("style", noticeCss("blocked"));
717
+ notice.setAttribute("data-variant", "blocked");
718
+ q("schemeless-text").innerHTML = uiLabels.linkBlocked.replace(
719
+ "{v}",
720
+ escapeHtml(raw.trim())
721
+ );
722
+ q("submit").disabled = true;
723
+ } else if (doubtful) {
724
+ notice.setAttribute("style", noticeCss("schemeless"));
725
+ notice.setAttribute("data-variant", "schemeless");
706
726
  q("schemeless-text").innerHTML = uiLabels.linkSchemeless.replace(
707
727
  "{v}",
708
728
  escapeHtml(raw.trim())
709
729
  );
710
730
  }
731
+ notice.style.display = show ? "block" : "none";
732
+ q("schemeless-text").style.color = dangerous ? "#f5c6c6" : "#e8d6b0";
733
+ q("add-scheme").style.display = doubtful ? "" : "none";
711
734
  if (!onPage && !linkNewTabTouched) {
712
735
  linkNewTab = isExternalHost(raw, typeof location !== "undefined" ? location.host : "");
713
736
  }
@@ -732,6 +755,7 @@ function applyLink() {
732
755
  const own = q("text").value.trim();
733
756
  const href = onPage ? linkPicked?.path ?? "" : q("url").value.trim();
734
757
  if (!href) return;
758
+ if (isDangerousUrl(href)) return;
735
759
  const ref = onPage && linkPicked ? `${linkPicked.collection}:${linkPicked.slug}` : "";
736
760
  const label = own || (onPage ? linkPicked?.title ?? href : href);
737
761
  const applyTarget = (el) => setLinkTarget(el, linkNewTab);
@@ -773,6 +797,7 @@ function escapeHtml(v) {
773
797
  return v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
774
798
  }
775
799
  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;";
800
+ var noticeCss = (variant) => "display:none;margin-top:8px;padding:8px 10px;border-radius:8px;background:" + (variant === "blocked" ? "rgba(255,90,90,.10)" : "rgba(255,176,32,.10)") + ";border:1px solid " + (variant === "blocked" ? "rgba(255,90,90,.38)" : "rgba(255,176,32,.34)") + ";";
776
801
  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;");
777
802
  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;";
778
803
  var labelCss = () => "font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:#9aa3b2;margin:12px 0 5px;display:block;";
@@ -1064,6 +1089,10 @@ function serializeInlineNode(child) {
1064
1089
  break;
1065
1090
  case "a": {
1066
1091
  const href = el.getAttribute("href") || "";
1092
+ if (isDangerousUrl(href)) {
1093
+ out += inner;
1094
+ break;
1095
+ }
1067
1096
  const extras = preservedLinkAttrs(el);
1068
1097
  if (extras.length > 0) {
1069
1098
  out += `<a ${[`href="${escapeAttr(href)}"`, ...extras].join(" ")}>${inner}</a>`;
@@ -1137,6 +1166,7 @@ function showPill(el, state) {
1137
1166
  if (!pill) {
1138
1167
  pill = document.createElement("span");
1139
1168
  pill.setAttribute("data-cms-inline-edit-pill", "");
1169
+ pill.setAttribute("data-testid", "inline-edit-pill");
1140
1170
  document.body.appendChild(pill);
1141
1171
  pills.set(el, pill);
1142
1172
  }
@@ -1164,6 +1194,7 @@ function showActiveBadge(options) {
1164
1194
  const badge = document.createElement("button");
1165
1195
  badge.type = "button";
1166
1196
  badge.setAttribute("data-cms-inline-edit-badge", "");
1197
+ badge.setAttribute("data-testid", "inline-edit-badge");
1167
1198
  badge.style.cssText = "position:fixed;bottom:16px;left:16px;display:inline-flex;align-items:center;gap:7px;background:#1c2027;color:#fff;font:600 12px system-ui,sans-serif;padding:8px 14px;border-radius:999px;z-index:2147483647;box-shadow:0 4px 16px rgba(0,0,0,.3);border:none;cursor:pointer;opacity:.9;transition:opacity .15s;";
1168
1199
  const text = document.createElement("span");
1169
1200
  text.textContent = options.disconnectLabel;