@cancia/toolbar 0.12.0 → 0.13.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 +160 -151
- package/dist/cancia.js +195 -70
- package/dist/cancia.js.map +1 -1
- package/package.json +3 -3
package/dist/cancia.js
CHANGED
|
@@ -145,6 +145,7 @@ function inlineToShorthand(el) {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
// src/state.ts
|
|
148
|
+
import { parseLinkValue } from "@cancia/astro/schema";
|
|
148
149
|
var state = {
|
|
149
150
|
config: null,
|
|
150
151
|
cmsData: {},
|
|
@@ -170,24 +171,14 @@ function getValue(key, lang) {
|
|
|
170
171
|
if (state.cmsData[full] !== void 0) return state.cmsData[full];
|
|
171
172
|
return "";
|
|
172
173
|
}
|
|
174
|
+
function hasValue(key, lang) {
|
|
175
|
+
const full = `${key}.${lang}`;
|
|
176
|
+
return state.pending.has(full) || state.cmsData[full] !== void 0;
|
|
177
|
+
}
|
|
173
178
|
function setPending(key, lang, value) {
|
|
174
179
|
const full = pendingKey(key, lang);
|
|
175
180
|
state.pending.set(full, { key, lang, value });
|
|
176
181
|
}
|
|
177
|
-
function parseLinkOverlay(raw) {
|
|
178
|
-
if (!raw) return { label: "", href: "" };
|
|
179
|
-
const trimmed = raw.trim();
|
|
180
|
-
if (trimmed.startsWith("{")) {
|
|
181
|
-
try {
|
|
182
|
-
const p = JSON.parse(trimmed);
|
|
183
|
-
if (p && typeof p === "object") {
|
|
184
|
-
return { label: String(p.label ?? ""), href: String(p.href ?? "") };
|
|
185
|
-
}
|
|
186
|
-
} catch {
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
return { label: raw, href: "" };
|
|
190
|
-
}
|
|
191
182
|
function applyOverlay() {
|
|
192
183
|
document.querySelectorAll("[data-cms]").forEach((el) => {
|
|
193
184
|
if (el.dataset.cmsList !== void 0) return;
|
|
@@ -205,7 +196,7 @@ function applyOverlay() {
|
|
|
205
196
|
return;
|
|
206
197
|
}
|
|
207
198
|
if (el.dataset.cmsType === "link") {
|
|
208
|
-
const link =
|
|
199
|
+
const link = parseLinkValue(savedValue);
|
|
209
200
|
if (link.href && el.tagName === "A") el.setAttribute("href", link.href);
|
|
210
201
|
const labelEls = el.querySelectorAll("[data-cms-label]");
|
|
211
202
|
if (labelEls.length > 0) labelEls.forEach((n) => n.textContent = link.label);
|
|
@@ -223,7 +214,12 @@ function applyOverlay() {
|
|
|
223
214
|
}
|
|
224
215
|
function revertPending() {
|
|
225
216
|
for (const [fullKey, { key }] of state.pending) {
|
|
226
|
-
|
|
217
|
+
if (state.cmsData[fullKey] === void 0) {
|
|
218
|
+
state.pending.clear();
|
|
219
|
+
location.reload();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const savedValue = state.cmsData[fullKey];
|
|
227
223
|
document.querySelectorAll(`[data-cms="${key}"]`).forEach((el) => {
|
|
228
224
|
if (el.tagName === "IMG") {
|
|
229
225
|
el.src = savedValue;
|
|
@@ -271,6 +267,22 @@ async function saveEntry(key, lang, value) {
|
|
|
271
267
|
});
|
|
272
268
|
if (!res.ok) throw new Error(`Cancia: failed to save (${res.status})`);
|
|
273
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
|
+
}
|
|
274
286
|
function isAuthError(err) {
|
|
275
287
|
return err instanceof Error && err.message.includes("(401)");
|
|
276
288
|
}
|
|
@@ -347,8 +359,7 @@ async function createListEntry(listName, data, locale, id) {
|
|
|
347
359
|
{ method: "POST", headers: headers(), body: JSON.stringify({ data, id }) }
|
|
348
360
|
);
|
|
349
361
|
if (!res.ok) {
|
|
350
|
-
|
|
351
|
-
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.");
|
|
352
363
|
}
|
|
353
364
|
const body = await res.json();
|
|
354
365
|
return body.entry;
|
|
@@ -360,9 +371,7 @@ async function updateListEntry(listName, id, data, rev, locale) {
|
|
|
360
371
|
{ method: "PATCH", headers: headers(), body: JSON.stringify({ data, _rev: rev }) }
|
|
361
372
|
);
|
|
362
373
|
if (!res.ok) {
|
|
363
|
-
|
|
364
|
-
const message = err.error ?? `Cancia: failed to update entry (${res.status})`;
|
|
365
|
-
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.");
|
|
366
375
|
}
|
|
367
376
|
const body = await res.json();
|
|
368
377
|
return body.entry;
|
|
@@ -383,19 +392,23 @@ async function reorderList(listName, ids) {
|
|
|
383
392
|
);
|
|
384
393
|
if (!res.ok) throw new Error(`Cancia: failed to reorder list (${res.status})`);
|
|
385
394
|
}
|
|
395
|
+
async function flushEdits(edits, save) {
|
|
396
|
+
const results = await Promise.allSettled(edits.map((edit) => save(edit)));
|
|
397
|
+
const saved = [];
|
|
398
|
+
const failed = [];
|
|
399
|
+
results.forEach((result, i) => {
|
|
400
|
+
(result.status === "fulfilled" ? saved : failed).push(edits[i]);
|
|
401
|
+
});
|
|
402
|
+
return { saved, failed };
|
|
403
|
+
}
|
|
386
404
|
async function flushPending() {
|
|
387
405
|
const entries = Array.from(state.pending.values());
|
|
388
|
-
const
|
|
389
|
-
entries
|
|
406
|
+
const { saved, failed } = await flushEdits(
|
|
407
|
+
entries,
|
|
408
|
+
({ key, lang, value }) => saveEntry(key, lang, value)
|
|
390
409
|
);
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const { key, lang } = entries[i];
|
|
394
|
-
state.pending.delete(`${key}.${lang}`);
|
|
395
|
-
}
|
|
396
|
-
});
|
|
397
|
-
const failed = results.filter((r) => r.status === "rejected").length;
|
|
398
|
-
if (failed > 0) throw new Error(`Cancia: ${failed} save(s) failed`);
|
|
410
|
+
for (const { key, lang } of saved) state.pending.delete(`${key}.${lang}`);
|
|
411
|
+
if (failed.length > 0) throw new Error(`Cancia: ${failed.length} save(s) failed`);
|
|
399
412
|
}
|
|
400
413
|
|
|
401
414
|
// src/tokens.ts
|
|
@@ -1110,9 +1123,11 @@ function onPendingChange(cb) {
|
|
|
1110
1123
|
|
|
1111
1124
|
// src/popup.ts
|
|
1112
1125
|
import {
|
|
1126
|
+
isSafeHref,
|
|
1113
1127
|
portableTextToRows,
|
|
1114
1128
|
rowsToPortableText
|
|
1115
1129
|
} from "@cancia/astro/richtext";
|
|
1130
|
+
import { parseLinkValue as parseLinkValue2 } from "@cancia/astro/schema";
|
|
1116
1131
|
var popupEl = null;
|
|
1117
1132
|
var outsideListener = null;
|
|
1118
1133
|
var keyListener = null;
|
|
@@ -1172,6 +1187,20 @@ function buildHeader(key, onClose) {
|
|
|
1172
1187
|
header.appendChild(makeCloseButton(onClose));
|
|
1173
1188
|
return header;
|
|
1174
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
|
+
}
|
|
1175
1204
|
function buildTextPopup(key, anchorEl, onClose) {
|
|
1176
1205
|
const langs = state.config?.languages ?? ["en"];
|
|
1177
1206
|
let activeLang = state.activeLang || langs[0];
|
|
@@ -1206,9 +1235,7 @@ function buildTextPopup(key, anchorEl, onClose) {
|
|
|
1206
1235
|
tab.addEventListener("click", () => {
|
|
1207
1236
|
const current = wrap.querySelector("textarea");
|
|
1208
1237
|
if (current) {
|
|
1209
|
-
|
|
1210
|
-
const fallback = anchorEl.textContent?.trim() || "";
|
|
1211
|
-
if (current.value !== (existing || fallback)) {
|
|
1238
|
+
if (current.value !== seedValue(key, activeLang, anchorEl)) {
|
|
1212
1239
|
setPending(key, activeLang, current.value);
|
|
1213
1240
|
}
|
|
1214
1241
|
}
|
|
@@ -1238,7 +1265,7 @@ function buildTextPopup(key, anchorEl, onClose) {
|
|
|
1238
1265
|
};
|
|
1239
1266
|
const renderTextarea = (isInit = false) => {
|
|
1240
1267
|
if (!isInit && textarea) {
|
|
1241
|
-
textarea.value =
|
|
1268
|
+
textarea.value = seedValue(key, activeLang, anchorEl);
|
|
1242
1269
|
textarea.style.borderColor = v("accent-ring");
|
|
1243
1270
|
textarea.style.background = v("surface-hover");
|
|
1244
1271
|
attachInputHandler();
|
|
@@ -1246,7 +1273,7 @@ function buildTextPopup(key, anchorEl, onClose) {
|
|
|
1246
1273
|
}
|
|
1247
1274
|
const footerEl = wrap.querySelector("[data-cancia-footer]");
|
|
1248
1275
|
textarea = document.createElement("textarea");
|
|
1249
|
-
textarea.value =
|
|
1276
|
+
textarea.value = seedValue(key, activeLang, anchorEl);
|
|
1250
1277
|
textarea.rows = 4;
|
|
1251
1278
|
textarea.placeholder = "Enter text\u2026";
|
|
1252
1279
|
textarea.style.cssText = `
|
|
@@ -1269,46 +1296,47 @@ function buildTextPopup(key, anchorEl, onClose) {
|
|
|
1269
1296
|
const footer = document.createElement("div");
|
|
1270
1297
|
footer.dataset.canciaFooter = "1";
|
|
1271
1298
|
footer.style.cssText = `display: flex; justify-content: flex-end; margin-top: 10px;`;
|
|
1272
|
-
const saveBtn = makePrimaryButton("
|
|
1299
|
+
const saveBtn = makePrimaryButton("Done", accent2());
|
|
1273
1300
|
saveBtn.dataset.canciaSave = "1";
|
|
1274
|
-
saveBtn.title = "
|
|
1301
|
+
saveBtn.title = "Apply to the page (\u2318S) \u2014 then Save in the toolbar to publish";
|
|
1275
1302
|
saveBtn.addEventListener("click", () => {
|
|
1276
|
-
|
|
1277
|
-
const fallback = anchorEl.textContent?.trim() || "";
|
|
1278
|
-
if (textarea.value !== (existing || fallback)) {
|
|
1303
|
+
if (textarea.value !== seedValue(key, activeLang, anchorEl)) {
|
|
1279
1304
|
setPending(key, activeLang, textarea.value);
|
|
1280
1305
|
}
|
|
1281
1306
|
onPendingChange();
|
|
1282
1307
|
onClose();
|
|
1283
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
|
+
}
|
|
1284
1330
|
footer.appendChild(saveBtn);
|
|
1285
1331
|
wrap.appendChild(footer);
|
|
1286
1332
|
return wrap;
|
|
1287
1333
|
}
|
|
1288
1334
|
function isSafeHrefValue(href) {
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
if (/^(https?:|mailto:|tel:)/i.test(trimmed)) return true;
|
|
1292
|
-
const schemeMatch = /^([a-z][a-z0-9+.-]*):/i.exec(trimmed);
|
|
1293
|
-
if (schemeMatch) {
|
|
1294
|
-
const firstSep = trimmed.search(/[/?#]/);
|
|
1295
|
-
if (firstSep === -1 || schemeMatch[1].length < firstSep) return false;
|
|
1296
|
-
}
|
|
1297
|
-
return true;
|
|
1335
|
+
if (href.trim() === "") return true;
|
|
1336
|
+
return isSafeHref(href);
|
|
1298
1337
|
}
|
|
1299
1338
|
function parseLink(raw) {
|
|
1300
|
-
|
|
1301
|
-
const trimmed = raw.trim();
|
|
1302
|
-
if (trimmed.startsWith("{")) {
|
|
1303
|
-
try {
|
|
1304
|
-
const p = JSON.parse(trimmed);
|
|
1305
|
-
if (p && typeof p === "object") {
|
|
1306
|
-
return { label: String(p.label ?? ""), href: String(p.href ?? "") };
|
|
1307
|
-
}
|
|
1308
|
-
} catch {
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
return { label: raw, href: "" };
|
|
1339
|
+
return parseLinkValue2(raw);
|
|
1312
1340
|
}
|
|
1313
1341
|
function buildLinkPopup(key, anchorEl, onClose) {
|
|
1314
1342
|
const langs = state.config?.languages ?? ["en"];
|
|
@@ -1433,9 +1461,9 @@ function buildLinkPopup(key, anchorEl, onClose) {
|
|
|
1433
1461
|
const footer = document.createElement("div");
|
|
1434
1462
|
footer.dataset.canciaFooter = "1";
|
|
1435
1463
|
footer.style.cssText = `display: flex; justify-content: flex-end; margin-top: 10px;`;
|
|
1436
|
-
const saveBtn = makePrimaryButton("
|
|
1464
|
+
const saveBtn = makePrimaryButton("Done", accent2());
|
|
1437
1465
|
saveBtn.dataset.canciaSave = "1";
|
|
1438
|
-
saveBtn.title = "
|
|
1466
|
+
saveBtn.title = "Apply to the page (\u2318S) \u2014 then Save in the toolbar to publish";
|
|
1439
1467
|
saveBtn.addEventListener("click", () => {
|
|
1440
1468
|
if (!validate()) return;
|
|
1441
1469
|
stage(activeLang);
|
|
@@ -1473,6 +1501,33 @@ function buildImagePopup(key, anchorEl, onClose) {
|
|
|
1473
1501
|
previewWrap.appendChild(previewImg);
|
|
1474
1502
|
previewWrap.appendChild(previewLabel);
|
|
1475
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
|
+
}
|
|
1476
1531
|
}
|
|
1477
1532
|
const dropZone = document.createElement("label");
|
|
1478
1533
|
dropZone.style.cssText = `
|
|
@@ -1828,9 +1883,9 @@ function buildRichPopup(key, anchorEl, onClose) {
|
|
|
1828
1883
|
rowsWrap.appendChild(r.el);
|
|
1829
1884
|
r.el.querySelector("textarea")?.focus();
|
|
1830
1885
|
});
|
|
1831
|
-
const saveBtn = makePrimaryButton("
|
|
1886
|
+
const saveBtn = makePrimaryButton("Done", accent2());
|
|
1832
1887
|
saveBtn.dataset.canciaSave = "1";
|
|
1833
|
-
saveBtn.title = "
|
|
1888
|
+
saveBtn.title = "Apply to the page (\u2318S) \u2014 then Save in the toolbar to publish";
|
|
1834
1889
|
saveBtn.addEventListener("click", () => {
|
|
1835
1890
|
commit();
|
|
1836
1891
|
onClose();
|
|
@@ -1899,6 +1954,7 @@ function closePopup() {
|
|
|
1899
1954
|
}
|
|
1900
1955
|
|
|
1901
1956
|
// src/list-panel.ts
|
|
1957
|
+
import { isDraft } from "@cancia/astro/schema";
|
|
1902
1958
|
var PANEL_Z = v("z-panel");
|
|
1903
1959
|
var TOOLBAR_RESERVE = "100px";
|
|
1904
1960
|
var BACKDROP_Z = v("z-overlay");
|
|
@@ -2239,7 +2295,7 @@ function renderEntries(body, schema, activeLocale, entriesInLocale, translations
|
|
|
2239
2295
|
const titleValue = row.entry.data[schema.titleField];
|
|
2240
2296
|
const title = typeof titleValue === "string" && titleValue.trim().length > 0 ? titleValue : `(untitled ${schema.labelSingular.toLowerCase()})`;
|
|
2241
2297
|
const { subtitle, thumbnail } = derivePreview(schema, row.entry.data);
|
|
2242
|
-
const isDraftRow =
|
|
2298
|
+
const isDraftRow = isDraft(schema, row.entry.data);
|
|
2243
2299
|
const draftBadge = isDraftRow ? `<span style="
|
|
2244
2300
|
flex-shrink: 0; margin-left: ${v("space-2")}; padding: 1px 6px;
|
|
2245
2301
|
font-size: 11px; font-weight: 600; line-height: 1.5;
|
|
@@ -2454,7 +2510,7 @@ import {
|
|
|
2454
2510
|
portableTextSubsetSchema as portableTextSubsetSchema2,
|
|
2455
2511
|
PT_STYLES as PT_STYLES2
|
|
2456
2512
|
} from "@cancia/astro/richtext";
|
|
2457
|
-
import { slugify } from "@cancia/astro/schema";
|
|
2513
|
+
import { slugify, parseLinkValue as parseLinkValue3, isSafeHref as isSafeHref2 } from "@cancia/astro/schema";
|
|
2458
2514
|
var MODAL_Z = v("z-bar");
|
|
2459
2515
|
var BACKDROP_Z2 = v("z-panel");
|
|
2460
2516
|
var modalEl = null;
|
|
@@ -2834,6 +2890,57 @@ function renderField(field, initial, depth = 0) {
|
|
|
2834
2890
|
};
|
|
2835
2891
|
break;
|
|
2836
2892
|
}
|
|
2893
|
+
case "link": {
|
|
2894
|
+
const parsed = parseLinkValue3(initial);
|
|
2895
|
+
const box = document.createElement("div");
|
|
2896
|
+
box.style.cssText = `
|
|
2897
|
+
display: flex; flex-direction: column; gap: 8px;
|
|
2898
|
+
background: ${v("surface-raised")};
|
|
2899
|
+
border: 1px solid ${v("border")};
|
|
2900
|
+
border-radius: ${v("radius")};
|
|
2901
|
+
padding: 10px;
|
|
2902
|
+
`;
|
|
2903
|
+
const mkRow = (labelText2, value, placeholder) => {
|
|
2904
|
+
const row = document.createElement("label");
|
|
2905
|
+
row.style.cssText = `display: flex; flex-direction: column; gap: 4px;`;
|
|
2906
|
+
const cap = document.createElement("span");
|
|
2907
|
+
cap.textContent = labelText2;
|
|
2908
|
+
cap.style.cssText = `font-size: 11px; font-weight: 600; color: ${v("fg-muted")}; letter-spacing: 0.02em;`;
|
|
2909
|
+
const inp = document.createElement("input");
|
|
2910
|
+
inp.type = "text";
|
|
2911
|
+
inp.className = "cancia-form-input";
|
|
2912
|
+
inp.style.cssText = `${INPUT_BASE} caret-color: ${v("accent")};`;
|
|
2913
|
+
inp.value = value;
|
|
2914
|
+
inp.placeholder = placeholder;
|
|
2915
|
+
row.appendChild(cap);
|
|
2916
|
+
row.appendChild(inp);
|
|
2917
|
+
box.appendChild(row);
|
|
2918
|
+
return inp;
|
|
2919
|
+
};
|
|
2920
|
+
const labelInput = mkRow("Label", parsed.label, "Book a call");
|
|
2921
|
+
const hrefInput = mkRow("URL", parsed.href, "/start or https://\u2026");
|
|
2922
|
+
wrapper.appendChild(box);
|
|
2923
|
+
getValue2 = () => {
|
|
2924
|
+
const label2 = labelInput.value.trim();
|
|
2925
|
+
const href = hrefInput.value.trim();
|
|
2926
|
+
if (!label2 && !href) return void 0;
|
|
2927
|
+
return { label: label2, href };
|
|
2928
|
+
};
|
|
2929
|
+
validate = () => {
|
|
2930
|
+
const value = getValue2();
|
|
2931
|
+
setError(null);
|
|
2932
|
+
if (field.required && !value) {
|
|
2933
|
+
setError(`${field.label || "This field"} is required`);
|
|
2934
|
+
return { value, ok: false };
|
|
2935
|
+
}
|
|
2936
|
+
if (value && value.href && !isSafeHref2(value.href)) {
|
|
2937
|
+
setError("That link type isn't allowed. Use https://, /a-path, #anchor, mailto: or tel:");
|
|
2938
|
+
return { value, ok: false };
|
|
2939
|
+
}
|
|
2940
|
+
return { value, ok: true };
|
|
2941
|
+
};
|
|
2942
|
+
break;
|
|
2943
|
+
}
|
|
2837
2944
|
default: {
|
|
2838
2945
|
const input2 = document.createElement("input");
|
|
2839
2946
|
input2.type = field.widget === "url" ? "url" : field.widget === "email" ? "email" : "text";
|
|
@@ -3343,6 +3450,19 @@ function preValidate(fieldStates) {
|
|
|
3343
3450
|
}
|
|
3344
3451
|
return { data, ok };
|
|
3345
3452
|
}
|
|
3453
|
+
function paintServerIssues(fieldStates, issues) {
|
|
3454
|
+
if (!issues?.length) return 0;
|
|
3455
|
+
let painted = 0;
|
|
3456
|
+
for (const issue of issues) {
|
|
3457
|
+
const head = issue.path?.[0];
|
|
3458
|
+
if (head === void 0) continue;
|
|
3459
|
+
const target = fieldStates.find((fs) => fs.field.name === String(head));
|
|
3460
|
+
if (!target) continue;
|
|
3461
|
+
target.setError(issue.message);
|
|
3462
|
+
painted++;
|
|
3463
|
+
}
|
|
3464
|
+
return painted;
|
|
3465
|
+
}
|
|
3346
3466
|
function validateValue(field, value) {
|
|
3347
3467
|
if (value === void 0 || value === null || value === "") return null;
|
|
3348
3468
|
if (typeof value === "string") {
|
|
@@ -3648,10 +3768,15 @@ function openEntryModal(opts) {
|
|
|
3648
3768
|
closeEntryModal();
|
|
3649
3769
|
} catch (err) {
|
|
3650
3770
|
const error = err;
|
|
3651
|
-
|
|
3652
|
-
|
|
3771
|
+
const painted = paintServerIssues(fieldStates, error.issues);
|
|
3772
|
+
if (painted > 0) {
|
|
3773
|
+
showFormError(
|
|
3774
|
+
painted === 1 ? "One field needs fixing \u2014 see the message below it." : `${painted} fields need fixing \u2014 see the messages below them.`
|
|
3775
|
+
);
|
|
3653
3776
|
} else if (error.code === "REV_CONFLICT") {
|
|
3654
3777
|
showFormError("This entry was changed by someone else. Close and reopen to see the latest version.");
|
|
3778
|
+
} else if (error.issues?.length) {
|
|
3779
|
+
showFormError(error.issues.map((i) => `${i.path.join(".") || "Entry"}: ${i.message}`).join(" \xB7 "));
|
|
3655
3780
|
} else {
|
|
3656
3781
|
showFormError(error.message);
|
|
3657
3782
|
}
|