@cancia/toolbar 0.0.1 → 0.1.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 +243 -96
- package/dist/cancia.js +755 -29
- package/dist/cancia.js.map +1 -1
- package/package.json +4 -1
package/dist/cancia.js
CHANGED
|
@@ -189,6 +189,14 @@ async function deleteListEntry(listName, id, locale) {
|
|
|
189
189
|
);
|
|
190
190
|
if (!res.ok) throw new Error(`Cancia: failed to delete entry (${res.status})`);
|
|
191
191
|
}
|
|
192
|
+
async function reorderList(listName, ids) {
|
|
193
|
+
const { apiUrl, site } = state.config;
|
|
194
|
+
const res = await fetch(
|
|
195
|
+
`${apiUrl}/api/cancia/lists/${encodeURIComponent(listName)}/reorder?site=${encodeURIComponent(site)}`,
|
|
196
|
+
{ method: "POST", headers: headers(), body: JSON.stringify({ ids }) }
|
|
197
|
+
);
|
|
198
|
+
if (!res.ok) throw new Error(`Cancia: failed to reorder list (${res.status})`);
|
|
199
|
+
}
|
|
192
200
|
async function flushPending() {
|
|
193
201
|
const entries = Array.from(state.pending.values());
|
|
194
202
|
const results = await Promise.allSettled(
|
|
@@ -958,11 +966,55 @@ function escapeHtml(s) {
|
|
|
958
966
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
959
967
|
}
|
|
960
968
|
function formatExcerpt(value, maxLength = 120) {
|
|
961
|
-
|
|
962
|
-
|
|
969
|
+
const text = toPlainText(value);
|
|
970
|
+
if (!text) return "";
|
|
971
|
+
const trimmed = text.trim();
|
|
963
972
|
if (trimmed.length <= maxLength) return trimmed;
|
|
964
973
|
return trimmed.slice(0, maxLength).trimEnd() + "\u2026";
|
|
965
974
|
}
|
|
975
|
+
function toPlainText(value) {
|
|
976
|
+
if (typeof value === "string") return value;
|
|
977
|
+
if (Array.isArray(value)) {
|
|
978
|
+
const parts = [];
|
|
979
|
+
for (const block of value) {
|
|
980
|
+
if (block && typeof block === "object" && Array.isArray(block.children)) {
|
|
981
|
+
for (const span of block.children) {
|
|
982
|
+
const t = span?.text;
|
|
983
|
+
if (typeof t === "string") parts.push(t);
|
|
984
|
+
}
|
|
985
|
+
parts.push(" ");
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
return parts.join("").replace(/\s+/g, " ").trim();
|
|
989
|
+
}
|
|
990
|
+
return "";
|
|
991
|
+
}
|
|
992
|
+
function derivePreview(schema, data) {
|
|
993
|
+
let subtitle = "";
|
|
994
|
+
if (schema.bodyField) subtitle = formatExcerpt(data[schema.bodyField]);
|
|
995
|
+
if (!subtitle) {
|
|
996
|
+
const TEXTISH = /* @__PURE__ */ new Set(["text", "textarea", "richtext"]);
|
|
997
|
+
for (const f of schema.fields) {
|
|
998
|
+
if (f.name === schema.titleField) continue;
|
|
999
|
+
if (!TEXTISH.has(f.widget)) continue;
|
|
1000
|
+
const s = formatExcerpt(data[f.name]);
|
|
1001
|
+
if (s) {
|
|
1002
|
+
subtitle = s;
|
|
1003
|
+
break;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
let thumbnail = "";
|
|
1008
|
+
for (const f of schema.fields) {
|
|
1009
|
+
if (f.widget !== "image") continue;
|
|
1010
|
+
const v = data[f.name];
|
|
1011
|
+
if (typeof v === "string" && v.trim()) {
|
|
1012
|
+
thumbnail = v.trim();
|
|
1013
|
+
break;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return { subtitle, thumbnail };
|
|
1017
|
+
}
|
|
966
1018
|
function locales() {
|
|
967
1019
|
return state.config?.languages ?? [];
|
|
968
1020
|
}
|
|
@@ -1097,25 +1149,106 @@ function renderEntries(body, schema, activeLocale, entriesInLocale, translations
|
|
|
1097
1149
|
return;
|
|
1098
1150
|
}
|
|
1099
1151
|
body.innerHTML = "";
|
|
1152
|
+
const dragRecs = [];
|
|
1153
|
+
let dragging = null;
|
|
1154
|
+
let orderBeforeDrag = [];
|
|
1155
|
+
const currentOrder = () => dragRecs.map((r) => r.id);
|
|
1156
|
+
async function commitOrder(prevOrder) {
|
|
1157
|
+
try {
|
|
1158
|
+
await reorderList(schema.name, currentOrder());
|
|
1159
|
+
} catch {
|
|
1160
|
+
const byId = new Map(dragRecs.map((r) => [r.id, r]));
|
|
1161
|
+
dragRecs.length = 0;
|
|
1162
|
+
for (const id of prevOrder) {
|
|
1163
|
+
const rec = byId.get(id);
|
|
1164
|
+
if (rec) {
|
|
1165
|
+
dragRecs.push(rec);
|
|
1166
|
+
body.appendChild(rec.wrap);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
const err = document.createElement("div");
|
|
1170
|
+
err.textContent = "Couldn't save the new order. Reverted.";
|
|
1171
|
+
err.style.cssText = `text-align:center; padding:8px; color:#c0392b; font-size:12px;`;
|
|
1172
|
+
body.insertBefore(err, body.firstChild);
|
|
1173
|
+
setTimeout(() => err.remove(), 3e3);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1100
1176
|
rows.forEach((row) => {
|
|
1101
|
-
const item = document.createElement("button");
|
|
1102
1177
|
const isStub = row.entry === null;
|
|
1178
|
+
const wrap = document.createElement("div");
|
|
1179
|
+
wrap.style.cssText = `display: flex; align-items: stretch; gap: 4px; margin-bottom: 6px;`;
|
|
1180
|
+
const handle = document.createElement("div");
|
|
1181
|
+
handle.textContent = "\u22EE\u22EE";
|
|
1182
|
+
handle.title = "Drag to reorder";
|
|
1183
|
+
handle.draggable = true;
|
|
1184
|
+
handle.style.cssText = `
|
|
1185
|
+
cursor: grab; user-select: none;
|
|
1186
|
+
display: flex; align-items: center; justify-content: center;
|
|
1187
|
+
color: #bbb; font-size: 13px; letter-spacing: -2px;
|
|
1188
|
+
padding: 0 2px; flex-shrink: 0;
|
|
1189
|
+
`;
|
|
1190
|
+
const item = document.createElement("button");
|
|
1103
1191
|
item.style.cssText = `
|
|
1104
1192
|
appearance: none; border: 1px solid transparent; background: ${isStub ? "#fff8ee" : "#fafafa"};
|
|
1105
|
-
text-align: left; width:
|
|
1193
|
+
text-align: left; flex: 1; min-width: 0;
|
|
1106
1194
|
padding: 12px 14px; border-radius: 8px; cursor: pointer;
|
|
1107
|
-
margin-bottom: 6px;
|
|
1108
1195
|
transition: background 0.12s, border-color 0.12s, transform 0.12s;
|
|
1109
1196
|
display: block;
|
|
1110
1197
|
`;
|
|
1198
|
+
const rec = { wrap, id: row.id };
|
|
1199
|
+
handle.addEventListener("dragstart", (e) => {
|
|
1200
|
+
dragging = rec;
|
|
1201
|
+
orderBeforeDrag = currentOrder();
|
|
1202
|
+
handle.style.cursor = "grabbing";
|
|
1203
|
+
wrap.style.opacity = "0.5";
|
|
1204
|
+
e.dataTransfer?.setData("text/plain", "");
|
|
1205
|
+
if (e.dataTransfer) e.dataTransfer.effectAllowed = "move";
|
|
1206
|
+
});
|
|
1207
|
+
handle.addEventListener("dragend", () => {
|
|
1208
|
+
handle.style.cursor = "grab";
|
|
1209
|
+
wrap.style.opacity = "1";
|
|
1210
|
+
dragging = null;
|
|
1211
|
+
});
|
|
1212
|
+
wrap.addEventListener("dragover", (e) => {
|
|
1213
|
+
if (!dragging || dragging === rec) return;
|
|
1214
|
+
e.preventDefault();
|
|
1215
|
+
const rect = wrap.getBoundingClientRect();
|
|
1216
|
+
const after = e.clientY > rect.top + rect.height / 2;
|
|
1217
|
+
const from = dragRecs.indexOf(dragging);
|
|
1218
|
+
let to = dragRecs.indexOf(rec);
|
|
1219
|
+
if (from < 0 || to < 0) return;
|
|
1220
|
+
if (after) to += 1;
|
|
1221
|
+
if (from < to) to -= 1;
|
|
1222
|
+
if (from === to) return;
|
|
1223
|
+
dragRecs.splice(from, 1);
|
|
1224
|
+
dragRecs.splice(to, 0, dragging);
|
|
1225
|
+
body.insertBefore(dragging.wrap, after ? wrap.nextSibling : wrap);
|
|
1226
|
+
});
|
|
1227
|
+
wrap.addEventListener("drop", (e) => {
|
|
1228
|
+
if (!dragging) return;
|
|
1229
|
+
e.preventDefault();
|
|
1230
|
+
const next = currentOrder();
|
|
1231
|
+
const changed = next.length !== orderBeforeDrag.length || next.some((id, i) => id !== orderBeforeDrag[i]);
|
|
1232
|
+
if (changed) void commitOrder(orderBeforeDrag);
|
|
1233
|
+
});
|
|
1111
1234
|
if (row.entry) {
|
|
1112
1235
|
const titleValue = row.entry.data[schema.titleField];
|
|
1113
1236
|
const title = typeof titleValue === "string" && titleValue.trim().length > 0 ? titleValue : `(untitled ${schema.labelSingular.toLowerCase()})`;
|
|
1114
|
-
const
|
|
1115
|
-
const
|
|
1237
|
+
const { subtitle, thumbnail } = derivePreview(schema, row.entry.data);
|
|
1238
|
+
const textCol = `
|
|
1239
|
+
<div style="min-width: 0; flex: 1;">
|
|
1240
|
+
<div style="font-weight: 600; font-size: 14px; color: #1a1a1d; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${escapeHtml(title)}</div>
|
|
1241
|
+
${subtitle ? `<div style="font-size: 12px; color: #666; margin-top: 4px; line-height: 1.4;">${escapeHtml(subtitle)}</div>` : ""}
|
|
1242
|
+
</div>
|
|
1243
|
+
`;
|
|
1244
|
+
const thumb = thumbnail ? `<img src="${escapeHtml(thumbnail)}" alt="" loading="lazy" style="
|
|
1245
|
+
width: 44px; height: 44px; flex-shrink: 0; object-fit: cover;
|
|
1246
|
+
border-radius: 6px; background: #eee; border: 1px solid #eaeaea;
|
|
1247
|
+
" onerror="this.style.display='none'" />` : "";
|
|
1116
1248
|
item.innerHTML = `
|
|
1117
|
-
<div style="
|
|
1118
|
-
|
|
1249
|
+
<div style="display: flex; align-items: center; gap: 12px;">
|
|
1250
|
+
${thumb}${textCol}
|
|
1251
|
+
</div>
|
|
1119
1252
|
`;
|
|
1120
1253
|
item.addEventListener("mouseenter", () => {
|
|
1121
1254
|
item.style.background = "#f3f3f3";
|
|
@@ -1156,7 +1289,10 @@ function renderEntries(body, schema, activeLocale, entriesInLocale, translations
|
|
|
1156
1289
|
() => currentOnTranslateEntry?.(row.id, sourceEntry, activeLocale)
|
|
1157
1290
|
);
|
|
1158
1291
|
}
|
|
1159
|
-
|
|
1292
|
+
wrap.appendChild(handle);
|
|
1293
|
+
wrap.appendChild(item);
|
|
1294
|
+
dragRecs.push(rec);
|
|
1295
|
+
body.appendChild(wrap);
|
|
1160
1296
|
});
|
|
1161
1297
|
}
|
|
1162
1298
|
async function openListPanel(opts) {
|
|
@@ -1250,6 +1386,13 @@ function isListPanelOpen() {
|
|
|
1250
1386
|
}
|
|
1251
1387
|
|
|
1252
1388
|
// src/entry-modal.ts
|
|
1389
|
+
import {
|
|
1390
|
+
rowsToPortableText,
|
|
1391
|
+
portableTextToRows,
|
|
1392
|
+
portableTextSubsetSchema,
|
|
1393
|
+
PT_STYLES
|
|
1394
|
+
} from "@cancia/astro/richtext";
|
|
1395
|
+
import { slugify } from "@cancia/astro/schema";
|
|
1253
1396
|
var MODAL_Z = 2147483647;
|
|
1254
1397
|
var BACKDROP_Z2 = 2147483646;
|
|
1255
1398
|
var modalEl = null;
|
|
@@ -1327,7 +1470,8 @@ var INPUT_BASE = `
|
|
|
1327
1470
|
outline: none;
|
|
1328
1471
|
transition: border-color 0.18s, background 0.18s;
|
|
1329
1472
|
`;
|
|
1330
|
-
|
|
1473
|
+
var MAX_FIELD_DEPTH = 6;
|
|
1474
|
+
function renderField(field, initial, depth = 0) {
|
|
1331
1475
|
const wrapper = document.createElement("div");
|
|
1332
1476
|
wrapper.style.cssText = `margin-bottom: 14px;`;
|
|
1333
1477
|
const labelRow = document.createElement("label");
|
|
@@ -1348,7 +1492,7 @@ function renderField(field, initial) {
|
|
|
1348
1492
|
labelText.appendChild(star);
|
|
1349
1493
|
}
|
|
1350
1494
|
labelRow.appendChild(labelText);
|
|
1351
|
-
wrapper.appendChild(labelRow);
|
|
1495
|
+
if (field.label) wrapper.appendChild(labelRow);
|
|
1352
1496
|
if (field.description) {
|
|
1353
1497
|
const help = document.createElement("div");
|
|
1354
1498
|
help.style.cssText = `font-size: 11px; color: rgba(255,255,255,0.35); margin-bottom: 6px; line-height: 1.4;`;
|
|
@@ -1368,7 +1512,37 @@ function renderField(field, initial) {
|
|
|
1368
1512
|
}
|
|
1369
1513
|
};
|
|
1370
1514
|
let getValue2;
|
|
1515
|
+
let validate = null;
|
|
1516
|
+
let onInput;
|
|
1517
|
+
let setValue;
|
|
1371
1518
|
switch (field.widget) {
|
|
1519
|
+
case "array": {
|
|
1520
|
+
const built = renderArrayField(field, initial, setError, depth);
|
|
1521
|
+
wrapper.appendChild(built.control);
|
|
1522
|
+
getValue2 = built.getValue;
|
|
1523
|
+
validate = built.validate;
|
|
1524
|
+
break;
|
|
1525
|
+
}
|
|
1526
|
+
case "object": {
|
|
1527
|
+
const built = renderObjectField(field, initial, setError, depth);
|
|
1528
|
+
wrapper.appendChild(built.control);
|
|
1529
|
+
getValue2 = built.getValue;
|
|
1530
|
+
validate = built.validate;
|
|
1531
|
+
break;
|
|
1532
|
+
}
|
|
1533
|
+
case "reference": {
|
|
1534
|
+
const built = renderReferenceField(field, initial);
|
|
1535
|
+
wrapper.appendChild(built.control);
|
|
1536
|
+
getValue2 = built.getValue;
|
|
1537
|
+
break;
|
|
1538
|
+
}
|
|
1539
|
+
case "richtext": {
|
|
1540
|
+
const built = renderRichTextField(field, initial, setError);
|
|
1541
|
+
wrapper.appendChild(built.control);
|
|
1542
|
+
getValue2 = built.getValue;
|
|
1543
|
+
validate = built.validate;
|
|
1544
|
+
break;
|
|
1545
|
+
}
|
|
1372
1546
|
case "textarea": {
|
|
1373
1547
|
const ta = document.createElement("textarea");
|
|
1374
1548
|
ta.className = "cancia-form-textarea";
|
|
@@ -1579,40 +1753,591 @@ function renderField(field, initial) {
|
|
|
1579
1753
|
if (typeof initial === "string") input.value = initial;
|
|
1580
1754
|
wrapper.appendChild(input);
|
|
1581
1755
|
getValue2 = () => input.value;
|
|
1756
|
+
onInput = (cb) => input.addEventListener("input", cb);
|
|
1757
|
+
setValue = (v) => {
|
|
1758
|
+
input.value = v;
|
|
1759
|
+
};
|
|
1582
1760
|
break;
|
|
1583
1761
|
}
|
|
1584
1762
|
}
|
|
1585
1763
|
wrapper.appendChild(errorEl);
|
|
1586
|
-
|
|
1764
|
+
const scalarValidate = () => {
|
|
1765
|
+
const value = getValue2();
|
|
1766
|
+
setError(null);
|
|
1767
|
+
if (field.required && (value === void 0 || value === "" || value === null)) {
|
|
1768
|
+
setError(`${field.label || "This field"} is required`);
|
|
1769
|
+
return { value, ok: false };
|
|
1770
|
+
}
|
|
1771
|
+
const err = validateValue(field, value);
|
|
1772
|
+
if (err) {
|
|
1773
|
+
setError(err);
|
|
1774
|
+
return { value, ok: false };
|
|
1775
|
+
}
|
|
1776
|
+
return { value, ok: true };
|
|
1777
|
+
};
|
|
1778
|
+
return {
|
|
1779
|
+
wrapper,
|
|
1780
|
+
fieldState: { field, getValue: getValue2, setError, validate: validate ?? scalarValidate, onInput, setValue }
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
function renderArrayField(field, initial, setOwnError, depth) {
|
|
1784
|
+
const itemSchema = field.of;
|
|
1785
|
+
const container = document.createElement("div");
|
|
1786
|
+
container.style.cssText = `
|
|
1787
|
+
display: flex; flex-direction: column; gap: 8px;
|
|
1788
|
+
background: rgba(255,255,255,0.02);
|
|
1789
|
+
border: 1px solid rgba(255,255,255,0.07);
|
|
1790
|
+
border-radius: 10px;
|
|
1791
|
+
padding: 10px;
|
|
1792
|
+
`;
|
|
1793
|
+
const rowsWrap = document.createElement("div");
|
|
1794
|
+
rowsWrap.style.cssText = `display: flex; flex-direction: column; gap: 8px;`;
|
|
1795
|
+
container.appendChild(rowsWrap);
|
|
1796
|
+
if (!itemSchema || depth >= MAX_FIELD_DEPTH) {
|
|
1797
|
+
const note = document.createElement("div");
|
|
1798
|
+
note.style.cssText = `font-size: 11px; color: rgba(255,255,255,0.4);`;
|
|
1799
|
+
note.textContent = itemSchema ? "Nesting too deep to edit here." : "This array has no item schema.";
|
|
1800
|
+
container.appendChild(note);
|
|
1801
|
+
return { control: container, getValue: () => [], validate: () => ({ value: [], ok: true }) };
|
|
1802
|
+
}
|
|
1803
|
+
const rows = [];
|
|
1804
|
+
let dragging = null;
|
|
1805
|
+
function makeRow(itemValue) {
|
|
1806
|
+
const row = document.createElement("div");
|
|
1807
|
+
row.style.cssText = `
|
|
1808
|
+
display: flex; align-items: flex-start; gap: 8px;
|
|
1809
|
+
background: rgba(255,255,255,0.02);
|
|
1810
|
+
border: 1px solid rgba(255,255,255,0.06);
|
|
1811
|
+
border-radius: 8px;
|
|
1812
|
+
padding: 8px;
|
|
1813
|
+
`;
|
|
1814
|
+
const handle = document.createElement("div");
|
|
1815
|
+
handle.textContent = "\u22EE\u22EE";
|
|
1816
|
+
handle.title = "Drag to reorder";
|
|
1817
|
+
handle.draggable = true;
|
|
1818
|
+
handle.style.cssText = `
|
|
1819
|
+
cursor: grab; user-select: none;
|
|
1820
|
+
color: rgba(255,255,255,0.35);
|
|
1821
|
+
font-size: 13px; line-height: 1.2;
|
|
1822
|
+
padding: 4px 2px; flex-shrink: 0;
|
|
1823
|
+
letter-spacing: -2px;
|
|
1824
|
+
`;
|
|
1825
|
+
const { wrapper, fieldState } = renderField(itemSchema, itemValue, depth + 1);
|
|
1826
|
+
wrapper.style.marginBottom = "0";
|
|
1827
|
+
wrapper.style.flex = "1";
|
|
1828
|
+
wrapper.style.minWidth = "0";
|
|
1829
|
+
const removeBtn = document.createElement("button");
|
|
1830
|
+
removeBtn.type = "button";
|
|
1831
|
+
removeBtn.textContent = "\xD7";
|
|
1832
|
+
removeBtn.title = "Remove";
|
|
1833
|
+
removeBtn.style.cssText = `
|
|
1834
|
+
appearance: none; cursor: pointer; flex-shrink: 0;
|
|
1835
|
+
background: transparent; border: 1px solid transparent;
|
|
1836
|
+
color: rgba(255,135,134,0.7);
|
|
1837
|
+
font-size: 16px; line-height: 1;
|
|
1838
|
+
padding: 2px 7px; border-radius: 6px;
|
|
1839
|
+
transition: background 0.15s;
|
|
1840
|
+
`;
|
|
1841
|
+
removeBtn.addEventListener("mouseenter", () => {
|
|
1842
|
+
removeBtn.style.background = "rgba(255,135,134,0.1)";
|
|
1843
|
+
});
|
|
1844
|
+
removeBtn.addEventListener("mouseleave", () => {
|
|
1845
|
+
removeBtn.style.background = "transparent";
|
|
1846
|
+
});
|
|
1847
|
+
row.appendChild(handle);
|
|
1848
|
+
row.appendChild(wrapper);
|
|
1849
|
+
row.appendChild(removeBtn);
|
|
1850
|
+
const rec = { el: row, state: fieldState };
|
|
1851
|
+
removeBtn.addEventListener("click", () => {
|
|
1852
|
+
const i = rows.indexOf(rec);
|
|
1853
|
+
if (i >= 0) rows.splice(i, 1);
|
|
1854
|
+
row.remove();
|
|
1855
|
+
setOwnError(null);
|
|
1856
|
+
});
|
|
1857
|
+
handle.addEventListener("dragstart", (e) => {
|
|
1858
|
+
dragging = rec;
|
|
1859
|
+
handle.style.cursor = "grabbing";
|
|
1860
|
+
row.style.opacity = "0.5";
|
|
1861
|
+
e.dataTransfer?.setData("text/plain", "");
|
|
1862
|
+
if (e.dataTransfer) e.dataTransfer.effectAllowed = "move";
|
|
1863
|
+
});
|
|
1864
|
+
handle.addEventListener("dragend", () => {
|
|
1865
|
+
handle.style.cursor = "grab";
|
|
1866
|
+
row.style.opacity = "1";
|
|
1867
|
+
dragging = null;
|
|
1868
|
+
});
|
|
1869
|
+
row.addEventListener("dragover", (e) => {
|
|
1870
|
+
if (!dragging || dragging === rec) return;
|
|
1871
|
+
e.preventDefault();
|
|
1872
|
+
const rect = row.getBoundingClientRect();
|
|
1873
|
+
const after = e.clientY > rect.top + rect.height / 2;
|
|
1874
|
+
const from = rows.indexOf(dragging);
|
|
1875
|
+
let to = rows.indexOf(rec);
|
|
1876
|
+
if (from < 0 || to < 0) return;
|
|
1877
|
+
if (after) to += 1;
|
|
1878
|
+
if (from < to) to -= 1;
|
|
1879
|
+
if (from === to) return;
|
|
1880
|
+
rows.splice(from, 1);
|
|
1881
|
+
rows.splice(to, 0, dragging);
|
|
1882
|
+
rowsWrap.insertBefore(dragging.el, after ? row.nextSibling : row);
|
|
1883
|
+
});
|
|
1884
|
+
return rec;
|
|
1885
|
+
}
|
|
1886
|
+
function addRow(itemValue) {
|
|
1887
|
+
const rec = makeRow(itemValue);
|
|
1888
|
+
rows.push(rec);
|
|
1889
|
+
rowsWrap.appendChild(rec.el);
|
|
1890
|
+
}
|
|
1891
|
+
const initialItems = Array.isArray(initial) ? initial : [];
|
|
1892
|
+
for (const it of initialItems) addRow(it);
|
|
1893
|
+
const addBtn = document.createElement("button");
|
|
1894
|
+
addBtn.type = "button";
|
|
1895
|
+
const itemLabel = itemSchema.label || "item";
|
|
1896
|
+
addBtn.textContent = `+ Add ${itemLabel.toLowerCase()}`;
|
|
1897
|
+
addBtn.style.cssText = `
|
|
1898
|
+
appearance: none; cursor: pointer; align-self: flex-start;
|
|
1899
|
+
background: rgba(255,255,255,0.05); color: rgba(255,255,255,0.8);
|
|
1900
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
1901
|
+
font-size: 11px; font-weight: 500;
|
|
1902
|
+
padding: 6px 11px; border-radius: 7px;
|
|
1903
|
+
transition: background 0.15s;
|
|
1904
|
+
`;
|
|
1905
|
+
addBtn.addEventListener("mouseenter", () => {
|
|
1906
|
+
addBtn.style.background = "rgba(255,255,255,0.1)";
|
|
1907
|
+
});
|
|
1908
|
+
addBtn.addEventListener("mouseleave", () => {
|
|
1909
|
+
addBtn.style.background = "rgba(255,255,255,0.05)";
|
|
1910
|
+
});
|
|
1911
|
+
addBtn.addEventListener("click", () => addRow(defaultForField(itemSchema)));
|
|
1912
|
+
container.appendChild(addBtn);
|
|
1913
|
+
const collect = () => rows.map((r) => r.state.getValue());
|
|
1914
|
+
return {
|
|
1915
|
+
control: container,
|
|
1916
|
+
// Deleting the last item serialises to [] (not undefined) so a cleared
|
|
1917
|
+
// array persists as an empty array.
|
|
1918
|
+
getValue: () => collect(),
|
|
1919
|
+
validate: () => {
|
|
1920
|
+
setOwnError(null);
|
|
1921
|
+
let ok = true;
|
|
1922
|
+
for (const r of rows) {
|
|
1923
|
+
const res = r.state.validate();
|
|
1924
|
+
if (!res.ok) ok = false;
|
|
1925
|
+
}
|
|
1926
|
+
const value = collect();
|
|
1927
|
+
if (field.required && value.length === 0) {
|
|
1928
|
+
setOwnError(`${field.label || "This list"} needs at least one item`);
|
|
1929
|
+
ok = false;
|
|
1930
|
+
}
|
|
1931
|
+
return { value, ok };
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
function renderObjectField(field, initial, setOwnError, depth) {
|
|
1936
|
+
const subFields = field.fields ?? [];
|
|
1937
|
+
const subInitial = initial && typeof initial === "object" && !Array.isArray(initial) ? initial : {};
|
|
1938
|
+
const fieldset = document.createElement("div");
|
|
1939
|
+
fieldset.style.cssText = `
|
|
1940
|
+
display: flex; flex-direction: column; gap: 2px;
|
|
1941
|
+
background: rgba(255,255,255,0.02);
|
|
1942
|
+
border: 1px solid rgba(255,255,255,0.07);
|
|
1943
|
+
border-radius: 10px;
|
|
1944
|
+
padding: 10px 10px 0;
|
|
1945
|
+
`;
|
|
1946
|
+
if (depth >= MAX_FIELD_DEPTH) {
|
|
1947
|
+
const note = document.createElement("div");
|
|
1948
|
+
note.style.cssText = `font-size: 11px; color: rgba(255,255,255,0.4); padding-bottom: 10px;`;
|
|
1949
|
+
note.textContent = "Nesting too deep to edit here.";
|
|
1950
|
+
fieldset.appendChild(note);
|
|
1951
|
+
return { control: fieldset, getValue: () => ({}), validate: () => ({ value: {}, ok: true }) };
|
|
1952
|
+
}
|
|
1953
|
+
const childStates = [];
|
|
1954
|
+
for (const sub of subFields) {
|
|
1955
|
+
const { wrapper, fieldState } = renderField(sub, subInitial[sub.name], depth + 1);
|
|
1956
|
+
fieldset.appendChild(wrapper);
|
|
1957
|
+
childStates.push(fieldState);
|
|
1958
|
+
}
|
|
1959
|
+
const collect = () => {
|
|
1960
|
+
const out = {};
|
|
1961
|
+
for (const c of childStates) {
|
|
1962
|
+
const v = c.getValue();
|
|
1963
|
+
if (v !== void 0 && v !== "") out[c.field.name] = v;
|
|
1964
|
+
}
|
|
1965
|
+
return out;
|
|
1966
|
+
};
|
|
1967
|
+
return {
|
|
1968
|
+
control: fieldset,
|
|
1969
|
+
getValue: () => collect(),
|
|
1970
|
+
validate: () => {
|
|
1971
|
+
setOwnError(null);
|
|
1972
|
+
let ok = true;
|
|
1973
|
+
for (const c of childStates) {
|
|
1974
|
+
const res = c.validate();
|
|
1975
|
+
if (!res.ok) ok = false;
|
|
1976
|
+
}
|
|
1977
|
+
return { value: collect(), ok };
|
|
1978
|
+
}
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
function renderReferenceField(field, initial) {
|
|
1982
|
+
const currentId = typeof initial === "string" ? initial : "";
|
|
1983
|
+
const targetList = field.referenceList;
|
|
1984
|
+
const sel = document.createElement("select");
|
|
1985
|
+
sel.className = "cancia-form-select";
|
|
1986
|
+
sel.style.cssText = `${INPUT_BASE} appearance: none; padding-right: 30px; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'><path d='M2.5 3.75l2.5 2.5 2.5-2.5' stroke='rgba(255,255,255,0.4)' stroke-width='1.3' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>"); background-repeat: no-repeat; background-position: right 10px center; cursor: pointer;`;
|
|
1987
|
+
const opt = (value, text, selected = false) => {
|
|
1988
|
+
const o = document.createElement("option");
|
|
1989
|
+
o.value = value;
|
|
1990
|
+
o.textContent = text;
|
|
1991
|
+
if (selected) o.selected = true;
|
|
1992
|
+
return o;
|
|
1993
|
+
};
|
|
1994
|
+
const loadingOpt = opt("", "Loading\u2026");
|
|
1995
|
+
loadingOpt.disabled = true;
|
|
1996
|
+
sel.appendChild(loadingOpt);
|
|
1997
|
+
if (currentId) sel.appendChild(opt(currentId, `Current (${currentId})`, true));
|
|
1998
|
+
const getValue2 = () => sel.value === "" ? void 0 : sel.value;
|
|
1999
|
+
if (!targetList) {
|
|
2000
|
+
sel.innerHTML = "";
|
|
2001
|
+
const note = opt("", "No target list configured");
|
|
2002
|
+
note.disabled = true;
|
|
2003
|
+
sel.appendChild(note);
|
|
2004
|
+
return { control: sel, getValue: getValue2 };
|
|
2005
|
+
}
|
|
2006
|
+
const titleField = state.schemas[targetList]?.titleField;
|
|
2007
|
+
const titleOf = (entry) => {
|
|
2008
|
+
if (titleField) {
|
|
2009
|
+
const v = entry.data[titleField];
|
|
2010
|
+
if (typeof v === "string" && v.trim()) return v;
|
|
2011
|
+
}
|
|
2012
|
+
return `(untitled \xB7 ${entry.id})`;
|
|
2013
|
+
};
|
|
2014
|
+
void (async () => {
|
|
2015
|
+
let entries = [];
|
|
2016
|
+
let failed = false;
|
|
2017
|
+
try {
|
|
2018
|
+
entries = await fetchList(targetList, state.activeListLocale || state.activeLang || void 0);
|
|
2019
|
+
} catch {
|
|
2020
|
+
failed = true;
|
|
2021
|
+
}
|
|
2022
|
+
sel.innerHTML = "";
|
|
2023
|
+
if (failed) {
|
|
2024
|
+
const errOpt = opt("", "Failed to load options");
|
|
2025
|
+
errOpt.disabled = true;
|
|
2026
|
+
sel.appendChild(errOpt);
|
|
2027
|
+
if (currentId) sel.appendChild(opt(currentId, `Current (${currentId})`, true));
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
if (!field.required) sel.appendChild(opt("", "\u2014 none \u2014", currentId === ""));
|
|
2031
|
+
let matched = false;
|
|
2032
|
+
for (const entry of entries) {
|
|
2033
|
+
const isCurrent = entry.id === currentId;
|
|
2034
|
+
if (isCurrent) matched = true;
|
|
2035
|
+
sel.appendChild(opt(entry.id, titleOf(entry), isCurrent));
|
|
2036
|
+
}
|
|
2037
|
+
if (currentId && !matched) {
|
|
2038
|
+
sel.appendChild(opt(currentId, `\u26A0 missing (${currentId})`, true));
|
|
2039
|
+
}
|
|
2040
|
+
})();
|
|
2041
|
+
return { control: sel, getValue: getValue2 };
|
|
2042
|
+
}
|
|
2043
|
+
function renderRichTextField(field, initial, setOwnError) {
|
|
2044
|
+
const STYLE_LABELS = {
|
|
2045
|
+
normal: "Normal",
|
|
2046
|
+
h2: "Heading 2",
|
|
2047
|
+
h3: "Heading 3",
|
|
2048
|
+
blockquote: "Quote"
|
|
2049
|
+
};
|
|
2050
|
+
const LIST_LABELS = [
|
|
2051
|
+
{ value: "", label: "No list" },
|
|
2052
|
+
{ value: "bullet", label: "Bulleted" },
|
|
2053
|
+
{ value: "number", label: "Numbered" }
|
|
2054
|
+
];
|
|
2055
|
+
const container = document.createElement("div");
|
|
2056
|
+
container.style.cssText = `
|
|
2057
|
+
display: flex; flex-direction: column; gap: 8px;
|
|
2058
|
+
background: rgba(255,255,255,0.02);
|
|
2059
|
+
border: 1px solid rgba(255,255,255,0.07);
|
|
2060
|
+
border-radius: 10px;
|
|
2061
|
+
padding: 10px;
|
|
2062
|
+
`;
|
|
2063
|
+
const rowsWrap = document.createElement("div");
|
|
2064
|
+
rowsWrap.style.cssText = `display: flex; flex-direction: column; gap: 8px;`;
|
|
2065
|
+
container.appendChild(rowsWrap);
|
|
2066
|
+
const rows = [];
|
|
2067
|
+
let dragging = null;
|
|
2068
|
+
function makeRow(initialRow) {
|
|
2069
|
+
const row = document.createElement("div");
|
|
2070
|
+
row.style.cssText = `
|
|
2071
|
+
display: flex; align-items: flex-start; gap: 8px;
|
|
2072
|
+
background: rgba(255,255,255,0.02);
|
|
2073
|
+
border: 1px solid rgba(255,255,255,0.06);
|
|
2074
|
+
border-radius: 8px;
|
|
2075
|
+
padding: 8px;
|
|
2076
|
+
`;
|
|
2077
|
+
const handle = document.createElement("div");
|
|
2078
|
+
handle.textContent = "\u22EE\u22EE";
|
|
2079
|
+
handle.title = "Drag to reorder";
|
|
2080
|
+
handle.draggable = true;
|
|
2081
|
+
handle.style.cssText = `
|
|
2082
|
+
cursor: grab; user-select: none;
|
|
2083
|
+
color: rgba(255,255,255,0.35);
|
|
2084
|
+
font-size: 13px; line-height: 1.2;
|
|
2085
|
+
padding: 4px 2px; flex-shrink: 0;
|
|
2086
|
+
letter-spacing: -2px;
|
|
2087
|
+
`;
|
|
2088
|
+
const main = document.createElement("div");
|
|
2089
|
+
main.style.cssText = `flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px;`;
|
|
2090
|
+
const ta = document.createElement("textarea");
|
|
2091
|
+
ta.className = "cancia-form-textarea";
|
|
2092
|
+
ta.style.cssText = `${INPUT_BASE} resize: vertical; min-height: 54px; max-height: 240px; caret-color: ${accent4()};`;
|
|
2093
|
+
ta.rows = 2;
|
|
2094
|
+
ta.placeholder = "Text \u2014 use **bold**, *italic*, [label](https://\u2026)";
|
|
2095
|
+
ta.value = initialRow.text;
|
|
2096
|
+
const controls = document.createElement("div");
|
|
2097
|
+
controls.style.cssText = `display: flex; gap: 6px;`;
|
|
2098
|
+
const styleSel = document.createElement("select");
|
|
2099
|
+
styleSel.className = "cancia-form-select";
|
|
2100
|
+
styleSel.style.cssText = `${INPUT_BASE} width: auto; flex: 1; appearance: none; padding: 5px 26px 5px 9px; font-size: 11px; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'><path d='M2.5 3.75l2.5 2.5 2.5-2.5' stroke='rgba(255,255,255,0.4)' stroke-width='1.3' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>"); background-repeat: no-repeat; background-position: right 9px center; cursor: pointer;`;
|
|
2101
|
+
for (const s of PT_STYLES) {
|
|
2102
|
+
const o = document.createElement("option");
|
|
2103
|
+
o.value = s;
|
|
2104
|
+
o.textContent = STYLE_LABELS[s];
|
|
2105
|
+
if (initialRow.style === s) o.selected = true;
|
|
2106
|
+
styleSel.appendChild(o);
|
|
2107
|
+
}
|
|
2108
|
+
const listSel = document.createElement("select");
|
|
2109
|
+
listSel.className = "cancia-form-select";
|
|
2110
|
+
listSel.style.cssText = styleSel.style.cssText;
|
|
2111
|
+
for (const { value, label } of LIST_LABELS) {
|
|
2112
|
+
const o = document.createElement("option");
|
|
2113
|
+
o.value = value;
|
|
2114
|
+
o.textContent = label;
|
|
2115
|
+
if ((initialRow.listItem ?? "") === value) o.selected = true;
|
|
2116
|
+
listSel.appendChild(o);
|
|
2117
|
+
}
|
|
2118
|
+
controls.appendChild(styleSel);
|
|
2119
|
+
controls.appendChild(listSel);
|
|
2120
|
+
main.appendChild(ta);
|
|
2121
|
+
main.appendChild(controls);
|
|
2122
|
+
const removeBtn = document.createElement("button");
|
|
2123
|
+
removeBtn.type = "button";
|
|
2124
|
+
removeBtn.textContent = "\xD7";
|
|
2125
|
+
removeBtn.title = "Remove block";
|
|
2126
|
+
removeBtn.style.cssText = `
|
|
2127
|
+
appearance: none; cursor: pointer; flex-shrink: 0;
|
|
2128
|
+
background: transparent; border: 1px solid transparent;
|
|
2129
|
+
color: rgba(255,135,134,0.7);
|
|
2130
|
+
font-size: 16px; line-height: 1;
|
|
2131
|
+
padding: 2px 7px; border-radius: 6px;
|
|
2132
|
+
transition: background 0.15s;
|
|
2133
|
+
`;
|
|
2134
|
+
removeBtn.addEventListener("mouseenter", () => {
|
|
2135
|
+
removeBtn.style.background = "rgba(255,135,134,0.1)";
|
|
2136
|
+
});
|
|
2137
|
+
removeBtn.addEventListener("mouseleave", () => {
|
|
2138
|
+
removeBtn.style.background = "transparent";
|
|
2139
|
+
});
|
|
2140
|
+
row.appendChild(handle);
|
|
2141
|
+
row.appendChild(main);
|
|
2142
|
+
row.appendChild(removeBtn);
|
|
2143
|
+
const rec = {
|
|
2144
|
+
el: row,
|
|
2145
|
+
read: () => {
|
|
2146
|
+
const style = styleSel.value ?? "normal";
|
|
2147
|
+
const listValue = listSel.value;
|
|
2148
|
+
const out = { text: ta.value, style };
|
|
2149
|
+
if (listValue) out.listItem = listValue;
|
|
2150
|
+
return out;
|
|
2151
|
+
}
|
|
2152
|
+
};
|
|
2153
|
+
removeBtn.addEventListener("click", () => {
|
|
2154
|
+
const i = rows.indexOf(rec);
|
|
2155
|
+
if (i >= 0) rows.splice(i, 1);
|
|
2156
|
+
row.remove();
|
|
2157
|
+
setOwnError(null);
|
|
2158
|
+
});
|
|
2159
|
+
handle.addEventListener("dragstart", (e) => {
|
|
2160
|
+
dragging = rec;
|
|
2161
|
+
handle.style.cursor = "grabbing";
|
|
2162
|
+
row.style.opacity = "0.5";
|
|
2163
|
+
e.dataTransfer?.setData("text/plain", "");
|
|
2164
|
+
if (e.dataTransfer) e.dataTransfer.effectAllowed = "move";
|
|
2165
|
+
});
|
|
2166
|
+
handle.addEventListener("dragend", () => {
|
|
2167
|
+
handle.style.cursor = "grab";
|
|
2168
|
+
row.style.opacity = "1";
|
|
2169
|
+
dragging = null;
|
|
2170
|
+
});
|
|
2171
|
+
row.addEventListener("dragover", (e) => {
|
|
2172
|
+
if (!dragging || dragging === rec) return;
|
|
2173
|
+
e.preventDefault();
|
|
2174
|
+
const rect = row.getBoundingClientRect();
|
|
2175
|
+
const after = e.clientY > rect.top + rect.height / 2;
|
|
2176
|
+
const from = rows.indexOf(dragging);
|
|
2177
|
+
let to = rows.indexOf(rec);
|
|
2178
|
+
if (from < 0 || to < 0) return;
|
|
2179
|
+
if (after) to += 1;
|
|
2180
|
+
if (from < to) to -= 1;
|
|
2181
|
+
if (from === to) return;
|
|
2182
|
+
rows.splice(from, 1);
|
|
2183
|
+
rows.splice(to, 0, dragging);
|
|
2184
|
+
rowsWrap.insertBefore(dragging.el, after ? row.nextSibling : row);
|
|
2185
|
+
});
|
|
2186
|
+
return rec;
|
|
2187
|
+
}
|
|
2188
|
+
function addRow(initialRow) {
|
|
2189
|
+
const rec = makeRow(initialRow);
|
|
2190
|
+
rows.push(rec);
|
|
2191
|
+
rowsWrap.appendChild(rec.el);
|
|
2192
|
+
}
|
|
2193
|
+
const initialRows = (() => {
|
|
2194
|
+
const parsed = portableTextSubsetSchema.safeParse(initial);
|
|
2195
|
+
if (parsed.success && parsed.data.length > 0) return portableTextToRows(parsed.data);
|
|
2196
|
+
return [{ text: "", style: "normal" }];
|
|
2197
|
+
})();
|
|
2198
|
+
for (const r of initialRows) addRow(r);
|
|
2199
|
+
const addBtn = document.createElement("button");
|
|
2200
|
+
addBtn.type = "button";
|
|
2201
|
+
addBtn.textContent = "+ Add block";
|
|
2202
|
+
addBtn.style.cssText = `
|
|
2203
|
+
appearance: none; cursor: pointer; align-self: flex-start;
|
|
2204
|
+
background: rgba(255,255,255,0.05); color: rgba(255,255,255,0.8);
|
|
2205
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
2206
|
+
font-size: 11px; font-weight: 500;
|
|
2207
|
+
padding: 6px 11px; border-radius: 7px;
|
|
2208
|
+
transition: background 0.15s;
|
|
2209
|
+
`;
|
|
2210
|
+
addBtn.addEventListener("mouseenter", () => {
|
|
2211
|
+
addBtn.style.background = "rgba(255,255,255,0.1)";
|
|
2212
|
+
});
|
|
2213
|
+
addBtn.addEventListener("mouseleave", () => {
|
|
2214
|
+
addBtn.style.background = "rgba(255,255,255,0.05)";
|
|
2215
|
+
});
|
|
2216
|
+
addBtn.addEventListener("click", () => addRow({ text: "", style: "normal" }));
|
|
2217
|
+
container.appendChild(addBtn);
|
|
2218
|
+
const serialise = () => {
|
|
2219
|
+
const editorRows = rows.map((r) => r.read()).filter((r) => r.text.trim() !== "");
|
|
2220
|
+
return rowsToPortableText(editorRows);
|
|
2221
|
+
};
|
|
2222
|
+
return {
|
|
2223
|
+
control: container,
|
|
2224
|
+
getValue: () => serialise(),
|
|
2225
|
+
validate: () => {
|
|
2226
|
+
setOwnError(null);
|
|
2227
|
+
const value = serialise();
|
|
2228
|
+
const parsed = portableTextSubsetSchema.safeParse(value);
|
|
2229
|
+
if (!parsed.success) {
|
|
2230
|
+
setOwnError("This rich-text content is not valid. Check links and formatting.");
|
|
2231
|
+
return { value, ok: false };
|
|
2232
|
+
}
|
|
2233
|
+
if (field.required && value.length === 0) {
|
|
2234
|
+
setOwnError(`${field.label || "This field"} is required`);
|
|
2235
|
+
return { value, ok: false };
|
|
2236
|
+
}
|
|
2237
|
+
return { value: parsed.data, ok: true };
|
|
2238
|
+
}
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
function defaultForField(field) {
|
|
2242
|
+
if (field.widget === "array" || field.widget === "richtext") return [];
|
|
2243
|
+
if (field.widget === "object") {
|
|
2244
|
+
const out = {};
|
|
2245
|
+
for (const sub of field.fields ?? []) {
|
|
2246
|
+
const d = defaultForField(sub);
|
|
2247
|
+
if (d !== void 0) out[sub.name] = d;
|
|
2248
|
+
}
|
|
2249
|
+
return out;
|
|
2250
|
+
}
|
|
2251
|
+
if (field.widget === "checkbox") return false;
|
|
2252
|
+
return "";
|
|
1587
2253
|
}
|
|
1588
2254
|
function preValidate(fieldStates) {
|
|
1589
2255
|
const data = {};
|
|
1590
2256
|
let ok = true;
|
|
1591
2257
|
for (const f of fieldStates) {
|
|
1592
|
-
const value = f.
|
|
1593
|
-
|
|
1594
|
-
if (f.field.required && (value === void 0 || value === "" || value === null)) {
|
|
1595
|
-
f.setError(`${f.field.label} is required`);
|
|
2258
|
+
const { value, ok: fieldOk } = f.validate();
|
|
2259
|
+
if (!fieldOk) {
|
|
1596
2260
|
ok = false;
|
|
1597
2261
|
continue;
|
|
1598
2262
|
}
|
|
1599
|
-
if (
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
2263
|
+
if (f.field.widget === "array" || f.field.widget === "object" || f.field.widget === "richtext") {
|
|
2264
|
+
data[f.field.name] = value;
|
|
2265
|
+
} else if (value !== void 0 && value !== "") {
|
|
2266
|
+
data[f.field.name] = value;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
return { data, ok };
|
|
2270
|
+
}
|
|
2271
|
+
function validateValue(field, value) {
|
|
2272
|
+
if (value === void 0 || value === null || value === "") return null;
|
|
2273
|
+
if (typeof value === "string") {
|
|
2274
|
+
if (field.minLength !== void 0 && value.length < field.minLength) {
|
|
2275
|
+
return `Must be at least ${field.minLength} character${field.minLength === 1 ? "" : "s"}`;
|
|
2276
|
+
}
|
|
2277
|
+
if (field.maxLength !== void 0 && value.length > field.maxLength) {
|
|
2278
|
+
return `Must be at most ${field.maxLength} characters`;
|
|
2279
|
+
}
|
|
2280
|
+
if (field.pattern !== void 0) {
|
|
2281
|
+
let re = null;
|
|
2282
|
+
try {
|
|
2283
|
+
re = new RegExp(field.pattern);
|
|
2284
|
+
} catch {
|
|
2285
|
+
re = null;
|
|
1604
2286
|
}
|
|
1605
|
-
if (
|
|
1606
|
-
|
|
1607
|
-
ok = false;
|
|
1608
|
-
continue;
|
|
2287
|
+
if (re && !re.test(value)) {
|
|
2288
|
+
return "Invalid format";
|
|
1609
2289
|
}
|
|
1610
2290
|
}
|
|
1611
|
-
if (
|
|
1612
|
-
|
|
2291
|
+
if (field.options && field.options.length > 0 && !field.options.includes(value)) {
|
|
2292
|
+
return "Choose one of the allowed options";
|
|
2293
|
+
}
|
|
2294
|
+
if (field.widget === "email" && !isLikelyEmail(value)) {
|
|
2295
|
+
return "Enter a valid email address";
|
|
2296
|
+
}
|
|
2297
|
+
if ((field.widget === "url" || field.widget === "image") && !isLikelyUrl(value)) {
|
|
2298
|
+
return "Enter a valid URL";
|
|
1613
2299
|
}
|
|
1614
2300
|
}
|
|
1615
|
-
|
|
2301
|
+
if (typeof value === "number") {
|
|
2302
|
+
if (field.min !== void 0 && value < field.min) {
|
|
2303
|
+
return `Must be at least ${field.min}`;
|
|
2304
|
+
}
|
|
2305
|
+
if (field.max !== void 0 && value > field.max) {
|
|
2306
|
+
return `Must be at most ${field.max}`;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
return null;
|
|
2310
|
+
}
|
|
2311
|
+
function isLikelyEmail(s) {
|
|
2312
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s);
|
|
2313
|
+
}
|
|
2314
|
+
function isLikelyUrl(s) {
|
|
2315
|
+
try {
|
|
2316
|
+
new URL(s);
|
|
2317
|
+
return true;
|
|
2318
|
+
} catch {
|
|
2319
|
+
return false;
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
function wireSlugAutoFill(schema, fieldStates, sourceEntry) {
|
|
2323
|
+
const byName = new Map(fieldStates.map((f) => [f.field.name, f]));
|
|
2324
|
+
for (const slug of fieldStates) {
|
|
2325
|
+
if (slug.field.widget !== "slug") continue;
|
|
2326
|
+
const sourceName = slug.field.source;
|
|
2327
|
+
if (!sourceName) continue;
|
|
2328
|
+
const src = byName.get(sourceName);
|
|
2329
|
+
if (!src || !src.onInput || !slug.setValue) continue;
|
|
2330
|
+
const existing = sourceEntry?.data[slug.field.name];
|
|
2331
|
+
let dirty = typeof existing === "string" && existing.trim().length > 0;
|
|
2332
|
+
slug.onInput?.(() => {
|
|
2333
|
+
dirty = true;
|
|
2334
|
+
});
|
|
2335
|
+
src.onInput(() => {
|
|
2336
|
+
if (dirty) return;
|
|
2337
|
+
const sv = src.getValue();
|
|
2338
|
+
slug.setValue?.(typeof sv === "string" ? slugify(sv) : "");
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
1616
2341
|
}
|
|
1617
2342
|
function openEntryModal(opts) {
|
|
1618
2343
|
closeEntryModal();
|
|
@@ -1741,6 +2466,7 @@ function openEntryModal(opts) {
|
|
|
1741
2466
|
body.appendChild(wrapper);
|
|
1742
2467
|
fieldStates.push(fieldState);
|
|
1743
2468
|
}
|
|
2469
|
+
wireSlugAutoFill(opts.schema, fieldStates, sourceForInitial);
|
|
1744
2470
|
const footer = document.createElement("div");
|
|
1745
2471
|
footer.style.cssText = `
|
|
1746
2472
|
display: flex; align-items: center; justify-content: space-between;
|