@cancia/toolbar 0.4.2 → 0.5.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/cancia.iife.js +133 -114
- package/dist/cancia.js +221 -7
- package/dist/cancia.js.map +1 -1
- package/package.json +2 -2
package/dist/cancia.js
CHANGED
|
@@ -28,6 +28,20 @@ function setPending(key, lang, value) {
|
|
|
28
28
|
const full = pendingKey(key, lang);
|
|
29
29
|
state.pending.set(full, { key, lang, value });
|
|
30
30
|
}
|
|
31
|
+
function parseLinkOverlay(raw) {
|
|
32
|
+
if (!raw) return { label: "", href: "" };
|
|
33
|
+
const trimmed = raw.trim();
|
|
34
|
+
if (trimmed.startsWith("{")) {
|
|
35
|
+
try {
|
|
36
|
+
const p = JSON.parse(trimmed);
|
|
37
|
+
if (p && typeof p === "object") {
|
|
38
|
+
return { label: String(p.label ?? ""), href: String(p.href ?? "") };
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { label: raw, href: "" };
|
|
44
|
+
}
|
|
31
45
|
function applyOverlay() {
|
|
32
46
|
document.querySelectorAll("[data-cms]").forEach((el) => {
|
|
33
47
|
if (el.dataset.cmsList !== void 0) return;
|
|
@@ -39,6 +53,14 @@ function applyOverlay() {
|
|
|
39
53
|
el.src = savedValue;
|
|
40
54
|
return;
|
|
41
55
|
}
|
|
56
|
+
if (el.dataset.cmsType === "link" || el.tagName === "A") {
|
|
57
|
+
const link = parseLinkOverlay(savedValue);
|
|
58
|
+
if (link.href && el.tagName === "A") el.setAttribute("href", link.href);
|
|
59
|
+
const labelEl = el.querySelector("[data-cms-label]");
|
|
60
|
+
if (labelEl) labelEl.textContent = link.label;
|
|
61
|
+
else if (el.childElementCount === 0) el.textContent = link.label;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
42
64
|
if (el.childElementCount > 0) {
|
|
43
65
|
console.warn(
|
|
44
66
|
`Cancia: skipping overlay for "${key}" \u2014 element has child elements (textContent would destroy them).`
|
|
@@ -234,7 +256,10 @@ function accent() {
|
|
|
234
256
|
}
|
|
235
257
|
function fieldType(el) {
|
|
236
258
|
if (el.dataset.cmsType === "image") return "image";
|
|
237
|
-
|
|
259
|
+
if (el.dataset.cmsType === "link") return "link";
|
|
260
|
+
if (el.tagName === "IMG") return "image";
|
|
261
|
+
if (el.tagName === "A") return "link";
|
|
262
|
+
return "text";
|
|
238
263
|
}
|
|
239
264
|
function elementMode(el) {
|
|
240
265
|
return el.dataset.cmsList ? "list" : "field";
|
|
@@ -319,6 +344,10 @@ var IMAGE_ICON = `<svg width="10" height="10" viewBox="0 0 14 14" fill="none" st
|
|
|
319
344
|
<circle cx="4.5" cy="4.5" r="1.2"/>
|
|
320
345
|
<path d="M1 9.5l3.5-3 2.5 2.5 2-1.5 3 3.5"/>
|
|
321
346
|
</svg>`;
|
|
347
|
+
var LINK_ICON = `<svg width="10" height="10" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
348
|
+
<path d="M6 8a2.5 2.5 0 003.6.3l2.2-2.2a2.5 2.5 0 00-3.5-3.5L7.2 3.6"/>
|
|
349
|
+
<path d="M8 6a2.5 2.5 0 00-3.6-.3L2.2 7.9a2.5 2.5 0 003.5 3.5l1.1-1.1"/>
|
|
350
|
+
</svg>`;
|
|
322
351
|
var LIST_ICON = `<svg width="10" height="10" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
323
352
|
<rect x="1" y="2" width="3" height="3" rx="0.5"/>
|
|
324
353
|
<rect x="1" y="7.5" width="3" height="3" rx="0.5"/>
|
|
@@ -348,7 +377,7 @@ function positionOverlay(el, animate = false) {
|
|
|
348
377
|
tooltipText = `list: ${el.dataset.cmsList}`;
|
|
349
378
|
} else {
|
|
350
379
|
const type = fieldType(el);
|
|
351
|
-
badgeIcon = type === "image" ? IMAGE_ICON : FIELD_ICON;
|
|
380
|
+
badgeIcon = type === "image" ? IMAGE_ICON : type === "link" ? LINK_ICON : FIELD_ICON;
|
|
352
381
|
badgeLabel = type;
|
|
353
382
|
tooltipText = el.dataset.cms ?? "";
|
|
354
383
|
}
|
|
@@ -434,9 +463,27 @@ function handleClick(e) {
|
|
|
434
463
|
onSelect?.({ kind: "field", el: target, key, fieldType: fieldType(target) });
|
|
435
464
|
}
|
|
436
465
|
}
|
|
466
|
+
function warnUnhoverableRegions() {
|
|
467
|
+
const els = document.querySelectorAll(CMS_SELECTOR);
|
|
468
|
+
els.forEach((el) => {
|
|
469
|
+
const rect = el.getBoundingClientRect();
|
|
470
|
+
if (rect.width > 0 && rect.height > 0) return;
|
|
471
|
+
const cs = getComputedStyle(el);
|
|
472
|
+
const selfInflicted = cs.display === "contents" || cs.position === "absolute" || cs.clipPath !== "none";
|
|
473
|
+
if (!selfInflicted) return;
|
|
474
|
+
const name = el.dataset.cmsList ? `list "${el.dataset.cmsList}"` : `field "${el.dataset.cms}"`;
|
|
475
|
+
console.warn(
|
|
476
|
+
`[cancia] ${name} is annotated but has no layout box (${Math.round(rect.width)}\xD7${Math.round(
|
|
477
|
+
rect.height
|
|
478
|
+
)}) \u2014 it cannot be hovered or clicked in the editor. Avoid sr-only / display:contents on CMS regions; give the container a real box.`,
|
|
479
|
+
el
|
|
480
|
+
);
|
|
481
|
+
});
|
|
482
|
+
}
|
|
437
483
|
function attachHighlight(selectCallback) {
|
|
438
484
|
injectStyles();
|
|
439
485
|
onSelect = selectCallback;
|
|
486
|
+
warnUnhoverableRegions();
|
|
440
487
|
document.addEventListener("mouseover", handleMouseOver, true);
|
|
441
488
|
document.addEventListener("mouseout", handleMouseOut, true);
|
|
442
489
|
document.addEventListener("click", handleClick, true);
|
|
@@ -659,6 +706,175 @@ function buildTextPopup(key, anchorEl, onClose) {
|
|
|
659
706
|
wrap.appendChild(footer);
|
|
660
707
|
return wrap;
|
|
661
708
|
}
|
|
709
|
+
function isSafeHrefValue(href) {
|
|
710
|
+
const trimmed = href.trim();
|
|
711
|
+
if (trimmed === "") return true;
|
|
712
|
+
if (/^(https?:|mailto:|tel:)/i.test(trimmed)) return true;
|
|
713
|
+
const schemeMatch = /^([a-z][a-z0-9+.-]*):/i.exec(trimmed);
|
|
714
|
+
if (schemeMatch) {
|
|
715
|
+
const firstSep = trimmed.search(/[/?#]/);
|
|
716
|
+
if (firstSep === -1 || schemeMatch[1].length < firstSep) return false;
|
|
717
|
+
}
|
|
718
|
+
return true;
|
|
719
|
+
}
|
|
720
|
+
function parseLink(raw) {
|
|
721
|
+
if (!raw) return { label: "", href: "" };
|
|
722
|
+
const trimmed = raw.trim();
|
|
723
|
+
if (trimmed.startsWith("{")) {
|
|
724
|
+
try {
|
|
725
|
+
const p = JSON.parse(trimmed);
|
|
726
|
+
if (p && typeof p === "object") {
|
|
727
|
+
return { label: String(p.label ?? ""), href: String(p.href ?? "") };
|
|
728
|
+
}
|
|
729
|
+
} catch {
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return { label: raw, href: "" };
|
|
733
|
+
}
|
|
734
|
+
function buildLinkPopup(key, anchorEl, onClose) {
|
|
735
|
+
const langs = state.config?.languages ?? ["en"];
|
|
736
|
+
let activeLang = state.activeLang || langs[0];
|
|
737
|
+
const wrap = document.createElement("div");
|
|
738
|
+
wrap.dataset.canciaPopup = "1";
|
|
739
|
+
applyPopupStyles(wrap);
|
|
740
|
+
wrap.appendChild(buildHeader(key, onClose));
|
|
741
|
+
const labelNode = anchorEl.querySelector("[data-cms-label]") ?? anchorEl;
|
|
742
|
+
const domLabel = labelNode.textContent?.trim() ?? "";
|
|
743
|
+
const domHref = anchorEl.getAttribute("href") ?? "";
|
|
744
|
+
const readCurrent = (lang) => {
|
|
745
|
+
const stored = parseLink(getValue(key, lang));
|
|
746
|
+
return {
|
|
747
|
+
label: stored.label || domLabel,
|
|
748
|
+
// The href is shared across languages (see the note below), so fall back
|
|
749
|
+
// to the default language's stored value before the DOM.
|
|
750
|
+
href: stored.href || parseLink(getValue(key, langs[0])).href || domHref
|
|
751
|
+
};
|
|
752
|
+
};
|
|
753
|
+
const inputStyle = `
|
|
754
|
+
width: 100%; box-sizing: border-box;
|
|
755
|
+
background: rgba(255,255,255,0.03); color: rgba(255,255,255,0.9);
|
|
756
|
+
border: 1px solid rgba(255,255,255,0.07); border-radius: 10px;
|
|
757
|
+
padding: 9px 12px;
|
|
758
|
+
font-size: 13px; font-family: inherit; outline: none;
|
|
759
|
+
transition: border-color 0.18s, background 0.18s;
|
|
760
|
+
caret-color: ${accent2()};
|
|
761
|
+
`;
|
|
762
|
+
const makeLabelled = (text, input) => {
|
|
763
|
+
const field = document.createElement("div");
|
|
764
|
+
field.style.cssText = `display: flex; flex-direction: column; gap: 5px; margin-bottom: 10px;`;
|
|
765
|
+
const lab = document.createElement("div");
|
|
766
|
+
lab.textContent = text;
|
|
767
|
+
lab.style.cssText = `font-size:10px;letter-spacing:0.08em;text-transform:uppercase;color:rgba(255,255,255,0.35);`;
|
|
768
|
+
input.style.cssText = inputStyle;
|
|
769
|
+
input.addEventListener("focus", () => {
|
|
770
|
+
input.style.borderColor = `${accent2()}66`;
|
|
771
|
+
input.style.background = "rgba(255,255,255,0.05)";
|
|
772
|
+
});
|
|
773
|
+
input.addEventListener("blur", () => {
|
|
774
|
+
input.style.borderColor = "rgba(255,255,255,0.07)";
|
|
775
|
+
input.style.background = "rgba(255,255,255,0.03)";
|
|
776
|
+
});
|
|
777
|
+
field.appendChild(lab);
|
|
778
|
+
field.appendChild(input);
|
|
779
|
+
return field;
|
|
780
|
+
};
|
|
781
|
+
const labelInput = document.createElement("input");
|
|
782
|
+
labelInput.type = "text";
|
|
783
|
+
labelInput.placeholder = "Book a call";
|
|
784
|
+
const hrefInput = document.createElement("input");
|
|
785
|
+
hrefInput.type = "text";
|
|
786
|
+
hrefInput.inputMode = "url";
|
|
787
|
+
hrefInput.placeholder = "/start or https://\u2026";
|
|
788
|
+
const current = readCurrent(activeLang);
|
|
789
|
+
labelInput.value = current.label;
|
|
790
|
+
hrefInput.value = current.href;
|
|
791
|
+
if (langs.length > 1) {
|
|
792
|
+
const tabs = document.createElement("div");
|
|
793
|
+
tabs.style.cssText = `display: flex; gap: 0; margin-bottom: 12px; border-bottom: 1px solid rgba(255,255,255,0.06);`;
|
|
794
|
+
const renderTabs2 = () => {
|
|
795
|
+
tabs.innerHTML = "";
|
|
796
|
+
langs.forEach((lang) => {
|
|
797
|
+
const tab = document.createElement("button");
|
|
798
|
+
tab.textContent = lang.toUpperCase();
|
|
799
|
+
const isActive = lang === activeLang;
|
|
800
|
+
tab.style.cssText = `
|
|
801
|
+
padding: 5px 10px 6px; border: none; border-bottom: 2px solid;
|
|
802
|
+
margin-bottom: -1px;
|
|
803
|
+
font-size: 11px; font-weight: 600; cursor: pointer; letter-spacing: 0.06em;
|
|
804
|
+
background: transparent;
|
|
805
|
+
border-bottom-color: ${isActive ? accent2() : "transparent"};
|
|
806
|
+
color: ${isActive ? "#fff" : "rgba(255,255,255,0.3)"};
|
|
807
|
+
transition: color 0.15s, border-color 0.15s;
|
|
808
|
+
`;
|
|
809
|
+
tab.addEventListener("click", () => {
|
|
810
|
+
stage(activeLang);
|
|
811
|
+
activeLang = lang;
|
|
812
|
+
state.activeLang = lang;
|
|
813
|
+
applyOverlay();
|
|
814
|
+
labelInput.value = readCurrent(lang).label;
|
|
815
|
+
renderTabs2();
|
|
816
|
+
});
|
|
817
|
+
tabs.appendChild(tab);
|
|
818
|
+
});
|
|
819
|
+
};
|
|
820
|
+
renderTabs2();
|
|
821
|
+
wrap.appendChild(tabs);
|
|
822
|
+
}
|
|
823
|
+
wrap.appendChild(makeLabelled("Label", labelInput));
|
|
824
|
+
wrap.appendChild(makeLabelled("URL", hrefInput));
|
|
825
|
+
const hint = document.createElement("div");
|
|
826
|
+
hint.style.cssText = `font-size:11px;color:rgba(255,255,255,0.28);margin:-4px 0 10px;line-height:1.45;`;
|
|
827
|
+
hint.textContent = langs.length > 1 ? "Relative (/start), #anchor, mailto: and tel: all work. The URL is shared across languages." : "Relative (/start), #anchor, mailto: and tel: all work.";
|
|
828
|
+
wrap.appendChild(hint);
|
|
829
|
+
const warn = document.createElement("div");
|
|
830
|
+
warn.style.cssText = `font-size:11px;color:#f0a; margin:-4px 0 10px; display:none;`;
|
|
831
|
+
wrap.appendChild(warn);
|
|
832
|
+
const paint = () => {
|
|
833
|
+
labelNode.textContent = labelInput.value;
|
|
834
|
+
if (isSafeHrefValue(hrefInput.value)) anchorEl.setAttribute("href", hrefInput.value);
|
|
835
|
+
};
|
|
836
|
+
const stage = (lang) => {
|
|
837
|
+
const value = {
|
|
838
|
+
label: labelInput.value,
|
|
839
|
+
href: hrefInput.value.trim()
|
|
840
|
+
};
|
|
841
|
+
const existing = parseLink(getValue(key, lang));
|
|
842
|
+
if (existing.label !== value.label || existing.href !== value.href) {
|
|
843
|
+
setPending(key, lang, JSON.stringify(value));
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
const validate = () => {
|
|
847
|
+
const ok = isSafeHrefValue(hrefInput.value);
|
|
848
|
+
warn.style.display = ok ? "none" : "block";
|
|
849
|
+
warn.textContent = ok ? "" : "That URL scheme isn\u2019t allowed and won\u2019t be saved.";
|
|
850
|
+
return ok;
|
|
851
|
+
};
|
|
852
|
+
labelInput.addEventListener("input", () => {
|
|
853
|
+
paint();
|
|
854
|
+
onPendingChange();
|
|
855
|
+
});
|
|
856
|
+
hrefInput.addEventListener("input", () => {
|
|
857
|
+
validate();
|
|
858
|
+
paint();
|
|
859
|
+
onPendingChange();
|
|
860
|
+
});
|
|
861
|
+
setTimeout(() => labelInput.focus(), 80);
|
|
862
|
+
const footer = document.createElement("div");
|
|
863
|
+
footer.dataset.canciaFooter = "1";
|
|
864
|
+
footer.style.cssText = `display: flex; justify-content: flex-end; margin-top: 10px;`;
|
|
865
|
+
const saveBtn = makePrimaryButton("Save", accent2());
|
|
866
|
+
saveBtn.dataset.canciaSave = "1";
|
|
867
|
+
saveBtn.title = "Save (\u2318S)";
|
|
868
|
+
saveBtn.addEventListener("click", () => {
|
|
869
|
+
if (!validate()) return;
|
|
870
|
+
stage(activeLang);
|
|
871
|
+
onPendingChange();
|
|
872
|
+
onClose();
|
|
873
|
+
});
|
|
874
|
+
footer.appendChild(saveBtn);
|
|
875
|
+
wrap.appendChild(footer);
|
|
876
|
+
return wrap;
|
|
877
|
+
}
|
|
662
878
|
function buildImagePopup(key, anchorEl, onClose) {
|
|
663
879
|
const wrap = document.createElement("div");
|
|
664
880
|
wrap.dataset.canciaPopup = "1";
|
|
@@ -880,13 +1096,11 @@ function applyPopupStyles(el) {
|
|
|
880
1096
|
}
|
|
881
1097
|
function openPopup(key, fieldType2, anchorEl, onClose) {
|
|
882
1098
|
closePopup();
|
|
883
|
-
const
|
|
884
|
-
onClose();
|
|
885
|
-
closePopup();
|
|
886
|
-
}) : buildTextPopup(key, anchorEl, () => {
|
|
1099
|
+
const done = () => {
|
|
887
1100
|
onClose();
|
|
888
1101
|
closePopup();
|
|
889
|
-
}
|
|
1102
|
+
};
|
|
1103
|
+
const popup = fieldType2 === "image" ? buildImagePopup(key, anchorEl, done) : fieldType2 === "link" ? buildLinkPopup(key, anchorEl, done) : buildTextPopup(key, anchorEl, done);
|
|
890
1104
|
document.body.appendChild(popup);
|
|
891
1105
|
popupEl = popup;
|
|
892
1106
|
const { top, left, origin } = getPopupPosition(anchorEl);
|