@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.cjs CHANGED
@@ -67,6 +67,10 @@ function isExternalHost(raw, currentHost) {
67
67
  return false;
68
68
  }
69
69
  }
70
+ function isDangerousUrl(raw) {
71
+ const v = (raw || "").replace(/[\u0000-\u0020]/g, "");
72
+ return /^(?:javascript|data|vbscript):/i.test(v);
73
+ }
70
74
 
71
75
  // src/token-safe.ts
72
76
  var TEXT_NODE = 3;
@@ -125,6 +129,7 @@ var DEFAULT_LABELS = {
125
129
  linkNewTab: "\xC5bn i ny fane",
126
130
  linkSchemeless: "<b>{v}</b> l\xE6ses som en side p\xE5 <b>dette</b> site \u2014 ikke som en adresse ude p\xE5 nettet.",
127
131
  linkSchemelessFix: "Tilf\xF8j https://",
132
+ linkBlocked: "<b>{v}</b> kan ikke bruges som link \u2014 adresser af den type kan k\xF8re kode p\xE5 sitet.",
128
133
  linkInsert: "Inds\xE6t link",
129
134
  linkSave: "Gem \xE6ndring",
130
135
  linkCancel: "Annull\xE9r",
@@ -327,10 +332,12 @@ function showIdlePill() {
327
332
  removeIdlePill();
328
333
  const wrap = document.createElement("div");
329
334
  wrap.setAttribute("data-cms-inline-edit-idle", "");
335
+ wrap.setAttribute("data-testid", "inline-edit-idle");
330
336
  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;";
331
337
  const edit = document.createElement("button");
332
338
  edit.type = "button";
333
339
  edit.setAttribute("data-cms-inline-edit-enter", "");
340
+ edit.setAttribute("data-testid", "inline-edit-enter");
334
341
  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;";
335
342
  const editText = document.createElement("span");
336
343
  editText.textContent = options.connectLabel;
@@ -339,6 +346,7 @@ function showIdlePill() {
339
346
  const out = document.createElement("button");
340
347
  out.type = "button";
341
348
  out.setAttribute("data-cms-inline-edit-logout", "");
349
+ out.setAttribute("data-testid", "inline-edit-logout");
342
350
  out.textContent = options.logoutLabel;
343
351
  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;";
344
352
  out.addEventListener("click", () => {
@@ -591,7 +599,7 @@ function renderLinkDialog() {
591
599
  }
592
600
  const existingRef = linkEditing?.getAttribute("data-cms-ref") ?? "";
593
601
  const onPageTab = !editing || !!existingRef;
594
- 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>`;
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>`;
595
603
  const q = (role) => d.querySelector(`[data-role="${role}"]`);
596
604
  const text = q("text");
597
605
  const urlIn = q("url");
@@ -736,14 +744,29 @@ function syncLinkDialog() {
736
744
  q("live").style.display = onPage ? "flex" : "none";
737
745
  q("submit").disabled = onPage ? !linkPicked : !q("url").value.trim();
738
746
  const raw = q("url").value;
739
- const doubtful = !onPage && isSchemeless(raw);
740
- q("schemeless").style.display = doubtful ? "block" : "none";
741
- if (doubtful) {
747
+ const dangerous = !onPage && isDangerousUrl(raw);
748
+ const doubtful = !onPage && !dangerous && isSchemeless(raw);
749
+ const notice = q("schemeless");
750
+ const show = dangerous || doubtful;
751
+ if (dangerous) {
752
+ notice.setAttribute("style", noticeCss("blocked"));
753
+ notice.setAttribute("data-variant", "blocked");
754
+ q("schemeless-text").innerHTML = uiLabels.linkBlocked.replace(
755
+ "{v}",
756
+ escapeHtml(raw.trim())
757
+ );
758
+ q("submit").disabled = true;
759
+ } else if (doubtful) {
760
+ notice.setAttribute("style", noticeCss("schemeless"));
761
+ notice.setAttribute("data-variant", "schemeless");
742
762
  q("schemeless-text").innerHTML = uiLabels.linkSchemeless.replace(
743
763
  "{v}",
744
764
  escapeHtml(raw.trim())
745
765
  );
746
766
  }
767
+ notice.style.display = show ? "block" : "none";
768
+ q("schemeless-text").style.color = dangerous ? "#f5c6c6" : "#e8d6b0";
769
+ q("add-scheme").style.display = doubtful ? "" : "none";
747
770
  if (!onPage && !linkNewTabTouched) {
748
771
  linkNewTab = isExternalHost(raw, typeof location !== "undefined" ? location.host : "");
749
772
  }
@@ -768,6 +791,7 @@ function applyLink() {
768
791
  const own = q("text").value.trim();
769
792
  const href = onPage ? linkPicked?.path ?? "" : q("url").value.trim();
770
793
  if (!href) return;
794
+ if (isDangerousUrl(href)) return;
771
795
  const ref = onPage && linkPicked ? `${linkPicked.collection}:${linkPicked.slug}` : "";
772
796
  const label = own || (onPage ? linkPicked?.title ?? href : href);
773
797
  const applyTarget = (el) => setLinkTarget(el, linkNewTab);
@@ -809,6 +833,7 @@ function escapeHtml(v) {
809
833
  return v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
810
834
  }
811
835
  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;";
836
+ 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)") + ";";
812
837
  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;");
813
838
  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;";
814
839
  var labelCss = () => "font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:#9aa3b2;margin:12px 0 5px;display:block;";
@@ -1100,6 +1125,10 @@ function serializeInlineNode(child) {
1100
1125
  break;
1101
1126
  case "a": {
1102
1127
  const href = el.getAttribute("href") || "";
1128
+ if (isDangerousUrl(href)) {
1129
+ out += inner;
1130
+ break;
1131
+ }
1103
1132
  const extras = preservedLinkAttrs(el);
1104
1133
  if (extras.length > 0) {
1105
1134
  out += `<a ${[`href="${escapeAttr(href)}"`, ...extras].join(" ")}>${inner}</a>`;
@@ -1173,6 +1202,7 @@ function showPill(el, state) {
1173
1202
  if (!pill) {
1174
1203
  pill = document.createElement("span");
1175
1204
  pill.setAttribute("data-cms-inline-edit-pill", "");
1205
+ pill.setAttribute("data-testid", "inline-edit-pill");
1176
1206
  document.body.appendChild(pill);
1177
1207
  pills.set(el, pill);
1178
1208
  }
@@ -1200,6 +1230,7 @@ function showActiveBadge(options) {
1200
1230
  const badge = document.createElement("button");
1201
1231
  badge.type = "button";
1202
1232
  badge.setAttribute("data-cms-inline-edit-badge", "");
1233
+ badge.setAttribute("data-testid", "inline-edit-badge");
1203
1234
  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;";
1204
1235
  const text = document.createElement("span");
1205
1236
  text.textContent = options.disconnectLabel;