@usefillo/dom 0.12.0 → 0.12.1
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 +116 -84
- package/dist/standalone.global.js +1 -1
- package/dist/styles.css +114 -25
- package/dist/styles.unlayered.css +114 -25
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -34,6 +34,8 @@ 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,
|
|
@@ -942,7 +944,7 @@ function checkbox(context) {
|
|
|
942
944
|
el("p", {
|
|
943
945
|
className: "fillo-error",
|
|
944
946
|
text: context.error,
|
|
945
|
-
attrs: {
|
|
947
|
+
attrs: { id: context.ids.errorId, "data-fillo": "error" }
|
|
946
948
|
})
|
|
947
949
|
);
|
|
948
950
|
return wrap;
|
|
@@ -1306,8 +1308,47 @@ function formatBytes(bytes) {
|
|
|
1306
1308
|
if (bytes < 1024 ** 3) return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
|
|
1307
1309
|
return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
|
|
1308
1310
|
}
|
|
1309
|
-
function
|
|
1310
|
-
const
|
|
1311
|
+
function uploadSvg(paths, size = 22) {
|
|
1312
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
1313
|
+
svg.setAttribute("class", "fillo-file-state-icon");
|
|
1314
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
1315
|
+
svg.setAttribute("width", String(size));
|
|
1316
|
+
svg.setAttribute("height", String(size));
|
|
1317
|
+
svg.setAttribute("fill", "none");
|
|
1318
|
+
svg.setAttribute("aria-hidden", "true");
|
|
1319
|
+
svg.setAttribute("focusable", "false");
|
|
1320
|
+
for (const d of paths) {
|
|
1321
|
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1322
|
+
path.setAttribute("d", d);
|
|
1323
|
+
svg.appendChild(path);
|
|
1324
|
+
}
|
|
1325
|
+
return svg;
|
|
1326
|
+
}
|
|
1327
|
+
function fileStateIcon(state) {
|
|
1328
|
+
const mark = el("span", {
|
|
1329
|
+
className: `fillo-file-state fillo-file-state--${state}`,
|
|
1330
|
+
attrs: { "aria-hidden": "true" }
|
|
1331
|
+
});
|
|
1332
|
+
mark.appendChild(
|
|
1333
|
+
state === "done" ? uploadSvg([
|
|
1334
|
+
"M3.75 12a8.25 8.25 0 1 0 16.5 0 8.25 8.25 0 1 0-16.5 0",
|
|
1335
|
+
"m8.5 12 2.25 2.25 4.75-5"
|
|
1336
|
+
]) : state === "failed" ? uploadSvg([
|
|
1337
|
+
"M3.75 12a8.25 8.25 0 1 0 16.5 0 8.25 8.25 0 1 0-16.5 0",
|
|
1338
|
+
"M12 7.75v5.5",
|
|
1339
|
+
"M12 16.5h.01"
|
|
1340
|
+
]) : uploadSvg(["M7.25 3.75h6.5l3 3v13.5h-9.5z", "M13.75 3.75v3h3"])
|
|
1341
|
+
);
|
|
1342
|
+
return mark;
|
|
1343
|
+
}
|
|
1344
|
+
function closeIcon() {
|
|
1345
|
+
const icon = uploadSvg(["m7 7 10 10M17 7 7 17"], 16);
|
|
1346
|
+
icon.setAttribute("class", "fillo-file-action-icon");
|
|
1347
|
+
return icon;
|
|
1348
|
+
}
|
|
1349
|
+
function fileActionBtn(className, label, content, onClick) {
|
|
1350
|
+
const btn = el("button", { className, attrs: { type: "button", "aria-label": label } });
|
|
1351
|
+
btn.append(content);
|
|
1311
1352
|
btn.addEventListener("click", onClick);
|
|
1312
1353
|
return btn;
|
|
1313
1354
|
}
|
|
@@ -1418,35 +1459,39 @@ function fileUpload(context) {
|
|
|
1418
1459
|
const list = el("ul", { className: "fillo-files" });
|
|
1419
1460
|
for (const file of done) {
|
|
1420
1461
|
const li = el("li", { className: "fillo-file fillo-file--done" });
|
|
1421
|
-
|
|
1462
|
+
const content = el("span", { className: "fillo-file-content" });
|
|
1463
|
+
appendChildren(content, [
|
|
1422
1464
|
el("span", { className: "fillo-file-name", text: file.name }),
|
|
1423
|
-
el("span", {
|
|
1424
|
-
|
|
1465
|
+
el("span", {
|
|
1466
|
+
className: "fillo-file-meta",
|
|
1467
|
+
text: DEFAULT_FIELD_STRINGS.uploadedFile(formatBytes(file.size))
|
|
1468
|
+
})
|
|
1469
|
+
]);
|
|
1470
|
+
const actions = el("span", { className: "fillo-file-actions" });
|
|
1471
|
+
actions.appendChild(
|
|
1425
1472
|
fileActionBtn(
|
|
1426
1473
|
"fillo-file-remove",
|
|
1427
|
-
|
|
1428
|
-
|
|
1474
|
+
`${DEFAULT_FIELD_STRINGS.uploadRemove} ${file.name}`,
|
|
1475
|
+
closeIcon(),
|
|
1429
1476
|
() => context.setValue(done.filter((f) => f.fileId !== file.fileId))
|
|
1430
1477
|
)
|
|
1431
|
-
|
|
1478
|
+
);
|
|
1479
|
+
appendChildren(li, [fileStateIcon("done"), content, actions]);
|
|
1432
1480
|
list.appendChild(li);
|
|
1433
1481
|
}
|
|
1434
1482
|
for (const row of rows) {
|
|
1435
1483
|
const failed = row.error !== void 0;
|
|
1436
1484
|
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
|
-
}
|
|
1485
|
+
className: `fillo-file${failed ? " fillo-file--failed" : ""}`
|
|
1445
1486
|
});
|
|
1446
|
-
li.appendChild(
|
|
1487
|
+
li.appendChild(fileStateIcon(failed ? "failed" : "uploading"));
|
|
1488
|
+
const content = el("span", { className: "fillo-file-content" });
|
|
1489
|
+
content.appendChild(el("span", { className: "fillo-file-name", text: row.name }));
|
|
1447
1490
|
if (failed) {
|
|
1448
1491
|
const actions = el("span", { className: "fillo-file-actions" });
|
|
1449
|
-
|
|
1492
|
+
content.appendChild(el("span", { className: "fillo-file-error", text: row.error ?? "" }));
|
|
1493
|
+
li.appendChild(content);
|
|
1494
|
+
if (!row.tooLarge && remaining > 0) {
|
|
1450
1495
|
actions.appendChild(
|
|
1451
1496
|
fileActionBtn(
|
|
1452
1497
|
"fillo-file-retry",
|
|
@@ -1459,29 +1504,47 @@ function fileUpload(context) {
|
|
|
1459
1504
|
actions.appendChild(
|
|
1460
1505
|
fileActionBtn(
|
|
1461
1506
|
"fillo-file-remove",
|
|
1462
|
-
|
|
1463
|
-
|
|
1507
|
+
`${DEFAULT_FIELD_STRINGS.uploadDismiss} ${row.name}`,
|
|
1508
|
+
closeIcon(),
|
|
1464
1509
|
() => internal.uploadAction?.(row.key, "dismiss")
|
|
1465
1510
|
)
|
|
1466
1511
|
);
|
|
1467
1512
|
li.appendChild(actions);
|
|
1468
|
-
li.appendChild(el("span", { className: "fillo-file-error", text: row.error ?? "" }));
|
|
1469
1513
|
} else {
|
|
1470
1514
|
const bar = el("span", { className: "fillo-progress-bar" });
|
|
1471
1515
|
bar.style.width = `${row.fraction * 100}%`;
|
|
1472
|
-
|
|
1516
|
+
content.appendChild(
|
|
1473
1517
|
el("span", {
|
|
1474
1518
|
className: "fillo-file-meta",
|
|
1475
|
-
text:
|
|
1476
|
-
|
|
1519
|
+
text: DEFAULT_FIELD_STRINGS.uploadingFile(
|
|
1520
|
+
Math.round(row.fraction * 100),
|
|
1521
|
+
formatBytes(row.size)
|
|
1522
|
+
)
|
|
1523
|
+
})
|
|
1524
|
+
);
|
|
1525
|
+
const actions = el("span", { className: "fillo-file-actions" });
|
|
1526
|
+
actions.appendChild(
|
|
1477
1527
|
fileActionBtn(
|
|
1478
1528
|
"fillo-file-remove",
|
|
1479
|
-
|
|
1480
|
-
|
|
1529
|
+
`${DEFAULT_FIELD_STRINGS.uploadCancel} ${row.name}`,
|
|
1530
|
+
closeIcon(),
|
|
1481
1531
|
() => internal.uploadAction?.(row.key, "cancel")
|
|
1482
|
-
)
|
|
1532
|
+
)
|
|
1533
|
+
);
|
|
1534
|
+
appendChildren(li, [
|
|
1535
|
+
content,
|
|
1536
|
+
actions,
|
|
1483
1537
|
appendChildren(
|
|
1484
|
-
el("span", {
|
|
1538
|
+
el("span", {
|
|
1539
|
+
className: "fillo-progress",
|
|
1540
|
+
attrs: {
|
|
1541
|
+
role: "progressbar",
|
|
1542
|
+
"aria-label": `Uploading ${row.name}`,
|
|
1543
|
+
"aria-valuemin": "0",
|
|
1544
|
+
"aria-valuemax": "100",
|
|
1545
|
+
"aria-valuenow": String(Math.round(row.fraction * 100))
|
|
1546
|
+
}
|
|
1547
|
+
}),
|
|
1485
1548
|
[bar]
|
|
1486
1549
|
)
|
|
1487
1550
|
]);
|
|
@@ -2162,7 +2225,8 @@ var DomFormController = class {
|
|
|
2162
2225
|
name: row.name,
|
|
2163
2226
|
size: row.size,
|
|
2164
2227
|
fraction: row.fraction,
|
|
2165
|
-
error: row.error
|
|
2228
|
+
error: row.error,
|
|
2229
|
+
tooLarge: row.tooLarge
|
|
2166
2230
|
});
|
|
2167
2231
|
}
|
|
2168
2232
|
return rows;
|
|
@@ -2179,6 +2243,7 @@ var DomFormController = class {
|
|
|
2179
2243
|
} else if (action === "dismiss") {
|
|
2180
2244
|
this.inFlight.delete(key);
|
|
2181
2245
|
} else {
|
|
2246
|
+
if (row.tooLarge) return;
|
|
2182
2247
|
this.inFlight.delete(key);
|
|
2183
2248
|
void this.startUpload(row.fieldId, row.file, row.maxFiles);
|
|
2184
2249
|
}
|
|
@@ -2242,6 +2307,7 @@ var DomFormController = class {
|
|
|
2242
2307
|
const maxFileSizeMb = this.uploadFileSizeLimitMb ? Math.min(configuredMaxMb, this.uploadFileSizeLimitMb) : configuredMaxMb;
|
|
2243
2308
|
const maxBytes = maxFileSizeMb * 1024 * 1024;
|
|
2244
2309
|
const selected = withinLimit.filter((file) => file.size <= maxBytes);
|
|
2310
|
+
const oversizedFiles = withinLimit.filter((file) => file.size > maxBytes);
|
|
2245
2311
|
const dropped = files.length - withinLimit.length;
|
|
2246
2312
|
const oversized = withinLimit.length - selected.length;
|
|
2247
2313
|
if (dropped > 0 || oversized > 0) {
|
|
@@ -2251,17 +2317,29 @@ var DomFormController = class {
|
|
|
2251
2317
|
`You can attach up to ${maxFiles} file${maxFiles === 1 ? "" : "s"}. ${dropped} ${dropped === 1 ? "file was" : "files were"} not added.`
|
|
2252
2318
|
);
|
|
2253
2319
|
}
|
|
2254
|
-
if (oversized > 0) {
|
|
2255
|
-
notices.push(
|
|
2256
|
-
`${oversized} ${oversized === 1 ? "file exceeds" : "files exceed"} the ${maxFileSizeMb} MB limit.`
|
|
2257
|
-
);
|
|
2258
|
-
}
|
|
2259
2320
|
const text = notices.join(" ");
|
|
2260
|
-
this.uploadNotices.set(field.id, text);
|
|
2261
|
-
this.
|
|
2321
|
+
if (text) this.uploadNotices.set(field.id, text);
|
|
2322
|
+
else this.uploadNotices.delete(field.id);
|
|
2323
|
+
if (text) this.announce(text);
|
|
2262
2324
|
} else {
|
|
2263
2325
|
this.uploadNotices.delete(field.id);
|
|
2264
2326
|
}
|
|
2327
|
+
for (const file of oversizedFiles) {
|
|
2328
|
+
const message = DEFAULT_FIELD_STRINGS.fileTooLarge(maxFileSizeMb);
|
|
2329
|
+
const key = `${file.name}-${file.size}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
2330
|
+
this.inFlight.set(key, {
|
|
2331
|
+
fieldId: field.id,
|
|
2332
|
+
name: file.name,
|
|
2333
|
+
size: file.size,
|
|
2334
|
+
fraction: 0,
|
|
2335
|
+
error: message,
|
|
2336
|
+
tooLarge: true,
|
|
2337
|
+
file,
|
|
2338
|
+
controller: new AbortController(),
|
|
2339
|
+
maxFiles
|
|
2340
|
+
});
|
|
2341
|
+
this.announceAlert(message);
|
|
2342
|
+
}
|
|
2265
2343
|
if (selected.length === 0) {
|
|
2266
2344
|
this.queueRender();
|
|
2267
2345
|
return;
|
|
@@ -2436,11 +2514,6 @@ var DomFormController = class {
|
|
|
2436
2514
|
applyFocus(intent, captured) {
|
|
2437
2515
|
if (typeof document === "undefined") return;
|
|
2438
2516
|
if (intent === "error") {
|
|
2439
|
-
const summary = this.element.querySelector(".fillo-error-summary");
|
|
2440
|
-
if (summary) {
|
|
2441
|
-
summary.focus();
|
|
2442
|
-
return;
|
|
2443
|
-
}
|
|
2444
2517
|
const invalid = this.element.querySelector('[aria-invalid="true"]');
|
|
2445
2518
|
invalid && focusableWithin(invalid)?.focus();
|
|
2446
2519
|
return;
|
|
@@ -2770,8 +2843,6 @@ var DomFormController = class {
|
|
|
2770
2843
|
})
|
|
2771
2844
|
);
|
|
2772
2845
|
}
|
|
2773
|
-
const summary = this.renderErrorSummary(api);
|
|
2774
|
-
if (summary) content.appendChild(summary);
|
|
2775
2846
|
const blocks = el("div", { className: "fillo-blocks", attrs: { "data-fillo": "blocks" } });
|
|
2776
2847
|
for (const block of api.blocks) {
|
|
2777
2848
|
const rendered = this.renderBlock(pipeBlock(block, this.mutableData, api.form), api);
|
|
@@ -2879,45 +2950,6 @@ var DomFormController = class {
|
|
|
2879
2950
|
}
|
|
2880
2951
|
return form;
|
|
2881
2952
|
}
|
|
2882
|
-
/** GOV.UK error summary (audit P2.8): scoped to the CURRENTLY RENDERED
|
|
2883
|
-
* page — a failed submit already jumps to the first page with a visible
|
|
2884
|
-
* error, so every link resolves to something in the live tree. Per-field
|
|
2885
|
-
* messages lose their own role="alert"; this is the one alert. */
|
|
2886
|
-
renderErrorSummary(api) {
|
|
2887
|
-
const fields = api.blocks.filter(isField).filter((field) => api.errors[field.id]);
|
|
2888
|
-
if (fields.length === 0) return null;
|
|
2889
|
-
const box = el("div", {
|
|
2890
|
-
className: "fillo-error-summary",
|
|
2891
|
-
attrs: { role: "alert", tabindex: "-1", "data-fillo": "error-summary" }
|
|
2892
|
-
});
|
|
2893
|
-
box.appendChild(
|
|
2894
|
-
el("p", {
|
|
2895
|
-
className: "fillo-error-summary-title",
|
|
2896
|
-
text: DEFAULT_FIELD_STRINGS.errorSummaryTitle
|
|
2897
|
-
})
|
|
2898
|
-
);
|
|
2899
|
-
const list = el("ul");
|
|
2900
|
-
for (const field of fields) {
|
|
2901
|
-
const link = el("a", {
|
|
2902
|
-
text: `${field.label}: ${api.errors[field.id]}`,
|
|
2903
|
-
attrs: { href: `#${fieldIds(this.instanceId, field.id).inputId}` }
|
|
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
|
-
}
|
|
2921
2953
|
renderBlock(block, api) {
|
|
2922
2954
|
if (!isField(block)) return renderContent(block);
|
|
2923
2955
|
const setValue = (value, options) => {
|
|
@@ -2936,7 +2968,7 @@ var DomFormController = class {
|
|
|
2936
2968
|
const context = {
|
|
2937
2969
|
field: block,
|
|
2938
2970
|
value: api.data[block.id],
|
|
2939
|
-
error: api.errors[block.id],
|
|
2971
|
+
error: api.errors[block.id] === REQUIRED_FIELD_MESSAGE ? requiredFieldMessage(block, DEFAULT_FIELD_STRINGS) : api.errors[block.id],
|
|
2940
2972
|
ids: fieldIds(this.instanceId, block.id),
|
|
2941
2973
|
api,
|
|
2942
2974
|
setValue,
|