@usefillo/dom 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/index.js +297 -91
- package/dist/standalone.global.js +1 -1
- package/dist/styles.css +157 -25
- package/dist/styles.unlayered.css +157 -25
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -34,11 +34,14 @@ import {
|
|
|
34
34
|
needsExplicitSubmit,
|
|
35
35
|
radioGroupStep,
|
|
36
36
|
reachablePageSequence,
|
|
37
|
+
REQUIRED_FIELD_MESSAGE,
|
|
38
|
+
requiredFieldMessage,
|
|
37
39
|
resolveThemeAppearance,
|
|
38
40
|
shouldAutoSubmit,
|
|
39
41
|
syncCodeForm,
|
|
40
42
|
toE164,
|
|
41
|
-
visibleFields
|
|
43
|
+
visibleFields,
|
|
44
|
+
visibleGroupChildren
|
|
42
45
|
} from "@usefillo/core";
|
|
43
46
|
var FIELD_INPUT_PREFIX = "fillo-";
|
|
44
47
|
var domInstanceCounter = 0;
|
|
@@ -46,6 +49,10 @@ var FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), input:not([disabled])
|
|
|
46
49
|
function focusableWithin(node) {
|
|
47
50
|
return node.matches(FOCUSABLE_SELECTOR) ? node : node.querySelector(FOCUSABLE_SELECTOR);
|
|
48
51
|
}
|
|
52
|
+
function firstChildControl(card) {
|
|
53
|
+
const focusable = Array.from(card.querySelectorAll(FOCUSABLE_SELECTOR));
|
|
54
|
+
return focusable.find((node) => !node.closest(".fillo-group-instance-header")) ?? null;
|
|
55
|
+
}
|
|
49
56
|
function fieldIds(instanceId, fieldId) {
|
|
50
57
|
const base = `${FIELD_INPUT_PREFIX}${instanceId}-${fieldId}`;
|
|
51
58
|
return {
|
|
@@ -159,7 +166,8 @@ function shell(field, error, child, ids) {
|
|
|
159
166
|
text: field.label,
|
|
160
167
|
attrs: { id: ids.labelId, for: ids.inputId, "data-fillo": "label" }
|
|
161
168
|
});
|
|
162
|
-
|
|
169
|
+
const optionalMarker = !field.required && !(field.kind === "repeating_group" && (field.minInstances ?? 1) > 0);
|
|
170
|
+
if (optionalMarker)
|
|
163
171
|
label.appendChild(el("span", { className: "fillo-optional", text: " (optional)" }));
|
|
164
172
|
wrap.appendChild(label);
|
|
165
173
|
if (field.description)
|
|
@@ -942,7 +950,7 @@ function checkbox(context) {
|
|
|
942
950
|
el("p", {
|
|
943
951
|
className: "fillo-error",
|
|
944
952
|
text: context.error,
|
|
945
|
-
attrs: {
|
|
953
|
+
attrs: { id: context.ids.errorId, "data-fillo": "error" }
|
|
946
954
|
})
|
|
947
955
|
);
|
|
948
956
|
return wrap;
|
|
@@ -1306,8 +1314,47 @@ function formatBytes(bytes) {
|
|
|
1306
1314
|
if (bytes < 1024 ** 3) return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
|
|
1307
1315
|
return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
|
|
1308
1316
|
}
|
|
1309
|
-
function
|
|
1310
|
-
const
|
|
1317
|
+
function uploadSvg(paths, size = 22) {
|
|
1318
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
1319
|
+
svg.setAttribute("class", "fillo-file-state-icon");
|
|
1320
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
1321
|
+
svg.setAttribute("width", String(size));
|
|
1322
|
+
svg.setAttribute("height", String(size));
|
|
1323
|
+
svg.setAttribute("fill", "none");
|
|
1324
|
+
svg.setAttribute("aria-hidden", "true");
|
|
1325
|
+
svg.setAttribute("focusable", "false");
|
|
1326
|
+
for (const d of paths) {
|
|
1327
|
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1328
|
+
path.setAttribute("d", d);
|
|
1329
|
+
svg.appendChild(path);
|
|
1330
|
+
}
|
|
1331
|
+
return svg;
|
|
1332
|
+
}
|
|
1333
|
+
function fileStateIcon(state) {
|
|
1334
|
+
const mark = el("span", {
|
|
1335
|
+
className: `fillo-file-state fillo-file-state--${state}`,
|
|
1336
|
+
attrs: { "aria-hidden": "true" }
|
|
1337
|
+
});
|
|
1338
|
+
mark.appendChild(
|
|
1339
|
+
state === "done" ? uploadSvg([
|
|
1340
|
+
"M3.75 12a8.25 8.25 0 1 0 16.5 0 8.25 8.25 0 1 0-16.5 0",
|
|
1341
|
+
"m8.5 12 2.25 2.25 4.75-5"
|
|
1342
|
+
]) : state === "failed" ? uploadSvg([
|
|
1343
|
+
"M3.75 12a8.25 8.25 0 1 0 16.5 0 8.25 8.25 0 1 0-16.5 0",
|
|
1344
|
+
"M12 7.75v5.5",
|
|
1345
|
+
"M12 16.5h.01"
|
|
1346
|
+
]) : uploadSvg(["M7.25 3.75h6.5l3 3v13.5h-9.5z", "M13.75 3.75v3h3"])
|
|
1347
|
+
);
|
|
1348
|
+
return mark;
|
|
1349
|
+
}
|
|
1350
|
+
function closeIcon() {
|
|
1351
|
+
const icon = uploadSvg(["m7 7 10 10M17 7 7 17"], 16);
|
|
1352
|
+
icon.setAttribute("class", "fillo-file-action-icon");
|
|
1353
|
+
return icon;
|
|
1354
|
+
}
|
|
1355
|
+
function fileActionBtn(className, label, content, onClick) {
|
|
1356
|
+
const btn = el("button", { className, attrs: { type: "button", "aria-label": label } });
|
|
1357
|
+
btn.append(content);
|
|
1311
1358
|
btn.addEventListener("click", onClick);
|
|
1312
1359
|
return btn;
|
|
1313
1360
|
}
|
|
@@ -1418,35 +1465,39 @@ function fileUpload(context) {
|
|
|
1418
1465
|
const list = el("ul", { className: "fillo-files" });
|
|
1419
1466
|
for (const file of done) {
|
|
1420
1467
|
const li = el("li", { className: "fillo-file fillo-file--done" });
|
|
1421
|
-
|
|
1468
|
+
const content = el("span", { className: "fillo-file-content" });
|
|
1469
|
+
appendChildren(content, [
|
|
1422
1470
|
el("span", { className: "fillo-file-name", text: file.name }),
|
|
1423
|
-
el("span", {
|
|
1424
|
-
|
|
1471
|
+
el("span", {
|
|
1472
|
+
className: "fillo-file-meta",
|
|
1473
|
+
text: DEFAULT_FIELD_STRINGS.uploadedFile(formatBytes(file.size))
|
|
1474
|
+
})
|
|
1475
|
+
]);
|
|
1476
|
+
const actions = el("span", { className: "fillo-file-actions" });
|
|
1477
|
+
actions.appendChild(
|
|
1425
1478
|
fileActionBtn(
|
|
1426
1479
|
"fillo-file-remove",
|
|
1427
|
-
|
|
1428
|
-
|
|
1480
|
+
`${DEFAULT_FIELD_STRINGS.uploadRemove} ${file.name}`,
|
|
1481
|
+
closeIcon(),
|
|
1429
1482
|
() => context.setValue(done.filter((f) => f.fileId !== file.fileId))
|
|
1430
1483
|
)
|
|
1431
|
-
|
|
1484
|
+
);
|
|
1485
|
+
appendChildren(li, [fileStateIcon("done"), content, actions]);
|
|
1432
1486
|
list.appendChild(li);
|
|
1433
1487
|
}
|
|
1434
1488
|
for (const row of rows) {
|
|
1435
1489
|
const failed = row.error !== void 0;
|
|
1436
1490
|
const li = el("li", {
|
|
1437
|
-
className: `fillo-file${failed ? " fillo-file--failed" : ""}
|
|
1438
|
-
attrs: failed ? {} : {
|
|
1439
|
-
role: "progressbar",
|
|
1440
|
-
"aria-label": `Uploading ${row.name}`,
|
|
1441
|
-
"aria-valuemin": "0",
|
|
1442
|
-
"aria-valuemax": "100",
|
|
1443
|
-
"aria-valuenow": String(Math.round(row.fraction * 100))
|
|
1444
|
-
}
|
|
1491
|
+
className: `fillo-file${failed ? " fillo-file--failed" : ""}`
|
|
1445
1492
|
});
|
|
1446
|
-
li.appendChild(
|
|
1493
|
+
li.appendChild(fileStateIcon(failed ? "failed" : "uploading"));
|
|
1494
|
+
const content = el("span", { className: "fillo-file-content" });
|
|
1495
|
+
content.appendChild(el("span", { className: "fillo-file-name", text: row.name }));
|
|
1447
1496
|
if (failed) {
|
|
1448
1497
|
const actions = el("span", { className: "fillo-file-actions" });
|
|
1449
|
-
|
|
1498
|
+
content.appendChild(el("span", { className: "fillo-file-error", text: row.error ?? "" }));
|
|
1499
|
+
li.appendChild(content);
|
|
1500
|
+
if (!row.tooLarge && remaining > 0) {
|
|
1450
1501
|
actions.appendChild(
|
|
1451
1502
|
fileActionBtn(
|
|
1452
1503
|
"fillo-file-retry",
|
|
@@ -1459,29 +1510,47 @@ function fileUpload(context) {
|
|
|
1459
1510
|
actions.appendChild(
|
|
1460
1511
|
fileActionBtn(
|
|
1461
1512
|
"fillo-file-remove",
|
|
1462
|
-
|
|
1463
|
-
|
|
1513
|
+
`${DEFAULT_FIELD_STRINGS.uploadDismiss} ${row.name}`,
|
|
1514
|
+
closeIcon(),
|
|
1464
1515
|
() => internal.uploadAction?.(row.key, "dismiss")
|
|
1465
1516
|
)
|
|
1466
1517
|
);
|
|
1467
1518
|
li.appendChild(actions);
|
|
1468
|
-
li.appendChild(el("span", { className: "fillo-file-error", text: row.error ?? "" }));
|
|
1469
1519
|
} else {
|
|
1470
1520
|
const bar = el("span", { className: "fillo-progress-bar" });
|
|
1471
1521
|
bar.style.width = `${row.fraction * 100}%`;
|
|
1472
|
-
|
|
1522
|
+
content.appendChild(
|
|
1473
1523
|
el("span", {
|
|
1474
1524
|
className: "fillo-file-meta",
|
|
1475
|
-
text:
|
|
1476
|
-
|
|
1525
|
+
text: DEFAULT_FIELD_STRINGS.uploadingFile(
|
|
1526
|
+
Math.round(row.fraction * 100),
|
|
1527
|
+
formatBytes(row.size)
|
|
1528
|
+
)
|
|
1529
|
+
})
|
|
1530
|
+
);
|
|
1531
|
+
const actions = el("span", { className: "fillo-file-actions" });
|
|
1532
|
+
actions.appendChild(
|
|
1477
1533
|
fileActionBtn(
|
|
1478
1534
|
"fillo-file-remove",
|
|
1479
|
-
|
|
1480
|
-
|
|
1535
|
+
`${DEFAULT_FIELD_STRINGS.uploadCancel} ${row.name}`,
|
|
1536
|
+
closeIcon(),
|
|
1481
1537
|
() => internal.uploadAction?.(row.key, "cancel")
|
|
1482
|
-
)
|
|
1538
|
+
)
|
|
1539
|
+
);
|
|
1540
|
+
appendChildren(li, [
|
|
1541
|
+
content,
|
|
1542
|
+
actions,
|
|
1483
1543
|
appendChildren(
|
|
1484
|
-
el("span", {
|
|
1544
|
+
el("span", {
|
|
1545
|
+
className: "fillo-progress",
|
|
1546
|
+
attrs: {
|
|
1547
|
+
role: "progressbar",
|
|
1548
|
+
"aria-label": `Uploading ${row.name}`,
|
|
1549
|
+
"aria-valuemin": "0",
|
|
1550
|
+
"aria-valuemax": "100",
|
|
1551
|
+
"aria-valuenow": String(Math.round(row.fraction * 100))
|
|
1552
|
+
}
|
|
1553
|
+
}),
|
|
1485
1554
|
[bar]
|
|
1486
1555
|
)
|
|
1487
1556
|
]);
|
|
@@ -1543,6 +1612,123 @@ function displayOptions(field) {
|
|
|
1543
1612
|
const shuffled = order.flatMap((id) => byId.get(id) ?? []);
|
|
1544
1613
|
return shuffled.length === field.options.length ? shuffled : field.options;
|
|
1545
1614
|
}
|
|
1615
|
+
function groupInstances(context) {
|
|
1616
|
+
const fresh = context.getValue?.() ?? context.value;
|
|
1617
|
+
return Array.isArray(fresh) ? fresh : [];
|
|
1618
|
+
}
|
|
1619
|
+
function groupRenderedCount(field, stored) {
|
|
1620
|
+
return Math.max(stored.length, field.minInstances ?? 1);
|
|
1621
|
+
}
|
|
1622
|
+
function groupFloor(field) {
|
|
1623
|
+
return Math.max(field.minInstances ?? 1, 0);
|
|
1624
|
+
}
|
|
1625
|
+
function materializeGroupInstances(stored, count) {
|
|
1626
|
+
const out = [...stored];
|
|
1627
|
+
while (out.length < count) out.push({});
|
|
1628
|
+
return out;
|
|
1629
|
+
}
|
|
1630
|
+
function repeatingGroup(context) {
|
|
1631
|
+
const field = context.field;
|
|
1632
|
+
if (field.kind !== "repeating_group") return null;
|
|
1633
|
+
const ictx = context;
|
|
1634
|
+
const announce = ictx.announce;
|
|
1635
|
+
const instanceId = ictx.instanceId ?? "";
|
|
1636
|
+
const item = field.itemLabel || field.label;
|
|
1637
|
+
const stored = groupInstances(context);
|
|
1638
|
+
const count = groupRenderedCount(field, stored);
|
|
1639
|
+
const floor = groupFloor(field);
|
|
1640
|
+
const wrap = el("div", {
|
|
1641
|
+
className: "fillo-group",
|
|
1642
|
+
attrs: { id: context.ids.inputId, "data-fillo": "control" }
|
|
1643
|
+
});
|
|
1644
|
+
applyFieldAria(wrap, field, context.error, context.ids, { requiredSupported: false });
|
|
1645
|
+
for (let index = 0; index < count; index++) {
|
|
1646
|
+
const instanceValues = stored[index] ?? {};
|
|
1647
|
+
const heading = DEFAULT_FIELD_STRINGS.groupInstanceLabel(item, index + 1, count);
|
|
1648
|
+
const card = el("div", {
|
|
1649
|
+
className: "fillo-group-instance",
|
|
1650
|
+
attrs: { role: "group", "aria-label": heading, tabindex: "-1" }
|
|
1651
|
+
});
|
|
1652
|
+
const header = el("div", { className: "fillo-group-instance-header" });
|
|
1653
|
+
const removeBtn = el("button", {
|
|
1654
|
+
className: "fillo-group-remove",
|
|
1655
|
+
text: "\xD7",
|
|
1656
|
+
attrs: {
|
|
1657
|
+
type: "button",
|
|
1658
|
+
"aria-label": DEFAULT_FIELD_STRINGS.groupRemoveLabel(item, index + 1)
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
const canRemove = count > floor;
|
|
1662
|
+
removeBtn.toggleAttribute("disabled", !canRemove);
|
|
1663
|
+
if (!canRemove) {
|
|
1664
|
+
removeBtn.setAttribute("aria-disabled", "true");
|
|
1665
|
+
removeBtn.setAttribute("title", `At least ${floor} ${floor === 1 ? "entry" : "entries"} required`);
|
|
1666
|
+
}
|
|
1667
|
+
removeBtn.addEventListener("click", () => {
|
|
1668
|
+
if (count <= floor) return;
|
|
1669
|
+
const next = materializeGroupInstances(groupInstances(context), count);
|
|
1670
|
+
next.splice(index, 1);
|
|
1671
|
+
context.setValue(next);
|
|
1672
|
+
ictx.setGroupFocus?.({ groupId: field.id, cardIndex: index - 1, intoControl: false });
|
|
1673
|
+
announce?.(DEFAULT_FIELD_STRINGS.groupInstanceRemoved(item, next.length));
|
|
1674
|
+
});
|
|
1675
|
+
appendChildren(header, [
|
|
1676
|
+
el("p", { className: "fillo-group-instance-title", text: heading }),
|
|
1677
|
+
removeBtn
|
|
1678
|
+
]);
|
|
1679
|
+
card.appendChild(header);
|
|
1680
|
+
for (const child of visibleGroupChildren(field, instanceValues)) {
|
|
1681
|
+
const compoundId = `${field.id}.${index}.${child.id}`;
|
|
1682
|
+
const childField = { ...child, id: compoundId };
|
|
1683
|
+
const childIds = fieldIds(instanceId, compoundId);
|
|
1684
|
+
const childSetValue = (value, options) => {
|
|
1685
|
+
const next = materializeGroupInstances(groupInstances(context), count);
|
|
1686
|
+
next[index] = { ...next[index] ?? {}, [child.id]: value };
|
|
1687
|
+
context.setValue(next, options);
|
|
1688
|
+
};
|
|
1689
|
+
const childContext = {
|
|
1690
|
+
field: childField,
|
|
1691
|
+
value: instanceValues[child.id],
|
|
1692
|
+
error: context.api.errors[compoundId],
|
|
1693
|
+
ids: childIds,
|
|
1694
|
+
api: context.api,
|
|
1695
|
+
setValue: childSetValue,
|
|
1696
|
+
getValue: () => groupInstances(context)[index]?.[child.id],
|
|
1697
|
+
announce: ictx.announce,
|
|
1698
|
+
announceAlert: ictx.announceAlert,
|
|
1699
|
+
registerOverlay: ictx.registerOverlay,
|
|
1700
|
+
activeOtherFields: ictx.activeOtherFields
|
|
1701
|
+
};
|
|
1702
|
+
const rendered = ictx.renderChildField?.(childContext) ?? defaultFieldRenderer(childContext);
|
|
1703
|
+
if (rendered) card.appendChild(rendered);
|
|
1704
|
+
}
|
|
1705
|
+
wrap.appendChild(card);
|
|
1706
|
+
}
|
|
1707
|
+
const atMax = count >= field.maxInstances;
|
|
1708
|
+
const addBtn = el("button", {
|
|
1709
|
+
className: "fillo-group-add",
|
|
1710
|
+
text: field.addLabel || DEFAULT_FIELD_STRINGS.groupAdd,
|
|
1711
|
+
attrs: { type: "button" }
|
|
1712
|
+
});
|
|
1713
|
+
addBtn.toggleAttribute("disabled", atMax);
|
|
1714
|
+
if (atMax) {
|
|
1715
|
+
addBtn.setAttribute("aria-disabled", "true");
|
|
1716
|
+
addBtn.setAttribute(
|
|
1717
|
+
"title",
|
|
1718
|
+
`Maximum ${field.maxInstances} ${field.maxInstances === 1 ? "entry" : "entries"}`
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
addBtn.addEventListener("click", () => {
|
|
1722
|
+
if (count >= field.maxInstances) return;
|
|
1723
|
+
const next = materializeGroupInstances(groupInstances(context), count);
|
|
1724
|
+
next.push({});
|
|
1725
|
+
context.setValue(next);
|
|
1726
|
+
ictx.setGroupFocus?.({ groupId: field.id, cardIndex: next.length - 1, intoControl: true });
|
|
1727
|
+
announce?.(DEFAULT_FIELD_STRINGS.groupInstanceAdded(item, next.length, next.length));
|
|
1728
|
+
});
|
|
1729
|
+
wrap.appendChild(addBtn);
|
|
1730
|
+
return shell(field, context.error, wrap, context.ids);
|
|
1731
|
+
}
|
|
1546
1732
|
var DEFAULT_COMPONENTS = {
|
|
1547
1733
|
short_text: (ctx) => textInput("text", ctx),
|
|
1548
1734
|
email: (ctx) => textInput("email", ctx),
|
|
@@ -1561,7 +1747,8 @@ var DEFAULT_COMPONENTS = {
|
|
|
1561
1747
|
matrix,
|
|
1562
1748
|
signature,
|
|
1563
1749
|
file_upload: fileUpload,
|
|
1564
|
-
calculated
|
|
1750
|
+
calculated,
|
|
1751
|
+
repeating_group: repeatingGroup
|
|
1565
1752
|
};
|
|
1566
1753
|
function defaultFieldRenderer(context) {
|
|
1567
1754
|
if (context.field.kind === "hidden") return null;
|
|
@@ -1672,7 +1859,9 @@ var DomFormController = class {
|
|
|
1672
1859
|
// exceed the limit") — plain text; announce() carries the live semantics.
|
|
1673
1860
|
uploadNotices = /* @__PURE__ */ new Map();
|
|
1674
1861
|
// Where to move focus on the next render: "error" after a failed submit/next,
|
|
1675
|
-
// "page" after navigating,
|
|
1862
|
+
// "page" after navigating, a GroupFocusIntent after a repeating-group
|
|
1863
|
+
// Add/Remove (contract decision 9), null → restore the pre-render active
|
|
1864
|
+
// control.
|
|
1676
1865
|
focusIntent = null;
|
|
1677
1866
|
// Teardown callbacks for open overlays (phone country popover) whose listeners
|
|
1678
1867
|
// live on window/document and would otherwise leak past a re-render/unmount.
|
|
@@ -2162,7 +2351,8 @@ var DomFormController = class {
|
|
|
2162
2351
|
name: row.name,
|
|
2163
2352
|
size: row.size,
|
|
2164
2353
|
fraction: row.fraction,
|
|
2165
|
-
error: row.error
|
|
2354
|
+
error: row.error,
|
|
2355
|
+
tooLarge: row.tooLarge
|
|
2166
2356
|
});
|
|
2167
2357
|
}
|
|
2168
2358
|
return rows;
|
|
@@ -2179,6 +2369,7 @@ var DomFormController = class {
|
|
|
2179
2369
|
} else if (action === "dismiss") {
|
|
2180
2370
|
this.inFlight.delete(key);
|
|
2181
2371
|
} else {
|
|
2372
|
+
if (row.tooLarge) return;
|
|
2182
2373
|
this.inFlight.delete(key);
|
|
2183
2374
|
void this.startUpload(row.fieldId, row.file, row.maxFiles);
|
|
2184
2375
|
}
|
|
@@ -2242,6 +2433,7 @@ var DomFormController = class {
|
|
|
2242
2433
|
const maxFileSizeMb = this.uploadFileSizeLimitMb ? Math.min(configuredMaxMb, this.uploadFileSizeLimitMb) : configuredMaxMb;
|
|
2243
2434
|
const maxBytes = maxFileSizeMb * 1024 * 1024;
|
|
2244
2435
|
const selected = withinLimit.filter((file) => file.size <= maxBytes);
|
|
2436
|
+
const oversizedFiles = withinLimit.filter((file) => file.size > maxBytes);
|
|
2245
2437
|
const dropped = files.length - withinLimit.length;
|
|
2246
2438
|
const oversized = withinLimit.length - selected.length;
|
|
2247
2439
|
if (dropped > 0 || oversized > 0) {
|
|
@@ -2251,17 +2443,29 @@ var DomFormController = class {
|
|
|
2251
2443
|
`You can attach up to ${maxFiles} file${maxFiles === 1 ? "" : "s"}. ${dropped} ${dropped === 1 ? "file was" : "files were"} not added.`
|
|
2252
2444
|
);
|
|
2253
2445
|
}
|
|
2254
|
-
if (oversized > 0) {
|
|
2255
|
-
notices.push(
|
|
2256
|
-
`${oversized} ${oversized === 1 ? "file exceeds" : "files exceed"} the ${maxFileSizeMb} MB limit.`
|
|
2257
|
-
);
|
|
2258
|
-
}
|
|
2259
2446
|
const text = notices.join(" ");
|
|
2260
|
-
this.uploadNotices.set(field.id, text);
|
|
2261
|
-
this.
|
|
2447
|
+
if (text) this.uploadNotices.set(field.id, text);
|
|
2448
|
+
else this.uploadNotices.delete(field.id);
|
|
2449
|
+
if (text) this.announce(text);
|
|
2262
2450
|
} else {
|
|
2263
2451
|
this.uploadNotices.delete(field.id);
|
|
2264
2452
|
}
|
|
2453
|
+
for (const file of oversizedFiles) {
|
|
2454
|
+
const message = DEFAULT_FIELD_STRINGS.fileTooLarge(maxFileSizeMb);
|
|
2455
|
+
const key = `${file.name}-${file.size}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
2456
|
+
this.inFlight.set(key, {
|
|
2457
|
+
fieldId: field.id,
|
|
2458
|
+
name: file.name,
|
|
2459
|
+
size: file.size,
|
|
2460
|
+
fraction: 0,
|
|
2461
|
+
error: message,
|
|
2462
|
+
tooLarge: true,
|
|
2463
|
+
file,
|
|
2464
|
+
controller: new AbortController(),
|
|
2465
|
+
maxFiles
|
|
2466
|
+
});
|
|
2467
|
+
this.announceAlert(message);
|
|
2468
|
+
}
|
|
2265
2469
|
if (selected.length === 0) {
|
|
2266
2470
|
this.queueRender();
|
|
2267
2471
|
return;
|
|
@@ -2436,11 +2640,6 @@ var DomFormController = class {
|
|
|
2436
2640
|
applyFocus(intent, captured) {
|
|
2437
2641
|
if (typeof document === "undefined") return;
|
|
2438
2642
|
if (intent === "error") {
|
|
2439
|
-
const summary = this.element.querySelector(".fillo-error-summary");
|
|
2440
|
-
if (summary) {
|
|
2441
|
-
summary.focus();
|
|
2442
|
-
return;
|
|
2443
|
-
}
|
|
2444
2643
|
const invalid = this.element.querySelector('[aria-invalid="true"]');
|
|
2445
2644
|
invalid && focusableWithin(invalid)?.focus();
|
|
2446
2645
|
return;
|
|
@@ -2454,8 +2653,32 @@ var DomFormController = class {
|
|
|
2454
2653
|
}
|
|
2455
2654
|
return;
|
|
2456
2655
|
}
|
|
2656
|
+
if (intent) {
|
|
2657
|
+
this.applyGroupFocus(intent);
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2457
2660
|
if (captured) this.restoreFocus(captured);
|
|
2458
2661
|
}
|
|
2662
|
+
/** Focus target after a repeating-group Add/Remove (contract decision 9),
|
|
2663
|
+
* resolved by POSITION against the tree render() just produced — unlike
|
|
2664
|
+
* ranking's restoreFocus precedent, the target card either didn't exist
|
|
2665
|
+
* yet at capture time (Add) or is a different card than whatever was
|
|
2666
|
+
* clicked (Remove), so a generic FocusDescriptor can't express either.
|
|
2667
|
+
* Falls back to the Add button, which always exists once a group has
|
|
2668
|
+
* rendered at all — never strands focus on <body>. */
|
|
2669
|
+
applyGroupFocus(intent) {
|
|
2670
|
+
const groupEl = Array.from(this.element.querySelectorAll("[data-field]")).find(
|
|
2671
|
+
(node) => node.getAttribute("data-field") === intent.groupId
|
|
2672
|
+
);
|
|
2673
|
+
const cards = groupEl ? Array.from(groupEl.querySelectorAll(".fillo-group-instance")) : [];
|
|
2674
|
+
const card = intent.cardIndex >= 0 ? cards[intent.cardIndex] : void 0;
|
|
2675
|
+
if (card) {
|
|
2676
|
+
const target = intent.intoControl ? firstChildControl(card) ?? card : card;
|
|
2677
|
+
target.focus();
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
2680
|
+
groupEl?.querySelector(".fillo-group-add")?.focus();
|
|
2681
|
+
}
|
|
2459
2682
|
handleError(error) {
|
|
2460
2683
|
if (this.destroyed) return;
|
|
2461
2684
|
this.fatalError = error;
|
|
@@ -2770,8 +2993,6 @@ var DomFormController = class {
|
|
|
2770
2993
|
})
|
|
2771
2994
|
);
|
|
2772
2995
|
}
|
|
2773
|
-
const summary = this.renderErrorSummary(api);
|
|
2774
|
-
if (summary) content.appendChild(summary);
|
|
2775
2996
|
const blocks = el("div", { className: "fillo-blocks", attrs: { "data-fillo": "blocks" } });
|
|
2776
2997
|
for (const block of api.blocks) {
|
|
2777
2998
|
const rendered = this.renderBlock(pipeBlock(block, this.mutableData, api.form), api);
|
|
@@ -2879,45 +3100,28 @@ var DomFormController = class {
|
|
|
2879
3100
|
}
|
|
2880
3101
|
return form;
|
|
2881
3102
|
}
|
|
2882
|
-
/**
|
|
2883
|
-
*
|
|
2884
|
-
*
|
|
2885
|
-
*
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
});
|
|
2905
|
-
link.addEventListener("click", (event) => {
|
|
2906
|
-
event.preventDefault();
|
|
2907
|
-
const fieldEl = Array.from(this.element.querySelectorAll("[data-field]")).find(
|
|
2908
|
-
(node) => node.getAttribute("data-field") === field.id
|
|
2909
|
-
);
|
|
2910
|
-
if (!fieldEl) return;
|
|
2911
|
-
const invalid = fieldEl.querySelector('[aria-invalid="true"]') ?? fieldEl;
|
|
2912
|
-
focusableWithin(invalid)?.focus();
|
|
2913
|
-
});
|
|
2914
|
-
const item = el("li");
|
|
2915
|
-
item.appendChild(link);
|
|
2916
|
-
list.appendChild(item);
|
|
2917
|
-
}
|
|
2918
|
-
box.appendChild(list);
|
|
2919
|
-
return box;
|
|
2920
|
-
}
|
|
3103
|
+
/** The per-kind renderer for a field: host `components`/`customComponents`
|
|
3104
|
+
* overrides win, else the shipped default. Factored out of renderBlock so
|
|
3105
|
+
* a repeating group's synthesized child contexts (renderChildField below)
|
|
3106
|
+
* dispatch through the exact SAME rule instead of a parallel, override-
|
|
3107
|
+
* blind copy — "the existing per-block path" contract decision 8 asks
|
|
3108
|
+
* group instances to render through. */
|
|
3109
|
+
fieldRenderer(field) {
|
|
3110
|
+
return field.kind === "custom" ? this.options.customComponents?.[field.component] ?? this.options.components?.custom : this.options.components?.[field.kind] ?? defaultFieldRenderer;
|
|
3111
|
+
}
|
|
3112
|
+
/** Threaded to a repeating group's synthesized child contexts so they
|
|
3113
|
+
* render through this SAME dispatch instead of the module-level default
|
|
3114
|
+
* alone (which would silently skip host per-kind overrides). */
|
|
3115
|
+
renderChildField = (context) => {
|
|
3116
|
+
return this.fieldRenderer(context.field)?.(context) ?? null;
|
|
3117
|
+
};
|
|
3118
|
+
/** Threaded to a repeating group's Add/Remove buttons: records where focus
|
|
3119
|
+
* belongs once the write they just made reaches the deferred
|
|
3120
|
+
* queueRender() rebuild — applyFocus/applyGroupFocus consume it exactly
|
|
3121
|
+
* like the "page"/"error" intents set elsewhere in this class. */
|
|
3122
|
+
setGroupFocus = (intent) => {
|
|
3123
|
+
this.focusIntent = intent;
|
|
3124
|
+
};
|
|
2921
3125
|
renderBlock(block, api) {
|
|
2922
3126
|
if (!isField(block)) return renderContent(block);
|
|
2923
3127
|
const setValue = (value, options) => {
|
|
@@ -2936,7 +3140,7 @@ var DomFormController = class {
|
|
|
2936
3140
|
const context = {
|
|
2937
3141
|
field: block,
|
|
2938
3142
|
value: api.data[block.id],
|
|
2939
|
-
error: api.errors[block.id],
|
|
3143
|
+
error: api.errors[block.id] === REQUIRED_FIELD_MESSAGE ? requiredFieldMessage(block, DEFAULT_FIELD_STRINGS) : api.errors[block.id],
|
|
2940
3144
|
ids: fieldIds(this.instanceId, block.id),
|
|
2941
3145
|
api,
|
|
2942
3146
|
setValue,
|
|
@@ -2950,10 +3154,12 @@ var DomFormController = class {
|
|
|
2950
3154
|
uploadDisabledReason: this.options.renderOnly ? "render_only" : this.storageBlocked() ? "storage_unavailable" : void 0,
|
|
2951
3155
|
storageFixUrl: this.storageBlocked() && this.devChrome() ? safeHttpUrl(this.syncWarningUrl) : null,
|
|
2952
3156
|
announce: this.announce,
|
|
2953
|
-
announceAlert: this.announceAlert
|
|
3157
|
+
announceAlert: this.announceAlert,
|
|
3158
|
+
instanceId: this.instanceId,
|
|
3159
|
+
renderChildField: this.renderChildField,
|
|
3160
|
+
setGroupFocus: this.setGroupFocus
|
|
2954
3161
|
};
|
|
2955
|
-
|
|
2956
|
-
return renderer?.(context) ?? null;
|
|
3162
|
+
return this.fieldRenderer(block)?.(context) ?? null;
|
|
2957
3163
|
}
|
|
2958
3164
|
};
|
|
2959
3165
|
function renderContent(block) {
|