@cancia/toolbar 0.12.1 → 0.14.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 +183 -165
- package/dist/cancia.js +267 -25
- package/dist/cancia.js.map +1 -1
- package/package.json +2 -2
package/dist/cancia.js
CHANGED
|
@@ -171,6 +171,10 @@ function getValue(key, lang) {
|
|
|
171
171
|
if (state.cmsData[full] !== void 0) return state.cmsData[full];
|
|
172
172
|
return "";
|
|
173
173
|
}
|
|
174
|
+
function hasValue(key, lang) {
|
|
175
|
+
const full = `${key}.${lang}`;
|
|
176
|
+
return state.pending.has(full) || state.cmsData[full] !== void 0;
|
|
177
|
+
}
|
|
174
178
|
function setPending(key, lang, value) {
|
|
175
179
|
const full = pendingKey(key, lang);
|
|
176
180
|
state.pending.set(full, { key, lang, value });
|
|
@@ -210,7 +214,12 @@ function applyOverlay() {
|
|
|
210
214
|
}
|
|
211
215
|
function revertPending() {
|
|
212
216
|
for (const [fullKey, { key }] of state.pending) {
|
|
213
|
-
|
|
217
|
+
if (state.cmsData[fullKey] === void 0) {
|
|
218
|
+
state.pending.clear();
|
|
219
|
+
location.reload();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const savedValue = state.cmsData[fullKey];
|
|
214
223
|
document.querySelectorAll(`[data-cms="${key}"]`).forEach((el) => {
|
|
215
224
|
if (el.tagName === "IMG") {
|
|
216
225
|
el.src = savedValue;
|
|
@@ -258,6 +267,22 @@ async function saveEntry(key, lang, value) {
|
|
|
258
267
|
});
|
|
259
268
|
if (!res.ok) throw new Error(`Cancia: failed to save (${res.status})`);
|
|
260
269
|
}
|
|
270
|
+
async function deleteContent(key, lang) {
|
|
271
|
+
const { apiUrl, site } = state.config;
|
|
272
|
+
const res = await fetch(`${apiUrl}/api/cancia/content`, {
|
|
273
|
+
method: "DELETE",
|
|
274
|
+
headers: headers(),
|
|
275
|
+
body: JSON.stringify({ site, key, lang, route: currentRoute() })
|
|
276
|
+
});
|
|
277
|
+
if (!res.ok) throw await toApiError(res, "Could not reset this text. Check your connection and try again.");
|
|
278
|
+
}
|
|
279
|
+
async function toApiError(res, fallback) {
|
|
280
|
+
const body = await res.json().catch(() => ({}));
|
|
281
|
+
const err = new Error(body.error ?? fallback);
|
|
282
|
+
if (body.code) err.code = body.code;
|
|
283
|
+
if (body.issues?.length) err.issues = body.issues;
|
|
284
|
+
return err;
|
|
285
|
+
}
|
|
261
286
|
function isAuthError(err) {
|
|
262
287
|
return err instanceof Error && err.message.includes("(401)");
|
|
263
288
|
}
|
|
@@ -334,8 +359,7 @@ async function createListEntry(listName, data, locale, id) {
|
|
|
334
359
|
{ method: "POST", headers: headers(), body: JSON.stringify({ data, id }) }
|
|
335
360
|
);
|
|
336
361
|
if (!res.ok) {
|
|
337
|
-
|
|
338
|
-
throw new Error(err.error ?? `Cancia: failed to create entry (${res.status})`);
|
|
362
|
+
throw await toApiError(res, "Could not create the entry. Check your connection and try again.");
|
|
339
363
|
}
|
|
340
364
|
const body = await res.json();
|
|
341
365
|
return body.entry;
|
|
@@ -347,9 +371,7 @@ async function updateListEntry(listName, id, data, rev, locale) {
|
|
|
347
371
|
{ method: "PATCH", headers: headers(), body: JSON.stringify({ data, _rev: rev }) }
|
|
348
372
|
);
|
|
349
373
|
if (!res.ok) {
|
|
350
|
-
|
|
351
|
-
const message = err.error ?? `Cancia: failed to update entry (${res.status})`;
|
|
352
|
-
throw Object.assign(new Error(message), { code: err.code });
|
|
374
|
+
throw await toApiError(res, "Could not save your changes. Check your connection and try again.");
|
|
353
375
|
}
|
|
354
376
|
const body = await res.json();
|
|
355
377
|
return body.entry;
|
|
@@ -366,9 +388,9 @@ async function reorderList(listName, ids) {
|
|
|
366
388
|
const { apiUrl, site } = state.config;
|
|
367
389
|
const res = await fetch(
|
|
368
390
|
`${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}/reorder?site=${encodeURIComponent(site)}${routeParam()}`,
|
|
369
|
-
{ method: "POST", headers: headers(), body: JSON.stringify({ ids }) }
|
|
391
|
+
{ method: "POST", headers: headers(), body: JSON.stringify({ ids, knownCount: ids.length }) }
|
|
370
392
|
);
|
|
371
|
-
if (!res.ok) throw
|
|
393
|
+
if (!res.ok) throw await toApiError(res, "Could not save the new order. Check your connection and try again.");
|
|
372
394
|
}
|
|
373
395
|
async function flushEdits(edits, save) {
|
|
374
396
|
const results = await Promise.allSettled(edits.map((edit) => save(edit)));
|
|
@@ -1165,6 +1187,20 @@ function buildHeader(key, onClose) {
|
|
|
1165
1187
|
header.appendChild(makeCloseButton(onClose));
|
|
1166
1188
|
return header;
|
|
1167
1189
|
}
|
|
1190
|
+
function seedValue(key, lang, anchorEl) {
|
|
1191
|
+
if (hasValue(key, lang)) return getValue(key, lang);
|
|
1192
|
+
return anchorEl.textContent?.trim() ?? "";
|
|
1193
|
+
}
|
|
1194
|
+
function activeImageLang() {
|
|
1195
|
+
return state.activeLang || state.config?.languages?.[0] || "en";
|
|
1196
|
+
}
|
|
1197
|
+
async function resetToAuthored(key, lang) {
|
|
1198
|
+
await deleteContent(key, lang);
|
|
1199
|
+
delete state.cmsData[`${key}.${lang}`];
|
|
1200
|
+
state.pending.delete(`${key}.${lang}`);
|
|
1201
|
+
onPendingChange();
|
|
1202
|
+
location.reload();
|
|
1203
|
+
}
|
|
1168
1204
|
function buildTextPopup(key, anchorEl, onClose) {
|
|
1169
1205
|
const langs = state.config?.languages ?? ["en"];
|
|
1170
1206
|
let activeLang = state.activeLang || langs[0];
|
|
@@ -1199,9 +1235,7 @@ function buildTextPopup(key, anchorEl, onClose) {
|
|
|
1199
1235
|
tab.addEventListener("click", () => {
|
|
1200
1236
|
const current = wrap.querySelector("textarea");
|
|
1201
1237
|
if (current) {
|
|
1202
|
-
|
|
1203
|
-
const fallback = anchorEl.textContent?.trim() || "";
|
|
1204
|
-
if (current.value !== (existing || fallback)) {
|
|
1238
|
+
if (current.value !== seedValue(key, activeLang, anchorEl)) {
|
|
1205
1239
|
setPending(key, activeLang, current.value);
|
|
1206
1240
|
}
|
|
1207
1241
|
}
|
|
@@ -1231,7 +1265,7 @@ function buildTextPopup(key, anchorEl, onClose) {
|
|
|
1231
1265
|
};
|
|
1232
1266
|
const renderTextarea = (isInit = false) => {
|
|
1233
1267
|
if (!isInit && textarea) {
|
|
1234
|
-
textarea.value =
|
|
1268
|
+
textarea.value = seedValue(key, activeLang, anchorEl);
|
|
1235
1269
|
textarea.style.borderColor = v("accent-ring");
|
|
1236
1270
|
textarea.style.background = v("surface-hover");
|
|
1237
1271
|
attachInputHandler();
|
|
@@ -1239,7 +1273,7 @@ function buildTextPopup(key, anchorEl, onClose) {
|
|
|
1239
1273
|
}
|
|
1240
1274
|
const footerEl = wrap.querySelector("[data-cancia-footer]");
|
|
1241
1275
|
textarea = document.createElement("textarea");
|
|
1242
|
-
textarea.value =
|
|
1276
|
+
textarea.value = seedValue(key, activeLang, anchorEl);
|
|
1243
1277
|
textarea.rows = 4;
|
|
1244
1278
|
textarea.placeholder = "Enter text\u2026";
|
|
1245
1279
|
textarea.style.cssText = `
|
|
@@ -1262,18 +1296,37 @@ function buildTextPopup(key, anchorEl, onClose) {
|
|
|
1262
1296
|
const footer = document.createElement("div");
|
|
1263
1297
|
footer.dataset.canciaFooter = "1";
|
|
1264
1298
|
footer.style.cssText = `display: flex; justify-content: flex-end; margin-top: 10px;`;
|
|
1265
|
-
const saveBtn = makePrimaryButton("
|
|
1299
|
+
const saveBtn = makePrimaryButton("Done", accent2());
|
|
1266
1300
|
saveBtn.dataset.canciaSave = "1";
|
|
1267
|
-
saveBtn.title = "
|
|
1301
|
+
saveBtn.title = "Apply to the page (\u2318S) \u2014 then Save in the toolbar to publish";
|
|
1268
1302
|
saveBtn.addEventListener("click", () => {
|
|
1269
|
-
|
|
1270
|
-
const fallback = anchorEl.textContent?.trim() || "";
|
|
1271
|
-
if (textarea.value !== (existing || fallback)) {
|
|
1303
|
+
if (textarea.value !== seedValue(key, activeLang, anchorEl)) {
|
|
1272
1304
|
setPending(key, activeLang, textarea.value);
|
|
1273
1305
|
}
|
|
1274
1306
|
onPendingChange();
|
|
1275
1307
|
onClose();
|
|
1276
1308
|
});
|
|
1309
|
+
if (hasValue(key, activeLang)) {
|
|
1310
|
+
const resetBtn = document.createElement("button");
|
|
1311
|
+
resetBtn.type = "button";
|
|
1312
|
+
resetBtn.textContent = "Reset";
|
|
1313
|
+
resetBtn.title = "Restore the text this page was built with";
|
|
1314
|
+
resetBtn.style.cssText = `${button("ghost")} font-size: ${v("text-xs")}; margin-right: auto;`;
|
|
1315
|
+
attachHover(resetBtn, { bg: v("surface-active") });
|
|
1316
|
+
attachPress(resetBtn);
|
|
1317
|
+
resetBtn.addEventListener("click", async () => {
|
|
1318
|
+
resetBtn.disabled = true;
|
|
1319
|
+
resetBtn.textContent = "Resetting\u2026";
|
|
1320
|
+
try {
|
|
1321
|
+
await resetToAuthored(key, activeLang);
|
|
1322
|
+
} catch (err) {
|
|
1323
|
+
resetBtn.disabled = false;
|
|
1324
|
+
resetBtn.textContent = "Reset";
|
|
1325
|
+
textarea.placeholder = err instanceof Error ? err.message : "Could not reset.";
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
footer.appendChild(resetBtn);
|
|
1329
|
+
}
|
|
1277
1330
|
footer.appendChild(saveBtn);
|
|
1278
1331
|
wrap.appendChild(footer);
|
|
1279
1332
|
return wrap;
|
|
@@ -1408,9 +1461,9 @@ function buildLinkPopup(key, anchorEl, onClose) {
|
|
|
1408
1461
|
const footer = document.createElement("div");
|
|
1409
1462
|
footer.dataset.canciaFooter = "1";
|
|
1410
1463
|
footer.style.cssText = `display: flex; justify-content: flex-end; margin-top: 10px;`;
|
|
1411
|
-
const saveBtn = makePrimaryButton("
|
|
1464
|
+
const saveBtn = makePrimaryButton("Done", accent2());
|
|
1412
1465
|
saveBtn.dataset.canciaSave = "1";
|
|
1413
|
-
saveBtn.title = "
|
|
1466
|
+
saveBtn.title = "Apply to the page (\u2318S) \u2014 then Save in the toolbar to publish";
|
|
1414
1467
|
saveBtn.addEventListener("click", () => {
|
|
1415
1468
|
if (!validate()) return;
|
|
1416
1469
|
stage(activeLang);
|
|
@@ -1448,6 +1501,33 @@ function buildImagePopup(key, anchorEl, onClose) {
|
|
|
1448
1501
|
previewWrap.appendChild(previewImg);
|
|
1449
1502
|
previewWrap.appendChild(previewLabel);
|
|
1450
1503
|
wrap.appendChild(previewWrap);
|
|
1504
|
+
if (hasValue(key, activeImageLang())) {
|
|
1505
|
+
const resetRow = document.createElement("div");
|
|
1506
|
+
resetRow.style.cssText = `display:flex; justify-content:flex-end; margin: -4px 0 10px;`;
|
|
1507
|
+
const resetBtn = document.createElement("button");
|
|
1508
|
+
resetBtn.type = "button";
|
|
1509
|
+
resetBtn.textContent = "Reset to original";
|
|
1510
|
+
resetBtn.style.cssText = `
|
|
1511
|
+
${button("ghost")}
|
|
1512
|
+
font-size: ${v("text-xs")};
|
|
1513
|
+
`;
|
|
1514
|
+
attachHover(resetBtn, { bg: v("surface-active") });
|
|
1515
|
+
attachPress(resetBtn);
|
|
1516
|
+
resetBtn.addEventListener("click", async () => {
|
|
1517
|
+
resetBtn.disabled = true;
|
|
1518
|
+
resetBtn.textContent = "Resetting\u2026";
|
|
1519
|
+
try {
|
|
1520
|
+
await resetToAuthored(key, activeImageLang());
|
|
1521
|
+
onClose();
|
|
1522
|
+
} catch (err) {
|
|
1523
|
+
resetBtn.disabled = false;
|
|
1524
|
+
resetBtn.textContent = "Reset to original";
|
|
1525
|
+
previewLabel.textContent = err instanceof Error ? err.message : "Could not reset. Try again.";
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
resetRow.appendChild(resetBtn);
|
|
1529
|
+
wrap.appendChild(resetRow);
|
|
1530
|
+
}
|
|
1451
1531
|
}
|
|
1452
1532
|
const dropZone = document.createElement("label");
|
|
1453
1533
|
dropZone.style.cssText = `
|
|
@@ -1803,9 +1883,9 @@ function buildRichPopup(key, anchorEl, onClose) {
|
|
|
1803
1883
|
rowsWrap.appendChild(r.el);
|
|
1804
1884
|
r.el.querySelector("textarea")?.focus();
|
|
1805
1885
|
});
|
|
1806
|
-
const saveBtn = makePrimaryButton("
|
|
1886
|
+
const saveBtn = makePrimaryButton("Done", accent2());
|
|
1807
1887
|
saveBtn.dataset.canciaSave = "1";
|
|
1808
|
-
saveBtn.title = "
|
|
1888
|
+
saveBtn.title = "Apply to the page (\u2318S) \u2014 then Save in the toolbar to publish";
|
|
1809
1889
|
saveBtn.addEventListener("click", () => {
|
|
1810
1890
|
commit();
|
|
1811
1891
|
onClose();
|
|
@@ -2430,7 +2510,7 @@ import {
|
|
|
2430
2510
|
portableTextSubsetSchema as portableTextSubsetSchema2,
|
|
2431
2511
|
PT_STYLES as PT_STYLES2
|
|
2432
2512
|
} from "@cancia/astro/richtext";
|
|
2433
|
-
import { slugify } from "@cancia/astro/schema";
|
|
2513
|
+
import { slugify, parseLinkValue as parseLinkValue3, isSafeHref as isSafeHref2 } from "@cancia/astro/schema";
|
|
2434
2514
|
var MODAL_Z = v("z-bar");
|
|
2435
2515
|
var BACKDROP_Z2 = v("z-panel");
|
|
2436
2516
|
var modalEl = null;
|
|
@@ -2660,6 +2740,99 @@ function renderField(field, initial, depth = 0) {
|
|
|
2660
2740
|
getValue2 = () => sel.value === "" ? void 0 : sel.value;
|
|
2661
2741
|
break;
|
|
2662
2742
|
}
|
|
2743
|
+
case "file": {
|
|
2744
|
+
let currentUrl = typeof initial === "string" ? initial : "";
|
|
2745
|
+
const box = document.createElement("div");
|
|
2746
|
+
box.style.cssText = `
|
|
2747
|
+
display: flex; flex-direction: column; gap: 8px;
|
|
2748
|
+
background: ${v("surface-raised")};
|
|
2749
|
+
border: 1px dashed ${v("border-strong")};
|
|
2750
|
+
border-radius: ${v("radius")};
|
|
2751
|
+
padding: 10px;
|
|
2752
|
+
`;
|
|
2753
|
+
const nameRow = document.createElement("div");
|
|
2754
|
+
nameRow.style.cssText = `display:flex; align-items:center; gap:8px; min-width:0;`;
|
|
2755
|
+
const nameLink = document.createElement("a");
|
|
2756
|
+
nameLink.target = "_blank";
|
|
2757
|
+
nameLink.rel = "noopener";
|
|
2758
|
+
nameLink.style.cssText = `
|
|
2759
|
+
font-size: ${v("text-sm")}; color: ${v("fg")}; text-decoration: underline;
|
|
2760
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
|
|
2761
|
+
`;
|
|
2762
|
+
const renderName = (url) => {
|
|
2763
|
+
if (!url) {
|
|
2764
|
+
nameLink.removeAttribute("href");
|
|
2765
|
+
nameLink.textContent = "No file attached";
|
|
2766
|
+
nameLink.style.color = v("fg-faint");
|
|
2767
|
+
nameLink.style.textDecoration = "none";
|
|
2768
|
+
return;
|
|
2769
|
+
}
|
|
2770
|
+
let label2 = url;
|
|
2771
|
+
try {
|
|
2772
|
+
label2 = decodeURIComponent(new URL(url, location.href).pathname.split("/").pop() || url);
|
|
2773
|
+
} catch {
|
|
2774
|
+
}
|
|
2775
|
+
nameLink.href = url;
|
|
2776
|
+
nameLink.textContent = label2;
|
|
2777
|
+
nameLink.style.color = v("fg");
|
|
2778
|
+
nameLink.style.textDecoration = "underline";
|
|
2779
|
+
};
|
|
2780
|
+
renderName(currentUrl);
|
|
2781
|
+
nameRow.appendChild(nameLink);
|
|
2782
|
+
box.appendChild(nameRow);
|
|
2783
|
+
const btnRow = document.createElement("div");
|
|
2784
|
+
btnRow.style.cssText = `display:flex; gap:8px; align-items:center;`;
|
|
2785
|
+
const fileInput = document.createElement("input");
|
|
2786
|
+
fileInput.type = "file";
|
|
2787
|
+
fileInput.accept = "application/pdf";
|
|
2788
|
+
fileInput.style.display = "none";
|
|
2789
|
+
const uploadBtn = document.createElement("button");
|
|
2790
|
+
uploadBtn.type = "button";
|
|
2791
|
+
uploadBtn.textContent = "Upload\u2026";
|
|
2792
|
+
uploadBtn.style.cssText = `${button("ghost")} font-size: ${v("text-xs")};`;
|
|
2793
|
+
attachHover(uploadBtn, { bg: v("surface-active") });
|
|
2794
|
+
uploadBtn.addEventListener("click", () => fileInput.click());
|
|
2795
|
+
const clearBtn = document.createElement("button");
|
|
2796
|
+
clearBtn.type = "button";
|
|
2797
|
+
clearBtn.textContent = "Clear";
|
|
2798
|
+
clearBtn.style.cssText = `${button("ghost")} font-size: ${v("text-xs")};`;
|
|
2799
|
+
attachHover(clearBtn, { bg: v("surface-active") });
|
|
2800
|
+
clearBtn.addEventListener("click", () => {
|
|
2801
|
+
currentUrl = "";
|
|
2802
|
+
renderName("");
|
|
2803
|
+
progressEl.textContent = "";
|
|
2804
|
+
});
|
|
2805
|
+
const progressEl = document.createElement("span");
|
|
2806
|
+
progressEl.style.cssText = `font-size: ${v("text-xs")}; color: ${v("fg-muted")};`;
|
|
2807
|
+
fileInput.addEventListener("change", async () => {
|
|
2808
|
+
const f = fileInput.files?.[0];
|
|
2809
|
+
if (!f) return;
|
|
2810
|
+
uploadBtn.disabled = true;
|
|
2811
|
+
try {
|
|
2812
|
+
const url = await uploadImage(f, (pct) => {
|
|
2813
|
+
progressEl.textContent = `Uploading\u2026 ${pct}%`;
|
|
2814
|
+
});
|
|
2815
|
+
currentUrl = url;
|
|
2816
|
+
renderName(url);
|
|
2817
|
+
progressEl.textContent = "Uploaded";
|
|
2818
|
+
} catch (err) {
|
|
2819
|
+
const msg = err instanceof Error && err.message.includes("(413)") ? "That file is too large." : "Upload failed \u2014 try again.";
|
|
2820
|
+
progressEl.textContent = msg;
|
|
2821
|
+
progressEl.style.color = v("danger");
|
|
2822
|
+
} finally {
|
|
2823
|
+
uploadBtn.disabled = false;
|
|
2824
|
+
fileInput.value = "";
|
|
2825
|
+
}
|
|
2826
|
+
});
|
|
2827
|
+
btnRow.appendChild(uploadBtn);
|
|
2828
|
+
btnRow.appendChild(clearBtn);
|
|
2829
|
+
btnRow.appendChild(progressEl);
|
|
2830
|
+
box.appendChild(btnRow);
|
|
2831
|
+
box.appendChild(fileInput);
|
|
2832
|
+
wrapper.appendChild(box);
|
|
2833
|
+
getValue2 = () => currentUrl === "" ? void 0 : currentUrl;
|
|
2834
|
+
break;
|
|
2835
|
+
}
|
|
2663
2836
|
case "image": {
|
|
2664
2837
|
const initialUrl = typeof initial === "string" ? initial : "";
|
|
2665
2838
|
let currentUrl = initialUrl;
|
|
@@ -2810,6 +2983,57 @@ function renderField(field, initial, depth = 0) {
|
|
|
2810
2983
|
};
|
|
2811
2984
|
break;
|
|
2812
2985
|
}
|
|
2986
|
+
case "link": {
|
|
2987
|
+
const parsed = parseLinkValue3(initial);
|
|
2988
|
+
const box = document.createElement("div");
|
|
2989
|
+
box.style.cssText = `
|
|
2990
|
+
display: flex; flex-direction: column; gap: 8px;
|
|
2991
|
+
background: ${v("surface-raised")};
|
|
2992
|
+
border: 1px solid ${v("border")};
|
|
2993
|
+
border-radius: ${v("radius")};
|
|
2994
|
+
padding: 10px;
|
|
2995
|
+
`;
|
|
2996
|
+
const mkRow = (labelText2, value, placeholder) => {
|
|
2997
|
+
const row = document.createElement("label");
|
|
2998
|
+
row.style.cssText = `display: flex; flex-direction: column; gap: 4px;`;
|
|
2999
|
+
const cap = document.createElement("span");
|
|
3000
|
+
cap.textContent = labelText2;
|
|
3001
|
+
cap.style.cssText = `font-size: 11px; font-weight: 600; color: ${v("fg-muted")}; letter-spacing: 0.02em;`;
|
|
3002
|
+
const inp = document.createElement("input");
|
|
3003
|
+
inp.type = "text";
|
|
3004
|
+
inp.className = "cancia-form-input";
|
|
3005
|
+
inp.style.cssText = `${INPUT_BASE} caret-color: ${v("accent")};`;
|
|
3006
|
+
inp.value = value;
|
|
3007
|
+
inp.placeholder = placeholder;
|
|
3008
|
+
row.appendChild(cap);
|
|
3009
|
+
row.appendChild(inp);
|
|
3010
|
+
box.appendChild(row);
|
|
3011
|
+
return inp;
|
|
3012
|
+
};
|
|
3013
|
+
const labelInput = mkRow("Label", parsed.label, "Book a call");
|
|
3014
|
+
const hrefInput = mkRow("URL", parsed.href, "/start or https://\u2026");
|
|
3015
|
+
wrapper.appendChild(box);
|
|
3016
|
+
getValue2 = () => {
|
|
3017
|
+
const label2 = labelInput.value.trim();
|
|
3018
|
+
const href = hrefInput.value.trim();
|
|
3019
|
+
if (!label2 && !href) return void 0;
|
|
3020
|
+
return { label: label2, href };
|
|
3021
|
+
};
|
|
3022
|
+
validate = () => {
|
|
3023
|
+
const value = getValue2();
|
|
3024
|
+
setError(null);
|
|
3025
|
+
if (field.required && !value) {
|
|
3026
|
+
setError(`${field.label || "This field"} is required`);
|
|
3027
|
+
return { value, ok: false };
|
|
3028
|
+
}
|
|
3029
|
+
if (value && value.href && !isSafeHref2(value.href)) {
|
|
3030
|
+
setError("That link type isn't allowed. Use https://, /a-path, #anchor, mailto: or tel:");
|
|
3031
|
+
return { value, ok: false };
|
|
3032
|
+
}
|
|
3033
|
+
return { value, ok: true };
|
|
3034
|
+
};
|
|
3035
|
+
break;
|
|
3036
|
+
}
|
|
2813
3037
|
default: {
|
|
2814
3038
|
const input2 = document.createElement("input");
|
|
2815
3039
|
input2.type = field.widget === "url" ? "url" : field.widget === "email" ? "email" : "text";
|
|
@@ -3319,6 +3543,19 @@ function preValidate(fieldStates) {
|
|
|
3319
3543
|
}
|
|
3320
3544
|
return { data, ok };
|
|
3321
3545
|
}
|
|
3546
|
+
function paintServerIssues(fieldStates, issues) {
|
|
3547
|
+
if (!issues?.length) return 0;
|
|
3548
|
+
let painted = 0;
|
|
3549
|
+
for (const issue of issues) {
|
|
3550
|
+
const head = issue.path?.[0];
|
|
3551
|
+
if (head === void 0) continue;
|
|
3552
|
+
const target = fieldStates.find((fs) => fs.field.name === String(head));
|
|
3553
|
+
if (!target) continue;
|
|
3554
|
+
target.setError(issue.message);
|
|
3555
|
+
painted++;
|
|
3556
|
+
}
|
|
3557
|
+
return painted;
|
|
3558
|
+
}
|
|
3322
3559
|
function validateValue(field, value) {
|
|
3323
3560
|
if (value === void 0 || value === null || value === "") return null;
|
|
3324
3561
|
if (typeof value === "string") {
|
|
@@ -3624,10 +3861,15 @@ function openEntryModal(opts) {
|
|
|
3624
3861
|
closeEntryModal();
|
|
3625
3862
|
} catch (err) {
|
|
3626
3863
|
const error = err;
|
|
3627
|
-
|
|
3628
|
-
|
|
3864
|
+
const painted = paintServerIssues(fieldStates, error.issues);
|
|
3865
|
+
if (painted > 0) {
|
|
3866
|
+
showFormError(
|
|
3867
|
+
painted === 1 ? "One field needs fixing \u2014 see the message below it." : `${painted} fields need fixing \u2014 see the messages below them.`
|
|
3868
|
+
);
|
|
3629
3869
|
} else if (error.code === "REV_CONFLICT") {
|
|
3630
3870
|
showFormError("This entry was changed by someone else. Close and reopen to see the latest version.");
|
|
3871
|
+
} else if (error.issues?.length) {
|
|
3872
|
+
showFormError(error.issues.map((i) => `${i.path.join(".") || "Entry"}: ${i.message}`).join(" \xB7 "));
|
|
3631
3873
|
} else {
|
|
3632
3874
|
showFormError(error.message);
|
|
3633
3875
|
}
|