@broberg/cms-inline-edit 0.5.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -29,7 +29,8 @@ __export(index_exports, {
29
29
  positionPopover: () => positionPopover,
30
30
  renderCurrentRefLine: () => renderCurrentRefLine,
31
31
  rescanFields: () => rescanFields,
32
- resolveExistingRef: () => resolveExistingRef
32
+ resolveExistingRef: () => resolveExistingRef,
33
+ setLinkTarget: () => setLinkTarget
33
34
  });
34
35
  module.exports = __toCommonJS(index_exports);
35
36
 
@@ -43,6 +44,30 @@ function applyFieldSlice(current, original, next) {
43
44
  return current.slice(0, first) + next + current.slice(first + original.length);
44
45
  }
45
46
 
47
+ // src/link-target.ts
48
+ function isSchemeless(raw) {
49
+ const v = (raw || "").trim();
50
+ if (!v) return false;
51
+ if (/^[a-z][a-z0-9+.-]*:/i.test(v)) return false;
52
+ if (v.startsWith("//")) return false;
53
+ if (v.startsWith("/")) return false;
54
+ if (v.startsWith("#")) return false;
55
+ if (v.startsWith("?")) return false;
56
+ return true;
57
+ }
58
+ function withHttps(raw) {
59
+ return `https://${(raw || "").trim().replace(/^\/+/, "")}`;
60
+ }
61
+ function isExternalHost(raw, currentHost) {
62
+ const v = (raw || "").trim();
63
+ if (!/^https?:\/\//i.test(v)) return false;
64
+ try {
65
+ return new URL(v).host.toLowerCase() !== (currentHost || "").toLowerCase();
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
46
71
  // src/token-safe.ts
47
72
  var TEXT_NODE = 3;
48
73
  var ELEMENT_NODE = 1;
@@ -97,6 +122,9 @@ var DEFAULT_LABELS = {
97
122
  linkLiveHint: "Linket peger p\xE5 <b>siden</b> \u2014 ikke p\xE5 en adresse. Flyttes siden, eller f\xE5r den et nyt navn, retter linket sig selv.",
98
123
  linkUrlHint: "En fri adresse peger pr\xE6cis d\xE9r, du skriver. Den f\xF8lger <b>ikke</b> med, hvis m\xE5let flytter sig.",
99
124
  linkUrl: "Adresse",
125
+ linkNewTab: "\xC5bn i ny fane",
126
+ 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
+ linkSchemelessFix: "Tilf\xF8j https://",
100
128
  linkInsert: "Inds\xE6t link",
101
129
  linkSave: "Gem \xE6ndring",
102
130
  linkCancel: "Annull\xE9r",
@@ -474,6 +502,8 @@ var savedRange = null;
474
502
  var linkDialog = null;
475
503
  var linkPages = null;
476
504
  var linkPicked = null;
505
+ var linkNewTab = false;
506
+ var linkNewTabTouched = false;
477
507
  var linkEditing = null;
478
508
  function hideLinkDialog() {
479
509
  if (linkDialog) linkDialog.style.display = "none";
@@ -555,9 +585,13 @@ function renderLinkDialog() {
555
585
  if (!d) return;
556
586
  const L = uiLabels;
557
587
  const editing = !!linkEditing;
588
+ if (!editing) {
589
+ linkNewTab = false;
590
+ linkNewTabTouched = false;
591
+ }
558
592
  const existingRef = linkEditing?.getAttribute("data-cms-ref") ?? "";
559
593
  const onPageTab = !editing || !!existingRef;
560
- d.innerHTML = `<div style="display:flex;gap:4px;padding:8px 8px 0"><button type="button" data-testid="inline-link-tab-page" data-tab="page" style="${tabCss(onPageTab)}">${L.linkTabPage}</button><button type="button" data-testid="inline-link-tab-url" data-tab="url" style="${tabCss(!onPageTab)}">${L.linkTabUrl}</button></div><div style="padding:10px 12px 12px"><div data-pane="page" style="display:${onPageTab ? "block" : "none"}"><input data-testid="inline-link-search" data-role="search" placeholder="${L.linkSearch}" style="${fieldCss()}"><div data-role="current" data-testid="inline-link-current" style="display:none;margin-top:8px;padding:7px 10px;background:rgba(0,178,255,.08);border:1px solid rgba(0,178,255,.28);border-radius:8px;font-size:11.5px;color:#c9d1dd;line-height:1.45"></div><div data-role="list" data-testid="inline-link-list" style="margin-top:8px;max-height:196px;overflow-y:auto;border:1px solid #3a3f4a;border-radius:8px;background:#141821"></div></div><div data-pane="url" style="display:${onPageTab ? "none" : "block"}"><label style="${labelCss()}">${L.linkUrl}</label><input data-testid="inline-link-url" data-role="url" placeholder="https://\u2026" style="${fieldCss()}"></div><label style="${labelCss()}">${L.linkText}</label><input data-testid="inline-link-text" data-role="text" placeholder="${L.linkTextAuto}" style="${fieldCss()}"><p data-role="hint" style="font-size:11.5px;color:#9aa3b2;margin:6px 0 0;line-height:1.5"></p><div data-role="live" style="display:flex;gap:8px;margin-top:12px;padding:9px 10px;background:rgba(0,178,255,.08);border:1px solid rgba(0,178,255,.28);border-radius:8px"><p style="margin:0;font-size:11.5px;color:#c9d1dd;line-height:1.55">${L.linkLiveHint}</p></div><div style="display:flex;gap:8px;align-items:center;margin-top:14px"><div data-role="remove"></div><div style="flex:1"></div><button type="button" data-testid="inline-link-cancel" data-role="cancel" style="${btnCss(false)}">${L.linkCancel}</button><button type="button" data-testid="inline-link-submit" data-role="submit" style="${btnCss(true)}" disabled>${editing ? L.linkSave : L.linkInsert}</button></div></div>`;
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>`;
561
595
  const q = (role) => d.querySelector(`[data-role="${role}"]`);
562
596
  const text = q("text");
563
597
  const urlIn = q("url");
@@ -565,6 +599,8 @@ function renderLinkDialog() {
565
599
  if (editing) {
566
600
  text.value = linkEditing?.getAttribute("data-cms-ref-label") === "auto" ? "" : linkEditing?.textContent ?? "";
567
601
  if (!existingRef) urlIn.value = linkEditing?.getAttribute("href") ?? "";
602
+ linkNewTab = linkEditing?.getAttribute("target") === "_blank";
603
+ linkNewTabTouched = true;
568
604
  q("remove").appendChild(buildRemoveLink());
569
605
  }
570
606
  d.querySelectorAll("[data-tab]").forEach((btn) => {
@@ -584,6 +620,16 @@ function renderLinkDialog() {
584
620
  urlIn.oninput = syncLinkDialog;
585
621
  q("cancel").onclick = hideLinkDialog;
586
622
  q("submit").onclick = applyLink;
623
+ q("newtab").onclick = () => {
624
+ linkNewTab = !linkNewTab;
625
+ linkNewTabTouched = true;
626
+ syncLinkDialog();
627
+ };
628
+ q("add-scheme").onclick = () => {
629
+ urlIn.value = withHttps(urlIn.value);
630
+ urlIn.focus();
631
+ syncLinkDialog();
632
+ };
587
633
  renderLinkList("");
588
634
  syncLinkDialog();
589
635
  }
@@ -689,6 +735,30 @@ function syncLinkDialog() {
689
735
  q("hint").innerHTML = own ? uiLabels.linkTextOwnHint : onPage ? uiLabels.linkTextAutoHint : uiLabels.linkUrlHint;
690
736
  q("live").style.display = onPage ? "flex" : "none";
691
737
  q("submit").disabled = onPage ? !linkPicked : !q("url").value.trim();
738
+ const raw = q("url").value;
739
+ const doubtful = !onPage && isSchemeless(raw);
740
+ q("schemeless").style.display = doubtful ? "block" : "none";
741
+ if (doubtful) {
742
+ q("schemeless-text").innerHTML = uiLabels.linkSchemeless.replace(
743
+ "{v}",
744
+ escapeHtml(raw.trim())
745
+ );
746
+ }
747
+ if (!onPage && !linkNewTabTouched) {
748
+ linkNewTab = isExternalHost(raw, typeof location !== "undefined" ? location.host : "");
749
+ }
750
+ const box = q("newtab-box");
751
+ if (box) box.setAttribute("style", checkboxCss(linkNewTab));
752
+ q("newtab").setAttribute("aria-pressed", String(linkNewTab));
753
+ }
754
+ function setLinkTarget(el, newTab) {
755
+ if (newTab) {
756
+ el.setAttribute("target", "_blank");
757
+ el.setAttribute("rel", "noopener");
758
+ } else {
759
+ el.removeAttribute("target");
760
+ el.removeAttribute("rel");
761
+ }
692
762
  }
693
763
  function applyLink() {
694
764
  const d = linkDialog;
@@ -700,18 +770,21 @@ function applyLink() {
700
770
  if (!href) return;
701
771
  const ref = onPage && linkPicked ? `${linkPicked.collection}:${linkPicked.slug}` : "";
702
772
  const label = own || (onPage ? linkPicked?.title ?? href : href);
773
+ const applyTarget = (el) => setLinkTarget(el, linkNewTab);
703
774
  if (linkEditing) {
704
775
  linkEditing.setAttribute("href", href);
705
776
  if (ref) linkEditing.setAttribute("data-cms-ref", ref);
706
777
  else linkEditing.removeAttribute("data-cms-ref");
707
778
  if (ref && !own) linkEditing.setAttribute("data-cms-ref-label", "auto");
708
779
  else linkEditing.removeAttribute("data-cms-ref-label");
780
+ applyTarget(linkEditing);
709
781
  linkEditing.textContent = label;
710
782
  hideLinkDialog();
711
783
  return;
712
784
  }
713
785
  const a = document.createElement("a");
714
786
  a.setAttribute("href", href);
787
+ applyTarget(a);
715
788
  if (ref) {
716
789
  a.setAttribute("data-cms-ref", ref);
717
790
  if (!own) a.setAttribute("data-cms-ref-label", "auto");
@@ -736,6 +809,7 @@ function escapeHtml(v) {
736
809
  return v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
737
810
  }
738
811
  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;";
812
+ 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;");
739
813
  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;";
740
814
  var labelCss = () => "font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:#9aa3b2;margin:12px 0 5px;display:block;";
741
815
  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;");
@@ -1166,6 +1240,7 @@ function injectStyles() {
1166
1240
  positionPopover,
1167
1241
  renderCurrentRefLine,
1168
1242
  rescanFields,
1169
- resolveExistingRef
1243
+ resolveExistingRef,
1244
+ setLinkTarget
1170
1245
  });
1171
1246
  //# sourceMappingURL=index.cjs.map