@cancia/toolbar 0.4.1 → 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 +234 -12
- 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).`
|
|
@@ -68,6 +90,13 @@ function headers() {
|
|
|
68
90
|
if (state.sessionToken) h["Authorization"] = `Bearer ${state.sessionToken}`;
|
|
69
91
|
return h;
|
|
70
92
|
}
|
|
93
|
+
function currentRoute() {
|
|
94
|
+
return typeof location !== "undefined" ? location.pathname : "";
|
|
95
|
+
}
|
|
96
|
+
function routeParam() {
|
|
97
|
+
const r = currentRoute();
|
|
98
|
+
return r ? `&route=${encodeURIComponent(r)}` : "";
|
|
99
|
+
}
|
|
71
100
|
async function fetchContent() {
|
|
72
101
|
const { apiUrl, site } = state.config;
|
|
73
102
|
const res = await fetch(`${apiUrl}/api/cancia/content?site=${encodeURIComponent(site)}`, {
|
|
@@ -81,7 +110,8 @@ async function saveEntry(key, lang, value) {
|
|
|
81
110
|
const res = await fetch(`${apiUrl}/api/cancia/save`, {
|
|
82
111
|
method: "POST",
|
|
83
112
|
headers: headers(),
|
|
84
|
-
|
|
113
|
+
// `route` lets the server invalidate this page's cache tag (034, invalidate mode).
|
|
114
|
+
body: JSON.stringify({ key, lang, value, site, route: currentRoute() })
|
|
85
115
|
});
|
|
86
116
|
if (!res.ok) throw new Error(`Cancia: failed to save (${res.status})`);
|
|
87
117
|
}
|
|
@@ -157,7 +187,7 @@ async function fetchTranslations(listName) {
|
|
|
157
187
|
async function createListEntry(listName, data, locale, id) {
|
|
158
188
|
const { apiUrl, site } = state.config;
|
|
159
189
|
const res = await fetch(
|
|
160
|
-
`${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}?site=${encodeURIComponent(site)}${localeParam(locale)}`,
|
|
190
|
+
`${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}?site=${encodeURIComponent(site)}${localeParam(locale)}${routeParam()}`,
|
|
161
191
|
{ method: "POST", headers: headers(), body: JSON.stringify({ data, id }) }
|
|
162
192
|
);
|
|
163
193
|
if (!res.ok) {
|
|
@@ -170,7 +200,7 @@ async function createListEntry(listName, data, locale, id) {
|
|
|
170
200
|
async function updateListEntry(listName, id, data, rev, locale) {
|
|
171
201
|
const { apiUrl, site } = state.config;
|
|
172
202
|
const res = await fetch(
|
|
173
|
-
`${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}/${encodeURIComponent(id)}?site=${encodeURIComponent(site)}${localeParam(locale)}`,
|
|
203
|
+
`${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}/${encodeURIComponent(id)}?site=${encodeURIComponent(site)}${localeParam(locale)}${routeParam()}`,
|
|
174
204
|
{ method: "PATCH", headers: headers(), body: JSON.stringify({ data, _rev: rev }) }
|
|
175
205
|
);
|
|
176
206
|
if (!res.ok) {
|
|
@@ -184,7 +214,7 @@ async function updateListEntry(listName, id, data, rev, locale) {
|
|
|
184
214
|
async function deleteListEntry(listName, id, locale) {
|
|
185
215
|
const { apiUrl, site } = state.config;
|
|
186
216
|
const res = await fetch(
|
|
187
|
-
`${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}/${encodeURIComponent(id)}?site=${encodeURIComponent(site)}${localeParam(locale)}`,
|
|
217
|
+
`${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}/${encodeURIComponent(id)}?site=${encodeURIComponent(site)}${localeParam(locale)}${routeParam()}`,
|
|
188
218
|
{ method: "DELETE", headers: headers() }
|
|
189
219
|
);
|
|
190
220
|
if (!res.ok) throw new Error(`Cancia: failed to delete entry (${res.status})`);
|
|
@@ -192,7 +222,7 @@ async function deleteListEntry(listName, id, locale) {
|
|
|
192
222
|
async function reorderList(listName, ids) {
|
|
193
223
|
const { apiUrl, site } = state.config;
|
|
194
224
|
const res = await fetch(
|
|
195
|
-
`${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}/reorder?site=${encodeURIComponent(site)}`,
|
|
225
|
+
`${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}/reorder?site=${encodeURIComponent(site)}${routeParam()}`,
|
|
196
226
|
{ method: "POST", headers: headers(), body: JSON.stringify({ ids }) }
|
|
197
227
|
);
|
|
198
228
|
if (!res.ok) throw new Error(`Cancia: failed to reorder list (${res.status})`);
|
|
@@ -226,7 +256,10 @@ function accent() {
|
|
|
226
256
|
}
|
|
227
257
|
function fieldType(el) {
|
|
228
258
|
if (el.dataset.cmsType === "image") return "image";
|
|
229
|
-
|
|
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";
|
|
230
263
|
}
|
|
231
264
|
function elementMode(el) {
|
|
232
265
|
return el.dataset.cmsList ? "list" : "field";
|
|
@@ -311,6 +344,10 @@ var IMAGE_ICON = `<svg width="10" height="10" viewBox="0 0 14 14" fill="none" st
|
|
|
311
344
|
<circle cx="4.5" cy="4.5" r="1.2"/>
|
|
312
345
|
<path d="M1 9.5l3.5-3 2.5 2.5 2-1.5 3 3.5"/>
|
|
313
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>`;
|
|
314
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">
|
|
315
352
|
<rect x="1" y="2" width="3" height="3" rx="0.5"/>
|
|
316
353
|
<rect x="1" y="7.5" width="3" height="3" rx="0.5"/>
|
|
@@ -340,7 +377,7 @@ function positionOverlay(el, animate = false) {
|
|
|
340
377
|
tooltipText = `list: ${el.dataset.cmsList}`;
|
|
341
378
|
} else {
|
|
342
379
|
const type = fieldType(el);
|
|
343
|
-
badgeIcon = type === "image" ? IMAGE_ICON : FIELD_ICON;
|
|
380
|
+
badgeIcon = type === "image" ? IMAGE_ICON : type === "link" ? LINK_ICON : FIELD_ICON;
|
|
344
381
|
badgeLabel = type;
|
|
345
382
|
tooltipText = el.dataset.cms ?? "";
|
|
346
383
|
}
|
|
@@ -426,9 +463,27 @@ function handleClick(e) {
|
|
|
426
463
|
onSelect?.({ kind: "field", el: target, key, fieldType: fieldType(target) });
|
|
427
464
|
}
|
|
428
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
|
+
}
|
|
429
483
|
function attachHighlight(selectCallback) {
|
|
430
484
|
injectStyles();
|
|
431
485
|
onSelect = selectCallback;
|
|
486
|
+
warnUnhoverableRegions();
|
|
432
487
|
document.addEventListener("mouseover", handleMouseOver, true);
|
|
433
488
|
document.addEventListener("mouseout", handleMouseOut, true);
|
|
434
489
|
document.addEventListener("click", handleClick, true);
|
|
@@ -651,6 +706,175 @@ function buildTextPopup(key, anchorEl, onClose) {
|
|
|
651
706
|
wrap.appendChild(footer);
|
|
652
707
|
return wrap;
|
|
653
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
|
+
}
|
|
654
878
|
function buildImagePopup(key, anchorEl, onClose) {
|
|
655
879
|
const wrap = document.createElement("div");
|
|
656
880
|
wrap.dataset.canciaPopup = "1";
|
|
@@ -872,13 +1096,11 @@ function applyPopupStyles(el) {
|
|
|
872
1096
|
}
|
|
873
1097
|
function openPopup(key, fieldType2, anchorEl, onClose) {
|
|
874
1098
|
closePopup();
|
|
875
|
-
const
|
|
876
|
-
onClose();
|
|
877
|
-
closePopup();
|
|
878
|
-
}) : buildTextPopup(key, anchorEl, () => {
|
|
1099
|
+
const done = () => {
|
|
879
1100
|
onClose();
|
|
880
1101
|
closePopup();
|
|
881
|
-
}
|
|
1102
|
+
};
|
|
1103
|
+
const popup = fieldType2 === "image" ? buildImagePopup(key, anchorEl, done) : fieldType2 === "link" ? buildLinkPopup(key, anchorEl, done) : buildTextPopup(key, anchorEl, done);
|
|
882
1104
|
document.body.appendChild(popup);
|
|
883
1105
|
popupEl = popup;
|
|
884
1106
|
const { top, left, origin } = getPopupPosition(anchorEl);
|