katalyst-govuk-formbuilder 1.30.1 → 2.0.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.
- checksums.yaml +4 -4
- data/README.md +141 -15
- data/app/assets/builds/katalyst/govuk/formbuilder.css +79 -1
- data/app/assets/builds/katalyst/govuk/formbuilder.js +573 -10
- data/app/assets/builds/katalyst/govuk/formbuilder.min.js +8 -8
- data/app/assets/builds/katalyst/govuk/formbuilder.min.js.map +1 -1
- data/app/assets/stylesheets/katalyst/govuk/components/_index.scss +1 -0
- data/app/assets/stylesheets/katalyst/govuk/components/attachment/_index.scss +3 -0
- data/app/assets/stylesheets/katalyst/govuk/components/attachment/_mixin.scss +89 -0
- data/app/helpers/katalyst/govuk/form_builder/builder.rb +174 -6
- data/app/helpers/katalyst/govuk/form_builder/elements/attachment.rb +80 -0
- data/app/helpers/katalyst/govuk/form_builder/frontend.rb +16 -5
- data/app/helpers/katalyst/govuk/form_builder/traits/attachment.rb +214 -0
- data/config/importmap.rb +1 -0
- data/config/locales/en.yml +16 -0
- data/lib/katalyst/govuk/form_builder/config.rb +20 -0
- data/lib/katalyst/govuk/form_builder/engine.rb +1 -1
- data/node_modules/govuk-frontend/dist/govuk/components/_index.import.scss +2 -0
- data/node_modules/govuk-frontend/dist/govuk/components/_index.scss +2 -0
- data/node_modules/govuk-frontend/dist/govuk/components/cookie-banner/_mixin.scss +6 -0
- data/node_modules/govuk-frontend/dist/govuk/components/feedback/_feedback.import.scss +6 -0
- data/node_modules/govuk-frontend/dist/govuk/components/feedback/_index.import.scss +8 -0
- data/node_modules/govuk-frontend/dist/govuk/components/feedback/_index.scss +5 -0
- data/node_modules/govuk-frontend/dist/govuk/components/feedback/_mixin.scss +48 -0
- data/node_modules/govuk-frontend/dist/govuk/components/footer/_mixin.scss +6 -0
- data/node_modules/govuk-frontend/dist/govuk/components/language-navigation/_index.import.scss +8 -0
- data/node_modules/govuk-frontend/dist/govuk/components/language-navigation/_index.scss +6 -0
- data/node_modules/govuk-frontend/dist/govuk/components/language-navigation/_language-navigation.import.scss +6 -0
- data/node_modules/govuk-frontend/dist/govuk/components/language-navigation/_mixin.scss +68 -0
- data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/_mixin.scss +48 -3
- data/node_modules/govuk-frontend/dist/govuk/custom-properties/_frontend-version.mixin.scss +1 -1
- data/node_modules/govuk-frontend/dist/govuk/custom-properties/_functional-colours.mixin.scss +1 -0
- data/node_modules/govuk-frontend/dist/govuk/settings/_colours-organisations.scss +16 -2
- metadata +28 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { Controller } from "@hotwired/stimulus";
|
|
1
|
+
import { Controller, Application } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
import { DirectUploadController } from "@rails/activestorage";
|
|
2
4
|
|
|
3
5
|
function setFocus($element, options = {}) {
|
|
4
6
|
var _options$onBeforeFocu;
|
|
@@ -940,7 +942,7 @@ ErrorSummary.schema = Object.freeze({
|
|
|
940
942
|
}
|
|
941
943
|
canDrop(dataTransfer) {
|
|
942
944
|
if (dataTransfer.items.length) {
|
|
943
|
-
return this.matchesInputCapacity(countFileItems(dataTransfer.items));
|
|
945
|
+
return this.matchesInputCapacity(countFileItems$1(dataTransfer.items));
|
|
944
946
|
}
|
|
945
947
|
if (dataTransfer.types.length) {
|
|
946
948
|
return dataTransfer.types.includes("Files");
|
|
@@ -1024,7 +1026,7 @@ FileUpload.schema = Object.freeze({
|
|
|
1024
1026
|
}
|
|
1025
1027
|
});
|
|
1026
1028
|
|
|
1027
|
-
function countFileItems(list) {
|
|
1029
|
+
function countFileItems$1(list) {
|
|
1028
1030
|
let result = 0;
|
|
1029
1031
|
for (let i = 0; i < list.length; i++) {
|
|
1030
1032
|
if (list[i].kind === "file") {
|
|
@@ -1210,6 +1212,240 @@ PasswordInput.schema = Object.freeze({
|
|
|
1210
1212
|
|
|
1211
1213
|
Radios.moduleName = "govuk-radios";
|
|
1212
1214
|
|
|
1215
|
+
const config = {
|
|
1216
|
+
brand: "govuk"
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
const uploadButtonSelector = "[type='button']:has(+ input[type='file'])";
|
|
1220
|
+
|
|
1221
|
+
const Attachment = {
|
|
1222
|
+
moduleName: "govuk-attachment",
|
|
1223
|
+
defaults: {
|
|
1224
|
+
i18n: {
|
|
1225
|
+
...FileUpload.defaults.i18n,
|
|
1226
|
+
uploadSucceeded: "Uploaded successfully",
|
|
1227
|
+
uploadFailed: "Upload failed — try again",
|
|
1228
|
+
retryButton: "Try again",
|
|
1229
|
+
fileRemoved: "%{filename} removed",
|
|
1230
|
+
removeButton: "Remove %{filename}",
|
|
1231
|
+
removeButtonContent: "Remove"
|
|
1232
|
+
}
|
|
1233
|
+
},
|
|
1234
|
+
schema: {
|
|
1235
|
+
properties: {
|
|
1236
|
+
i18n: {
|
|
1237
|
+
type: "object"
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
|
|
1243
|
+
function attachmentConfig(wrapper) {
|
|
1244
|
+
return mergeConfigs(Attachment.defaults, normaliseDataset(Attachment, wrapper?.dataset ?? {}));
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
class AttachmentUploadController extends DirectUploadController {
|
|
1248
|
+
async start(option) {
|
|
1249
|
+
this.dispatch("start");
|
|
1250
|
+
try {
|
|
1251
|
+
const attributes = await new Promise((resolve, reject) => {
|
|
1252
|
+
this.directUpload.create((error, attributes) => error ? reject(error) : resolve(attributes));
|
|
1253
|
+
});
|
|
1254
|
+
option.value = attributes.signed_id;
|
|
1255
|
+
} catch (error) {
|
|
1256
|
+
this.dispatch("error", {
|
|
1257
|
+
error: error
|
|
1258
|
+
});
|
|
1259
|
+
throw error;
|
|
1260
|
+
} finally {
|
|
1261
|
+
this.dispatch("end");
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
class AttachmentController extends Controller {
|
|
1267
|
+
connect() {
|
|
1268
|
+
this.config = attachmentConfig(this.element.closest(`.${config.brand}-file-upload-wrapper`));
|
|
1269
|
+
this.i18n = new I18n(this.config.i18n, {
|
|
1270
|
+
locale: closestAttributeValue(this.element, "lang")
|
|
1271
|
+
});
|
|
1272
|
+
this.select.addEventListener("change", this.change);
|
|
1273
|
+
this.previewPendingFile();
|
|
1274
|
+
this.uploadPendingFile();
|
|
1275
|
+
}
|
|
1276
|
+
disconnect() {
|
|
1277
|
+
this.select?.removeEventListener("change", this.change);
|
|
1278
|
+
}
|
|
1279
|
+
previewPendingFile() {
|
|
1280
|
+
const file = this.element.file;
|
|
1281
|
+
if (!file) return;
|
|
1282
|
+
if (!file.type.startsWith("image/")) {
|
|
1283
|
+
this.imageTag?.remove();
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
const preview = new FileReader;
|
|
1287
|
+
preview.onload = this.onPreviewReady;
|
|
1288
|
+
preview.readAsDataURL(file);
|
|
1289
|
+
}
|
|
1290
|
+
uploadPendingFile() {
|
|
1291
|
+
const file = this.element.file;
|
|
1292
|
+
if (!file) return;
|
|
1293
|
+
if (!this.input?.dataset.directUploadUrl) return;
|
|
1294
|
+
delete this.element.file;
|
|
1295
|
+
this.input.dispatchEvent(new CustomEvent("govuk:upload", {
|
|
1296
|
+
detail: {
|
|
1297
|
+
file: file
|
|
1298
|
+
}
|
|
1299
|
+
}));
|
|
1300
|
+
this.performUpload(file);
|
|
1301
|
+
}
|
|
1302
|
+
async performUpload(file) {
|
|
1303
|
+
this.uploader = new AttachmentUploadController(this.input, file);
|
|
1304
|
+
this.element.dataset.state = "uploading";
|
|
1305
|
+
this.statusText = "";
|
|
1306
|
+
this.retryButton?.remove();
|
|
1307
|
+
const progressTag = createProgressTag(this.filenameTag.id);
|
|
1308
|
+
this.captionTag.appendChild(progressTag);
|
|
1309
|
+
this.input.addEventListener("direct-upload:progress", this.progress);
|
|
1310
|
+
try {
|
|
1311
|
+
await this.uploader.start(this.inputOption);
|
|
1312
|
+
this.element.dataset.state = "upload-successful";
|
|
1313
|
+
this.statusText = this.i18n.t("uploadSucceeded");
|
|
1314
|
+
} catch (error) {
|
|
1315
|
+
console.warn(error);
|
|
1316
|
+
this.element.dataset.state = "upload-failed";
|
|
1317
|
+
this.statusText = this.i18n.t("uploadFailed");
|
|
1318
|
+
this.actionsTag?.prepend(createRetryButton(file.name, this.i18n));
|
|
1319
|
+
} finally {
|
|
1320
|
+
this.input.removeEventListener("direct-upload:progress", this.progress);
|
|
1321
|
+
progressTag.remove();
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
onPreviewReady=e => {
|
|
1325
|
+
this.imageTag.src = e.target.result;
|
|
1326
|
+
};
|
|
1327
|
+
change=() => {
|
|
1328
|
+
if (this.select.value === "") this.destroy();
|
|
1329
|
+
};
|
|
1330
|
+
retry() {
|
|
1331
|
+
this.performUpload(this.directUpload.file);
|
|
1332
|
+
this.removeButton?.focus();
|
|
1333
|
+
}
|
|
1334
|
+
progress=({detail: detail}) => {
|
|
1335
|
+
if (detail.id !== this.id) return;
|
|
1336
|
+
if (this.progressTag) this.progressTag.value = detail.progress;
|
|
1337
|
+
};
|
|
1338
|
+
destroy() {
|
|
1339
|
+
const focusTarget = this.uploadButton;
|
|
1340
|
+
const remove = new CustomEvent("govuk:remove", {
|
|
1341
|
+
detail: {
|
|
1342
|
+
name: this.element.querySelector(".filename")?.textContent,
|
|
1343
|
+
file: this.element.file
|
|
1344
|
+
},
|
|
1345
|
+
cancelable: true
|
|
1346
|
+
});
|
|
1347
|
+
if (!this.input.dispatchEvent(remove)) return;
|
|
1348
|
+
this.element.remove();
|
|
1349
|
+
focusTarget?.focus();
|
|
1350
|
+
}
|
|
1351
|
+
get id() {
|
|
1352
|
+
return this.directUpload?.id ?? this.select.id;
|
|
1353
|
+
}
|
|
1354
|
+
get directUpload() {
|
|
1355
|
+
return this.uploader?.directUpload;
|
|
1356
|
+
}
|
|
1357
|
+
get input() {
|
|
1358
|
+
return this.element.closest(`.${config.brand}-file-upload-wrapper`)?.querySelector("input[type=file]");
|
|
1359
|
+
}
|
|
1360
|
+
get uploadButton() {
|
|
1361
|
+
return this.element.closest(`.${config.brand}-file-upload-wrapper`)?.querySelector(uploadButtonSelector);
|
|
1362
|
+
}
|
|
1363
|
+
set statusText(message) {
|
|
1364
|
+
const tag = this.element.querySelector("figcaption .status");
|
|
1365
|
+
if (tag) tag.textContent = message;
|
|
1366
|
+
}
|
|
1367
|
+
get captionTag() {
|
|
1368
|
+
return this.element.querySelector("figcaption");
|
|
1369
|
+
}
|
|
1370
|
+
get filenameTag() {
|
|
1371
|
+
return this.element.querySelector("figcaption .filename");
|
|
1372
|
+
}
|
|
1373
|
+
get actionsTag() {
|
|
1374
|
+
return this.element.querySelector(".actions");
|
|
1375
|
+
}
|
|
1376
|
+
get select() {
|
|
1377
|
+
return this.element.querySelector("select");
|
|
1378
|
+
}
|
|
1379
|
+
get inputOption() {
|
|
1380
|
+
return this.element.querySelector("option");
|
|
1381
|
+
}
|
|
1382
|
+
get imageTag() {
|
|
1383
|
+
return this.element.querySelector("img");
|
|
1384
|
+
}
|
|
1385
|
+
get progressTag() {
|
|
1386
|
+
return this.element.querySelector("progress");
|
|
1387
|
+
}
|
|
1388
|
+
get retryButton() {
|
|
1389
|
+
return this.element.querySelector(".actions button[data-action*='retry']");
|
|
1390
|
+
}
|
|
1391
|
+
get removeButton() {
|
|
1392
|
+
return this.element.querySelector(".actions button[data-action*='destroy']");
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
let nextAttachmentId = 0;
|
|
1397
|
+
|
|
1398
|
+
function createAttachment(input, file, i18n) {
|
|
1399
|
+
const template = document.createElement("TEMPLATE");
|
|
1400
|
+
const id = ++nextAttachmentId;
|
|
1401
|
+
template.innerHTML = `\n <figure class="${config.brand}-attachment" data-controller="govuk-attachment" aria-labelledby="attachment-${id}-filename">\n <img alt="" class="preview" src="">\n <figcaption class="caption" aria-atomic="true" aria-live="polite">\n <span id="attachment-${id}-filename" class="filename"></span>\n <span class="size"></span>\n <span class="status"></span>\n </figcaption>\n <div class="actions">\n <select id="attachment-${id}-input" name="${input.name}" aria-labelledby="attachment-${id}-filename">\n <option selected="selected" value=""></option>\n <option value=""></option>\n </select>\n <button type="button" class="${config.brand}-button ${config.brand}-button--secondary ${config.brand}-attachment__remove" data-action="govuk-attachment#destroy" data-module="govuk-button"></button>\n </div>\n </figure>\n `;
|
|
1402
|
+
const figure = template.content.firstElementChild;
|
|
1403
|
+
figure.querySelector(".filename").textContent = file.name;
|
|
1404
|
+
figure.querySelector(".size").textContent = humanSize(file.size);
|
|
1405
|
+
const [keep, remove] = figure.querySelectorAll("option");
|
|
1406
|
+
keep.textContent = file.name;
|
|
1407
|
+
remove.textContent = i18n.t("removeButton", {
|
|
1408
|
+
filename: file.name
|
|
1409
|
+
});
|
|
1410
|
+
const removeButton = figure.querySelector("button");
|
|
1411
|
+
removeButton.textContent = i18n.t("removeButtonContent");
|
|
1412
|
+
removeButton.setAttribute("aria-label", i18n.t("removeButton", {
|
|
1413
|
+
filename: file.name
|
|
1414
|
+
}));
|
|
1415
|
+
figure.file = file;
|
|
1416
|
+
return figure;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
const UNITS = [ "Bytes", "KB", "MB", "GB", "TB", "PB" ];
|
|
1420
|
+
|
|
1421
|
+
function humanSize(bytes) {
|
|
1422
|
+
if (bytes === 1) return "1 Byte";
|
|
1423
|
+
if (bytes < 1024) return `${bytes} Bytes`;
|
|
1424
|
+
const exp = Math.min(Math.floor(Math.log2(bytes) / 10), UNITS.length - 1);
|
|
1425
|
+
const value = Number((bytes / 1024 ** exp).toPrecision(3));
|
|
1426
|
+
return `${value} ${UNITS[exp]}`;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function createRetryButton(filename, i18n) {
|
|
1430
|
+
const button = document.createElement("BUTTON");
|
|
1431
|
+
button.type = "button";
|
|
1432
|
+
button.className = `${config.brand}-button ${config.brand}-button--secondary ${config.brand}-attachment__retry`;
|
|
1433
|
+
button.textContent = i18n.t("retryButton");
|
|
1434
|
+
button.setAttribute("aria-label", `${i18n.t("retryButton")} ${filename}`);
|
|
1435
|
+
button.dataset.action = "govuk-attachment#retry";
|
|
1436
|
+
button.dataset.module = "govuk-button";
|
|
1437
|
+
return button;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
function createProgressTag(labelId) {
|
|
1441
|
+
const progress = document.createElement("PROGRESS");
|
|
1442
|
+
progress.className = `${config.brand}-attachment-progress`;
|
|
1443
|
+
if (labelId) progress.setAttribute("aria-labelledby", labelId);
|
|
1444
|
+
progress.value = 0;
|
|
1445
|
+
progress.max = 100;
|
|
1446
|
+
return progress;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1213
1449
|
class FileFieldController extends Controller {
|
|
1214
1450
|
static targets=[ "preview", "destroy" ];
|
|
1215
1451
|
static values={
|
|
@@ -1343,6 +1579,250 @@ class DocumentFieldController extends FileFieldController {
|
|
|
1343
1579
|
}
|
|
1344
1580
|
}
|
|
1345
1581
|
|
|
1582
|
+
class FileUploadController extends Controller {
|
|
1583
|
+
connect() {
|
|
1584
|
+
if (!this.fileInput) {
|
|
1585
|
+
throw new Error(`Missing file input for ${this.element}`);
|
|
1586
|
+
}
|
|
1587
|
+
this.config = attachmentConfig(this.element);
|
|
1588
|
+
this.i18n = new I18n(this.config.i18n, {
|
|
1589
|
+
locale: closestAttributeValue(this.element, "lang")
|
|
1590
|
+
});
|
|
1591
|
+
this.id = this.uploadButton?.id ?? this.fileInput.id;
|
|
1592
|
+
document.addEventListener("dragenter", this.onDragenter);
|
|
1593
|
+
document.addEventListener("dragleave", this.onDragleave);
|
|
1594
|
+
this.element.addEventListener("dragover", this.onDragover);
|
|
1595
|
+
this.element.addEventListener("drop", this.onDrop);
|
|
1596
|
+
this.enhance();
|
|
1597
|
+
this.morphObserver = new MutationObserver(this.onMorph);
|
|
1598
|
+
this.morphObserver.observe(this.element, {
|
|
1599
|
+
childList: true
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
disconnect() {
|
|
1603
|
+
this.morphObserver?.disconnect();
|
|
1604
|
+
this.disabledObserver?.disconnect();
|
|
1605
|
+
this.uploadButton?.removeEventListener("click", this.onClick);
|
|
1606
|
+
this.element.removeEventListener("dragover", this.onDragover);
|
|
1607
|
+
this.element.removeEventListener("drop", this.onDrop);
|
|
1608
|
+
this.unbindInput();
|
|
1609
|
+
document.removeEventListener("dragenter", this.onDragenter);
|
|
1610
|
+
document.removeEventListener("dragleave", this.onDragleave);
|
|
1611
|
+
this.announcements?.remove();
|
|
1612
|
+
}
|
|
1613
|
+
enhance() {
|
|
1614
|
+
const fileInput = this.fileInput;
|
|
1615
|
+
let uploadButton = this.uploadButton;
|
|
1616
|
+
if (!uploadButton) {
|
|
1617
|
+
this.ensureLabelId();
|
|
1618
|
+
fileInput.id = `${this.id}-input`;
|
|
1619
|
+
fileInput.setAttribute("hidden", "hidden");
|
|
1620
|
+
uploadButton = createUploadButton(this.id, this.i18n, fileInput);
|
|
1621
|
+
fileInput.insertAdjacentElement("beforebegin", uploadButton);
|
|
1622
|
+
uploadButton.addEventListener("click", this.onClick);
|
|
1623
|
+
}
|
|
1624
|
+
if (!this.announcements) this.element.appendChild(createAnnouncements());
|
|
1625
|
+
this.unbindInput();
|
|
1626
|
+
fileInput.addEventListener("change", this.onChange);
|
|
1627
|
+
fileInput.addEventListener("govuk:upload", this.onUpload);
|
|
1628
|
+
fileInput.addEventListener("govuk:remove", this.onRemove);
|
|
1629
|
+
this.updateDisabledState();
|
|
1630
|
+
this.observeDisabledState();
|
|
1631
|
+
this.updateCount();
|
|
1632
|
+
}
|
|
1633
|
+
unbindInput() {
|
|
1634
|
+
this.fileInput?.removeEventListener("change", this.onChange);
|
|
1635
|
+
this.fileInput?.removeEventListener("govuk:upload", this.onUpload);
|
|
1636
|
+
this.fileInput?.removeEventListener("govuk:remove", this.onRemove);
|
|
1637
|
+
}
|
|
1638
|
+
onMorph=() => {
|
|
1639
|
+
if (this.uploadButton) return;
|
|
1640
|
+
this.fileInput.value = "";
|
|
1641
|
+
this.element.querySelectorAll("figure").forEach(figure => {
|
|
1642
|
+
delete figure.file;
|
|
1643
|
+
});
|
|
1644
|
+
this.enhance();
|
|
1645
|
+
};
|
|
1646
|
+
ensureLabelId() {
|
|
1647
|
+
const label = document.querySelector(`label[for="${this.id}"]`);
|
|
1648
|
+
if (label && !label.id) label.id = `${this.id}-label`;
|
|
1649
|
+
}
|
|
1650
|
+
updateDisabledState() {
|
|
1651
|
+
if (!this.uploadButton) return;
|
|
1652
|
+
const disabled = this.fileInput.disabled;
|
|
1653
|
+
this.uploadButton.disabled = disabled;
|
|
1654
|
+
this.element.classList.toggle(`${config.brand}-file-upload-wrapper--disabled`, disabled);
|
|
1655
|
+
}
|
|
1656
|
+
observeDisabledState() {
|
|
1657
|
+
this.disabledObserver?.disconnect();
|
|
1658
|
+
this.disabledObserver = new MutationObserver(mutations => {
|
|
1659
|
+
for (const mutation of mutations) {
|
|
1660
|
+
if (mutation.attributeName === "disabled") this.updateDisabledState();
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
this.disabledObserver.observe(this.fileInput, {
|
|
1664
|
+
attributes: true
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
onClick=event => {
|
|
1668
|
+
this.fileInput.click();
|
|
1669
|
+
};
|
|
1670
|
+
onDragover=event => {
|
|
1671
|
+
if (this.fileInput.disabled) return;
|
|
1672
|
+
event.preventDefault();
|
|
1673
|
+
};
|
|
1674
|
+
onDragenter=event => {
|
|
1675
|
+
this.updateDropzoneVisibility(event);
|
|
1676
|
+
this.enteredAnotherElement = true;
|
|
1677
|
+
};
|
|
1678
|
+
onDragleave=() => {
|
|
1679
|
+
if (!this.enteredAnotherElement && !this.uploadButton.disabled) {
|
|
1680
|
+
this.hideDraggingState();
|
|
1681
|
+
this.announce(this.i18n.t("leftDropZone"));
|
|
1682
|
+
}
|
|
1683
|
+
this.enteredAnotherElement = false;
|
|
1684
|
+
};
|
|
1685
|
+
onDrop=event => {
|
|
1686
|
+
if (this.fileInput.disabled) return;
|
|
1687
|
+
event.preventDefault();
|
|
1688
|
+
if (event.dataTransfer && this.canFillInput(event.dataTransfer)) {
|
|
1689
|
+
this.fileInput.files = event.dataTransfer.files;
|
|
1690
|
+
this.fileInput.dispatchEvent(new CustomEvent("change"));
|
|
1691
|
+
this.hideDraggingState();
|
|
1692
|
+
}
|
|
1693
|
+
};
|
|
1694
|
+
updateDropzoneVisibility(event) {
|
|
1695
|
+
if (this.uploadButton.disabled) return;
|
|
1696
|
+
if (!(event.target instanceof Node)) return;
|
|
1697
|
+
if (this.element.contains(event.target)) {
|
|
1698
|
+
if (event.dataTransfer && this.canDrop(event.dataTransfer)) {
|
|
1699
|
+
if (!this.isDragging) {
|
|
1700
|
+
this.showDraggingState();
|
|
1701
|
+
this.announce(this.i18n.t("enteredDropZone"));
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
} else if (this.isDragging) {
|
|
1705
|
+
this.hideDraggingState();
|
|
1706
|
+
this.announce(this.i18n.t("leftDropZone"));
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
showDraggingState() {
|
|
1710
|
+
this.uploadButton.classList.add(`${config.brand}-file-upload-button--dragging`);
|
|
1711
|
+
}
|
|
1712
|
+
hideDraggingState() {
|
|
1713
|
+
this.uploadButton.classList.remove(`${config.brand}-file-upload-button--dragging`);
|
|
1714
|
+
}
|
|
1715
|
+
announce(message) {
|
|
1716
|
+
if (this.announcements) this.announcements.textContent = message;
|
|
1717
|
+
}
|
|
1718
|
+
get announcements() {
|
|
1719
|
+
return this.element.querySelector(`.${config.brand}-file-upload-announcements`);
|
|
1720
|
+
}
|
|
1721
|
+
matchesInputCapacity(numberOfFiles) {
|
|
1722
|
+
if (this.fileInput.multiple) return numberOfFiles > 0;
|
|
1723
|
+
return numberOfFiles === 1;
|
|
1724
|
+
}
|
|
1725
|
+
canDrop(dataTransfer) {
|
|
1726
|
+
if (dataTransfer.items.length) {
|
|
1727
|
+
return this.matchesInputCapacity(countFileItems(dataTransfer.items));
|
|
1728
|
+
}
|
|
1729
|
+
if (dataTransfer.types.length) {
|
|
1730
|
+
return dataTransfer.types.includes("Files");
|
|
1731
|
+
}
|
|
1732
|
+
return true;
|
|
1733
|
+
}
|
|
1734
|
+
canFillInput(dataTransfer) {
|
|
1735
|
+
return this.matchesInputCapacity(dataTransfer.files.length);
|
|
1736
|
+
}
|
|
1737
|
+
onChange=() => {
|
|
1738
|
+
const files = Array.from(this.fileInput.files);
|
|
1739
|
+
const figures = Array.from(this.element.querySelectorAll("figure"));
|
|
1740
|
+
figures.forEach(figure => {
|
|
1741
|
+
if (figure.file && !files.includes(figure.file)) figure.remove();
|
|
1742
|
+
});
|
|
1743
|
+
files.forEach(file => {
|
|
1744
|
+
if (figures.some(figure => figure.file === file)) return;
|
|
1745
|
+
const attachment = createAttachment(this.fileInput, file, this.i18n);
|
|
1746
|
+
this.uploadButton.insertAdjacentElement("beforebegin", attachment);
|
|
1747
|
+
});
|
|
1748
|
+
this.updateCount();
|
|
1749
|
+
};
|
|
1750
|
+
onUpload=({detail: {file: file}}) => {
|
|
1751
|
+
this.releaseFile(file);
|
|
1752
|
+
this.updateCount();
|
|
1753
|
+
};
|
|
1754
|
+
releaseFile(file) {
|
|
1755
|
+
const remaining = new DataTransfer;
|
|
1756
|
+
for (const held of this.fileInput.files) {
|
|
1757
|
+
if (held !== file) remaining.items.add(held);
|
|
1758
|
+
}
|
|
1759
|
+
this.fileInput.files = remaining.files;
|
|
1760
|
+
}
|
|
1761
|
+
onRemove=async event => {
|
|
1762
|
+
await Promise.resolve();
|
|
1763
|
+
if (event.defaultPrevented) return;
|
|
1764
|
+
const {name: name, file: file} = event.detail;
|
|
1765
|
+
if (file) this.releaseFile(file);
|
|
1766
|
+
this.announce(this.i18n.t("fileRemoved", {
|
|
1767
|
+
filename: name
|
|
1768
|
+
}));
|
|
1769
|
+
this.updateCount();
|
|
1770
|
+
};
|
|
1771
|
+
updateCount() {
|
|
1772
|
+
const count = this.fileCount;
|
|
1773
|
+
if (count === 0) {
|
|
1774
|
+
this.statusTag.innerText = this.i18n.t("noFileChosen");
|
|
1775
|
+
this.uploadButton.classList.add(`${config.brand}-file-upload-button--empty`);
|
|
1776
|
+
} else {
|
|
1777
|
+
this.statusTag.innerText = this.i18n.t("multipleFilesChosen", {
|
|
1778
|
+
count: count
|
|
1779
|
+
});
|
|
1780
|
+
this.uploadButton.classList.remove(`${config.brand}-file-upload-button--empty`);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
get fileInput() {
|
|
1784
|
+
return this.element.querySelector("input[type='file']");
|
|
1785
|
+
}
|
|
1786
|
+
get uploadButton() {
|
|
1787
|
+
return this.element.querySelector(uploadButtonSelector);
|
|
1788
|
+
}
|
|
1789
|
+
get isDragging() {
|
|
1790
|
+
return this.uploadButton.classList.contains(`${config.brand}-file-upload-button--dragging`);
|
|
1791
|
+
}
|
|
1792
|
+
get statusTag() {
|
|
1793
|
+
return this.element.querySelector("button [aria-live]");
|
|
1794
|
+
}
|
|
1795
|
+
get fileCount() {
|
|
1796
|
+
let count = this.fileInput.files.length;
|
|
1797
|
+
this.element.querySelectorAll(`select[name='${this.fileInput.name}']`).forEach(select => {
|
|
1798
|
+
if (!select.closest("figure")?.file) count += 1;
|
|
1799
|
+
});
|
|
1800
|
+
if (!this.fileInput.multiple) count = Math.min(count, 1);
|
|
1801
|
+
return count;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
function countFileItems(items) {
|
|
1806
|
+
return Array.from(items).filter(item => item.kind === "file").length;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
function createAnnouncements() {
|
|
1810
|
+
const region = document.createElement("span");
|
|
1811
|
+
region.className = `${config.brand}-file-upload-announcements ${config.brand}-visually-hidden`;
|
|
1812
|
+
region.setAttribute("aria-live", "assertive");
|
|
1813
|
+
return region;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
function createUploadButton(id, i18n, fileInput) {
|
|
1817
|
+
const brand = config.brand;
|
|
1818
|
+
const template = document.createElement("TEMPLATE");
|
|
1819
|
+
template.innerHTML = `\n <button\n class="${brand}-file-upload-button ${brand}-file-upload-button--empty"\n type="button"\n id="${id}"\n aria-labelledby="${id}-label ${id}-comma ${id}"\n >\n <span class="${brand}-body ${brand}-file-upload-button__status"\n aria-live="polite">${i18n.t("noFileChosen")}</span>\n <span class="${brand}-visually-hidden" id="${id}-comma">, </span>\n <span class="${brand}-file-upload-button__pseudo-button-container">\n <span class="${brand}-button\n ${brand}-button--secondary\n ${brand}-file-upload-button__pseudo-button">${i18n.t("chooseFilesButton")}</span>\n <span class="${brand}-body ${brand}-file-upload-button__instruction">${i18n.t("dropInstruction")}</span>\n </span>\n </button>\n `;
|
|
1820
|
+
const button = template.content.firstElementChild;
|
|
1821
|
+
const describedBy = fileInput.getAttribute("aria-describedby");
|
|
1822
|
+
if (describedBy) button.setAttribute("aria-describedby", describedBy);
|
|
1823
|
+
return button;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1346
1826
|
class ImageFieldController extends FileFieldController {
|
|
1347
1827
|
connect() {
|
|
1348
1828
|
super.connect();
|
|
@@ -1364,25 +1844,32 @@ class ImageFieldController extends FileFieldController {
|
|
|
1364
1844
|
}
|
|
1365
1845
|
|
|
1366
1846
|
const Definitions = [ {
|
|
1847
|
+
identifier: "govuk-attachment",
|
|
1848
|
+
controllerConstructor: AttachmentController
|
|
1849
|
+
}, {
|
|
1367
1850
|
identifier: "govuk-document-field",
|
|
1368
1851
|
controllerConstructor: DocumentFieldController
|
|
1852
|
+
}, {
|
|
1853
|
+
identifier: "govuk-file-upload",
|
|
1854
|
+
controllerConstructor: FileUploadController
|
|
1369
1855
|
}, {
|
|
1370
1856
|
identifier: "govuk-image-field",
|
|
1371
1857
|
controllerConstructor: ImageFieldController
|
|
1372
1858
|
} ];
|
|
1373
1859
|
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1860
|
+
let options = {};
|
|
1861
|
+
|
|
1862
|
+
function enhance($scope = document) {
|
|
1377
1863
|
if (!isSupported()) {
|
|
1378
1864
|
console.log(new SupportError);
|
|
1379
1865
|
return;
|
|
1380
1866
|
}
|
|
1381
|
-
const components = [ [ Button,
|
|
1382
|
-
const $scope = (_config$scope = config.scope) != null ? _config$scope : document;
|
|
1867
|
+
const components = [ [ Button, options.button ], [ CharacterCount, options.characterCount ], [ Checkboxes ], [ ErrorSummary, options.errorSummary ], [ FileUpload, options.fileUpload ], [ Radios ], [ PasswordInput, options.passwordInput ] ];
|
|
1383
1868
|
components.forEach(([Component, config]) => {
|
|
1384
|
-
const
|
|
1869
|
+
const selector = `[data-module="${Component.moduleName}"]`;
|
|
1870
|
+
const $elements = [ ...$scope instanceof Element && $scope.matches(selector) ? [ $scope ] : [], ...$scope.querySelectorAll(selector) ];
|
|
1385
1871
|
$elements.forEach($element => {
|
|
1872
|
+
if (isInitialised($element, Component.moduleName)) return;
|
|
1386
1873
|
try {
|
|
1387
1874
|
"defaults" in Component ? new Component($element, config) : new Component($element);
|
|
1388
1875
|
} catch (error) {
|
|
@@ -1392,4 +1879,80 @@ function initAll(config) {
|
|
|
1392
1879
|
});
|
|
1393
1880
|
}
|
|
1394
1881
|
|
|
1395
|
-
|
|
1882
|
+
function markSupport(body) {
|
|
1883
|
+
body.classList.toggle("js-enabled", true);
|
|
1884
|
+
body.classList.toggle("govuk-frontend-supported", "noModule" in HTMLScriptElement.prototype);
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
function supportMarked(body) {
|
|
1888
|
+
const supported = "noModule" in HTMLScriptElement.prototype;
|
|
1889
|
+
return body.classList.contains("js-enabled") && body.classList.contains("govuk-frontend-supported") === supported;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
function observe(body) {
|
|
1893
|
+
new MutationObserver(() => {
|
|
1894
|
+
if (supportMarked(body)) return;
|
|
1895
|
+
markSupport(body);
|
|
1896
|
+
enhance();
|
|
1897
|
+
}).observe(body, {
|
|
1898
|
+
attributes: true,
|
|
1899
|
+
attributeFilter: [ "class" ]
|
|
1900
|
+
});
|
|
1901
|
+
new MutationObserver(mutations => {
|
|
1902
|
+
for (const mutation of mutations) {
|
|
1903
|
+
for (const node of mutation.addedNodes) {
|
|
1904
|
+
if (node instanceof Element) enhance(node);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
}).observe(body, {
|
|
1908
|
+
childList: true,
|
|
1909
|
+
subtree: true
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
function setup(body) {
|
|
1914
|
+
if (!body || body.__govukFormbuilderInit) return;
|
|
1915
|
+
body.__govukFormbuilderInit = true;
|
|
1916
|
+
markSupport(body);
|
|
1917
|
+
enhance();
|
|
1918
|
+
observe(body);
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
let registered = false;
|
|
1922
|
+
|
|
1923
|
+
function register(application = undefined) {
|
|
1924
|
+
if (registered) return;
|
|
1925
|
+
registered = true;
|
|
1926
|
+
(application ?? Application.start()).load(Definitions);
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
function applyConfig(config$1) {
|
|
1930
|
+
if (config$1.brand) config.brand = config$1.brand;
|
|
1931
|
+
options = config$1;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
function initAll(config = {}) {
|
|
1935
|
+
applyConfig(config);
|
|
1936
|
+
register();
|
|
1937
|
+
setup(document.body);
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
let watching = false;
|
|
1941
|
+
|
|
1942
|
+
function start(application = undefined, config = {}) {
|
|
1943
|
+
applyConfig(config);
|
|
1944
|
+
register(application);
|
|
1945
|
+
if (!watching) {
|
|
1946
|
+
watching = true;
|
|
1947
|
+
new MutationObserver(() => setup(document.body)).observe(document.documentElement, {
|
|
1948
|
+
childList: true
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
setup(document.body);
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
var formbuilder = {
|
|
1955
|
+
start: start
|
|
1956
|
+
};
|
|
1957
|
+
|
|
1958
|
+
export { Button, CharacterCount, Checkboxes, ErrorSummary, PasswordInput, Radios, formbuilder as default, initAll };
|