@defra/forms-engine-plugin 0.1.11 → 0.1.13

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.
Files changed (147) hide show
  1. package/.public/javascripts/file-upload.min.js +1 -1
  2. package/.public/javascripts/file-upload.min.js.map +1 -1
  3. package/.server/client/javascripts/file-upload.js +45 -4
  4. package/.server/client/javascripts/file-upload.js.map +1 -1
  5. package/.server/server/constants.js +2 -0
  6. package/.server/server/constants.js.map +1 -1
  7. package/.server/server/index.js +1 -1
  8. package/.server/server/index.js.map +1 -1
  9. package/.server/server/plugins/engine/components/AutocompleteField.js +2 -0
  10. package/.server/server/plugins/engine/components/AutocompleteField.js.map +1 -1
  11. package/.server/server/plugins/engine/components/CheckboxesField.js +3 -4
  12. package/.server/server/plugins/engine/components/CheckboxesField.js.map +1 -1
  13. package/.server/server/plugins/engine/components/ComponentCollection.js +37 -16
  14. package/.server/server/plugins/engine/components/ComponentCollection.js.map +1 -1
  15. package/.server/server/plugins/engine/components/DatePartsField.js +36 -2
  16. package/.server/server/plugins/engine/components/DatePartsField.js.map +1 -1
  17. package/.server/server/plugins/engine/components/EmailAddressField.js +19 -3
  18. package/.server/server/plugins/engine/components/EmailAddressField.js.map +1 -1
  19. package/.server/server/plugins/engine/components/FileUploadField.js +44 -4
  20. package/.server/server/plugins/engine/components/FileUploadField.js.map +1 -1
  21. package/.server/server/plugins/engine/components/FormComponent.js +14 -2
  22. package/.server/server/plugins/engine/components/FormComponent.js.map +1 -1
  23. package/.server/server/plugins/engine/components/ListFormComponent.js +16 -3
  24. package/.server/server/plugins/engine/components/ListFormComponent.js.map +1 -1
  25. package/.server/server/plugins/engine/components/Markdown.js +24 -0
  26. package/.server/server/plugins/engine/components/Markdown.js.map +1 -0
  27. package/.server/server/plugins/engine/components/MonthYearField.js +30 -2
  28. package/.server/server/plugins/engine/components/MonthYearField.js.map +1 -1
  29. package/.server/server/plugins/engine/components/MultilineTextField.js +32 -3
  30. package/.server/server/plugins/engine/components/MultilineTextField.js.map +1 -1
  31. package/.server/server/plugins/engine/components/NumberField.js +28 -3
  32. package/.server/server/plugins/engine/components/NumberField.js.map +1 -1
  33. package/.server/server/plugins/engine/components/SelectionControlField.js +14 -0
  34. package/.server/server/plugins/engine/components/SelectionControlField.js.map +1 -1
  35. package/.server/server/plugins/engine/components/TelephoneNumberField.js +19 -3
  36. package/.server/server/plugins/engine/components/TelephoneNumberField.js.map +1 -1
  37. package/.server/server/plugins/engine/components/TextField.js +22 -3
  38. package/.server/server/plugins/engine/components/TextField.js.map +1 -1
  39. package/.server/server/plugins/engine/components/UkAddressField.js +29 -0
  40. package/.server/server/plugins/engine/components/UkAddressField.js.map +1 -1
  41. package/.server/server/plugins/engine/components/YesNoField.js +18 -0
  42. package/.server/server/plugins/engine/components/YesNoField.js.map +1 -1
  43. package/.server/server/plugins/engine/components/helpers.js +16 -0
  44. package/.server/server/plugins/engine/components/helpers.js.map +1 -1
  45. package/.server/server/plugins/engine/components/index.js +1 -0
  46. package/.server/server/plugins/engine/components/index.js.map +1 -1
  47. package/.server/server/plugins/engine/configureEnginePlugin.js +3 -1
  48. package/.server/server/plugins/engine/configureEnginePlugin.js.map +1 -1
  49. package/.server/server/plugins/engine/helpers.js +38 -18
  50. package/.server/server/plugins/engine/helpers.js.map +1 -1
  51. package/.server/server/plugins/engine/models/FormModel.js +60 -2
  52. package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
  53. package/.server/server/plugins/engine/models/SummaryViewModel.js +3 -2
  54. package/.server/server/plugins/engine/models/SummaryViewModel.js.map +1 -1
  55. package/.server/server/plugins/engine/outputFormatters/human/v1.js +1 -1
  56. package/.server/server/plugins/engine/outputFormatters/human/v1.js.map +1 -1
  57. package/.server/server/plugins/engine/pageControllers/PageController.js +13 -5
  58. package/.server/server/plugins/engine/pageControllers/PageController.js.map +1 -1
  59. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +2 -2
  60. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
  61. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +19 -5
  62. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
  63. package/.server/server/plugins/engine/pageControllers/validationOptions.js +6 -11
  64. package/.server/server/plugins/engine/pageControllers/validationOptions.js.map +1 -1
  65. package/.server/server/plugins/engine/plugin.js +5 -4
  66. package/.server/server/plugins/engine/plugin.js.map +1 -1
  67. package/.server/server/plugins/engine/services/notifyService.js +1 -4
  68. package/.server/server/plugins/engine/services/notifyService.js.map +1 -1
  69. package/.server/server/plugins/engine/services/uploadService.js +5 -3
  70. package/.server/server/plugins/engine/services/uploadService.js.map +1 -1
  71. package/.server/server/plugins/engine/types.js.map +1 -1
  72. package/.server/server/plugins/engine/views/components/html.html +1 -1
  73. package/.server/server/plugins/engine/views/components/markdown.html +5 -0
  74. package/.server/server/plugins/engine/views/summary.html +7 -1
  75. package/.server/server/plugins/nunjucks/context.js +7 -6
  76. package/.server/server/plugins/nunjucks/context.js.map +1 -1
  77. package/.server/server/plugins/nunjucks/enviroment.test.js +6 -3
  78. package/.server/server/plugins/nunjucks/enviroment.test.js.map +1 -1
  79. package/.server/server/utils/type-utils.js +8 -0
  80. package/.server/server/utils/type-utils.js.map +1 -0
  81. package/.server/typings/joi/index.d.js.map +1 -1
  82. package/package.json +3 -3
  83. package/src/client/javascripts/file-upload.js +60 -4
  84. package/src/server/constants.js +2 -0
  85. package/src/server/index.test.ts +34 -29
  86. package/src/server/index.ts +2 -1
  87. package/src/server/plugins/engine/components/AutocompleteField.test.ts +71 -3
  88. package/src/server/plugins/engine/components/AutocompleteField.ts +6 -2
  89. package/src/server/plugins/engine/components/CheckboxesField.test.ts +40 -8
  90. package/src/server/plugins/engine/components/CheckboxesField.ts +7 -3
  91. package/src/server/plugins/engine/components/ComponentCollection.ts +45 -18
  92. package/src/server/plugins/engine/components/DatePartsField.test.ts +13 -4
  93. package/src/server/plugins/engine/components/DatePartsField.ts +29 -8
  94. package/src/server/plugins/engine/components/EmailAddressField.test.ts +51 -1
  95. package/src/server/plugins/engine/components/EmailAddressField.ts +17 -2
  96. package/src/server/plugins/engine/components/FileUploadField.test.ts +53 -0
  97. package/src/server/plugins/engine/components/FileUploadField.ts +52 -3
  98. package/src/server/plugins/engine/components/FormComponent.ts +24 -2
  99. package/src/server/plugins/engine/components/ListFormComponent.ts +16 -2
  100. package/src/server/plugins/engine/components/Markdown.test.ts +48 -0
  101. package/src/server/plugins/engine/components/Markdown.ts +29 -0
  102. package/src/server/plugins/engine/components/MonthYearField.test.ts +35 -0
  103. package/src/server/plugins/engine/components/MonthYearField.ts +34 -9
  104. package/src/server/plugins/engine/components/MultilineTextField.test.ts +83 -5
  105. package/src/server/plugins/engine/components/MultilineTextField.ts +37 -2
  106. package/src/server/plugins/engine/components/NumberField.test.ts +24 -2
  107. package/src/server/plugins/engine/components/NumberField.ts +23 -3
  108. package/src/server/plugins/engine/components/RadiosField.test.ts +10 -1
  109. package/src/server/plugins/engine/components/SelectField.test.ts +2 -1
  110. package/src/server/plugins/engine/components/SelectionControlField.ts +14 -0
  111. package/src/server/plugins/engine/components/TelephoneNumberField.test.ts +30 -2
  112. package/src/server/plugins/engine/components/TelephoneNumberField.ts +17 -2
  113. package/src/server/plugins/engine/components/TextField.test.ts +33 -1
  114. package/src/server/plugins/engine/components/TextField.ts +17 -2
  115. package/src/server/plugins/engine/components/UkAddressField.test.ts +46 -3
  116. package/src/server/plugins/engine/components/UkAddressField.ts +28 -0
  117. package/src/server/plugins/engine/components/YesNoField.test.ts +9 -1
  118. package/src/server/plugins/engine/components/YesNoField.ts +24 -0
  119. package/src/server/plugins/engine/components/helpers.test.ts +24 -0
  120. package/src/server/plugins/engine/components/helpers.ts +39 -0
  121. package/src/server/plugins/engine/components/index.ts +1 -0
  122. package/src/server/plugins/engine/configureEnginePlugin.ts +13 -3
  123. package/src/server/plugins/engine/helpers.test.ts +71 -20
  124. package/src/server/plugins/engine/helpers.ts +46 -19
  125. package/src/server/plugins/engine/models/FormModel.test.ts +91 -1
  126. package/src/server/plugins/engine/models/FormModel.ts +86 -3
  127. package/src/server/plugins/engine/models/SummaryViewModel.test.ts +46 -7
  128. package/src/server/plugins/engine/models/SummaryViewModel.ts +7 -3
  129. package/src/server/plugins/engine/outputFormatters/human/v1.test.ts +1 -2
  130. package/src/server/plugins/engine/outputFormatters/human/v1.ts +1 -1
  131. package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +1 -0
  132. package/src/server/plugins/engine/pageControllers/PageController.test.ts +9 -6
  133. package/src/server/plugins/engine/pageControllers/PageController.ts +15 -5
  134. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +2 -2
  135. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +21 -6
  136. package/src/server/plugins/engine/pageControllers/validationOptions.ts +31 -17
  137. package/src/server/plugins/engine/plugin.ts +9 -5
  138. package/src/server/plugins/engine/services/notifyService.ts +1 -2
  139. package/src/server/plugins/engine/services/uploadService.js +10 -6
  140. package/src/server/plugins/engine/types.ts +10 -1
  141. package/src/server/plugins/engine/views/components/html.html +1 -1
  142. package/src/server/plugins/engine/views/components/markdown.html +5 -0
  143. package/src/server/plugins/engine/views/summary.html +7 -1
  144. package/src/server/plugins/nunjucks/context.js +5 -5
  145. package/src/server/plugins/nunjucks/enviroment.test.js +9 -3
  146. package/src/server/utils/type-utils.ts +15 -0
  147. package/src/typings/joi/index.d.ts +8 -0
@@ -1,2 +1,2 @@
1
- var e={d:(t,n)=>{for(var r in n)e.o(n,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:n[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},t={};e.d(t,{C:()=>n,u:()=>l});const n=300;function r(e,t,n){t&&(t.innerHTML=`\n <div class="govuk-error-summary" data-module="govuk-error-summary">\n <div role="alert">\n <h2 class="govuk-error-summary__title" id="error-summary-title">\n There is a problem\n </h2>\n <div class="govuk-error-summary__body">\n <ul class="govuk-list govuk-error-summary__list">\n <li>\n <a href="#file-upload">${e}</a>\n </li>\n </ul>\n </div>\n </div>\n </div>\n `,n.setAttribute("aria-describedby","error-summary-title"))}function o(){window.history.replaceState(null,"",window.location.href),window.location.href=window.location.pathname}function l(){var e;const t=document.querySelector('form:has(input[type="file"])'),l=t?t.querySelector('input[type="file"]'):null,a=t?t.querySelector(".upload-file-button"):null,i=null!=(e=Array.from(document.querySelectorAll("button.govuk-button")).find((e=>{var t;return"Continue"===(null==(t=e.textContent)?void 0:t.trim())})))?e:null,u=document.querySelector(".govuk-error-summary-container");if(!t||!l||!a)return;const s=t;let d=null,c=!1;const m=s.dataset.uploadId;l.addEventListener("change",(()=>{u&&(u.innerHTML=""),l.files&&l.files.length>0&&(d=l.files[0])})),a.addEventListener("click",(e=>{if(!d)return e.preventDefault(),void r("Select a file",u,l);c?e.preventDefault():(c=!0,function(e,t,n,r,o){(function(e,t,n){var r;const o=document.getElementById("uploadedFilesContainer"),l=o?o.closest("form"):null;if(!(l&&l instanceof HTMLFormElement))return;const a=l.querySelector("p.govuk-body");if(!a)return;const i=function(e,t){let n=null==e?void 0:e.querySelector("#statusInformation");if(!n){n=document.createElement("div"),n.id="statusInformation",n.className="govuk-visually-hidden",n.setAttribute("aria-live","polite");try{!function(e,t,n){var r;null!=t&&t.nextSibling&&t.parentNode===e?e.insertBefore(n,t.nextSibling):(null!=(r=null==t?void 0:t.parentNode)?r:e).appendChild(n)}(e,t,n)}catch(r){try{null==e||e.appendChild(n)}catch(o){document.body.appendChild(n)}}}return n}(l,a),u=n.querySelector('input[type="file"]');u&&u.setAttribute("aria-describedby","statusInformation");const s=function(e,t){let n=e.querySelector("dl.govuk-summary-list");if(!n){n=document.createElement("dl"),n.className="govuk-summary-list govuk-summary-list--long-key";const r=e.querySelector(".govuk-button");r?e.insertBefore(n,r):e.insertBefore(n,t.nextSibling)}return n}(l,a),d=document.querySelector(`[data-filename="${null==e?void 0:e.name}"]`);d&&d.remove();const c=function(e,t){var n,r;const o=document.createElement("div");return o.className="govuk-summary-list__row",o.setAttribute("data-filename",null!=(n=null==e?void 0:e.name)?n:""),o.innerHTML=`\n <dt class="govuk-summary-list__key">\n ${null!=(r=null==e?void 0:e.name)?r:""}\n </dt>\n <dd class="govuk-summary-list__value">\n <strong class="govuk-tag govuk-tag--yellow">${t}</strong>\n </dd>\n <dd class="govuk-summary-list__actions">\n </dd>\n `,o}(e,t);s.insertBefore(c,s.firstChild),i.textContent=`${null!=(r=null==e?void 0:e.name)?r:""} ${t}`})(o,"Uploading…",e),t.focus(),setTimeout((()=>{t.disabled=!0,n.disabled=!0,r.disabled=!0}),100)}(s,l,a,i,d),function(e,t,l,a,i,u){var s;if(!t.action||!u)return!1;e.preventDefault();const d=new FormData(t),c=!!t.dataset.proxyUrl,m=null!=(s=t.dataset.proxyUrl)?s:t.action,v={method:"POST",body:d,redirect:c?"follow":"manual"};c&&(v.mode="no-cors"),fetch(m,v).then((()=>{!function(e){let t=0;const r=setInterval((()=>{if(t++,t>=n)return clearInterval(r),void o();fetch(`/upload-status/${e}`,{headers:{Accept:"application/json"}}).then((e=>{if(!e.ok)throw new Error("Network response was not ok");return e.json()})).then((e=>{"ready"===e.uploadStatus&&(clearInterval(r),o())})).catch((()=>{clearInterval(r),o()}))}),1e3)}(u)})).catch((()=>(l.disabled=!1,a.disabled=!1,r("There was a problem uploading the file",i,l),null)))}(e,s,l,a,u,m))}))}var a=t.C,i=t.u;export{a as MAX_POLLING_DURATION,i as initFileUpload};
1
+ var e={d:(t,n)=>{for(var r in n)e.o(n,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:n[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},t={};e.d(t,{Ct:()=>n,n1:()=>u,u8:()=>i});const n=300,r="aria-describedby",o="error-summary-title";function l(e,t,n){if(document.querySelector(".govuk-error-summary"))return void(document.getElementById(o)?n.setAttribute(r,o):n.removeAttribute(r));t&&(t.innerHTML=`\n <div class="govuk-error-summary" data-module="govuk-error-summary">\n <div role="alert">\n <h2 class="govuk-error-summary__title" id="${o}">\n There is a problem\n </h2>\n <div class="govuk-error-summary__body">\n <ul class="govuk-list govuk-error-summary__list">\n <li>\n <a href="#file-upload">${e}</a>\n </li>\n </ul>\n </div>\n </div>\n </div>\n `,n.setAttribute(r,o));const l=n.closest(".govuk-form-group");if(l){l.classList.add("govuk-form-group--error"),n.classList.add("govuk-file-upload--error");const t=n.id;let o=document.getElementById(`${t}-error`);o||(o=document.createElement("p"),o.id=`${t}-error`,o.className="govuk-error-message",o.innerHTML=`<span class="govuk-visually-hidden">Error:</span> ${e}`,l.insertBefore(o,n)),n.setAttribute(r,`error-summary-title ${t}-error`)}}function a(){window.history.replaceState(null,"",window.location.href),window.location.href=window.location.pathname}function u(e,t){const n=e.split("/").filter((e=>e));return`${n.length>0?`/${n[0]}`:""}/upload-status/${t}`}function i(){var e;const t=document.querySelector('form:has(input[type="file"])'),o=t?t.querySelector('input[type="file"]'):null,i=t?t.querySelector(".upload-file-button"):null,s=null!=(e=Array.from(document.querySelectorAll("button.govuk-button")).find((e=>{var t;return"Continue"===(null==(t=e.textContent)?void 0:t.trim())})))?e:null,d=document.querySelector(".govuk-error-summary-container");if(!t||!o||!i)return;const c=t;let m=null,v=!1;const f=c.dataset.uploadId;o.addEventListener("change",(()=>{d&&(d.innerHTML=""),o.files&&o.files.length>0&&(m=o.files[0])})),i.addEventListener("click",(e=>{if(!m)return e.preventDefault(),void l("Select a file",d,o);v?e.preventDefault():(v=!0,function(e,t,n,o,l){(function(e,t,n){var o;const l=document.getElementById("uploadedFilesContainer"),a=l?l.closest("form"):null;if(!(a&&a instanceof HTMLFormElement))return;const u=a.querySelector("p.govuk-body");if(!u)return;const i=function(e,t){let n=null==e?void 0:e.querySelector("#statusInformation");if(!n){n=document.createElement("div"),n.id="statusInformation",n.className="govuk-visually-hidden",n.setAttribute("aria-live","polite");try{!function(e,t,n){var r;null!=t&&t.nextSibling&&t.parentNode===e?e.insertBefore(n,t.nextSibling):(null!=(r=null==t?void 0:t.parentNode)?r:e).appendChild(n)}(e,t,n)}catch(r){try{null==e||e.appendChild(n)}catch(o){document.body.appendChild(n)}}}return n}(a,u),s=n.querySelector('input[type="file"]');s&&s.setAttribute(r,"statusInformation");const d=function(e,t){let n=e.querySelector("dl.govuk-summary-list");if(!n){n=document.createElement("dl"),n.className="govuk-summary-list govuk-summary-list--long-key";const r=e.querySelector(".govuk-button");r?e.insertBefore(n,r):e.insertBefore(n,t.nextSibling)}return n}(a,u),c=document.querySelector(`[data-filename="${null==e?void 0:e.name}"]`);c&&c.remove();const m=function(e,t){var n,r;const o=document.createElement("div");return o.className="govuk-summary-list__row",o.setAttribute("data-filename",null!=(n=null==e?void 0:e.name)?n:""),o.innerHTML=`\n <dt class="govuk-summary-list__key">\n ${null!=(r=null==e?void 0:e.name)?r:""}\n </dt>\n <dd class="govuk-summary-list__value">\n <strong class="govuk-tag govuk-tag--yellow">${t}</strong>\n </dd>\n <dd class="govuk-summary-list__actions">\n </dd>\n `,o}(e,t);d.insertBefore(m,d.firstChild),i.textContent=`${null!=(o=null==e?void 0:e.name)?o:""} ${t}`})(l,"Uploading…",e),t.focus(),setTimeout((()=>{t.disabled=!0,n.disabled=!0,o.disabled=!0}),100)}(c,o,i,s,m),function(e,t,r,o,i,s){var d;if(!t.action||!s)return!1;e.preventDefault();const c=new FormData(t),m=!!t.dataset.proxyUrl,v=null!=(d=t.dataset.proxyUrl)?d:t.action,f={method:"POST",body:c,redirect:m?"follow":"manual"};m&&(f.mode="no-cors"),fetch(v,f).then((()=>{!function(e){let t=0;const r=setInterval((()=>{if(t++,t>=n)return clearInterval(r),void a();const o=u(window.location.pathname,e);fetch(o,{headers:{Accept:"application/json"}}).then((e=>{if(!e.ok)throw new Error("Network response was not ok");return e.json()})).then((e=>{"ready"===e.uploadStatus&&(clearInterval(r),a())})).catch((()=>{clearInterval(r),a()}))}),1e3)}(s)})).catch((()=>(r.disabled=!1,o.disabled=!1,l("There was a problem uploading the file",i,r),null)))}(e,c,o,i,d,f))}))}var s=t.Ct,d=t.n1,c=t.u8;export{s as MAX_POLLING_DURATION,d as buildUploadStatusUrl,c as initFileUpload};
2
2
  //# sourceMappingURL=file-upload.min.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"javascripts/file-upload.min.js","mappings":"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","sources":["webpack:///webpack/bootstrap","webpack:///webpack/runtime/define property getters","webpack:///webpack/runtime/hasOwnProperty shorthand","webpack:///./javascripts/file-upload.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","export const MAX_POLLING_DURATION = 300 // 5 minutes\n\n/**\n * Creates or updates status announcer for screen readers\n * @param {HTMLElement | null} form - The form element\n * @param {HTMLElement | null} fileCountP - The file count paragraph element\n * @returns {HTMLElement} The status announcer element\n */\nfunction createOrUpdateStatusAnnouncer(form, fileCountP) {\n let statusAnnouncer = form?.querySelector('#statusInformation')\n\n if (!statusAnnouncer) {\n statusAnnouncer = document.createElement('div')\n statusAnnouncer.id = 'statusInformation'\n statusAnnouncer.className = 'govuk-visually-hidden'\n statusAnnouncer.setAttribute('aria-live', 'polite')\n\n // multiple fallbacks to ensure the status announcer is always added to the DOM\n // this helps with cross-browser compatibility and unexpected DOM structures encountered during QA\n try {\n addStatusAnnouncerToDOM(\n asHTMLElement(form),\n asHTMLElement(fileCountP),\n asHTMLElement(statusAnnouncer)\n )\n } catch {\n try {\n form?.appendChild(statusAnnouncer)\n } catch {\n document.body.appendChild(statusAnnouncer)\n }\n }\n }\n\n return /** @type {HTMLElement} */ (statusAnnouncer)\n}\n\n/**\n * Helper function to add the status announcer to the DOM\n * @param {HTMLElement} form - The form element\n * @param {HTMLElement | null} fileCountP - The file count paragraph element\n * @param {HTMLElement} statusAnnouncer - The status announcer element to add\n */\nfunction addStatusAnnouncerToDOM(form, fileCountP, statusAnnouncer) {\n if (fileCountP?.nextSibling && fileCountP.parentNode === form) {\n form.insertBefore(statusAnnouncer, fileCountP.nextSibling)\n return\n }\n\n const parentElement = fileCountP?.parentNode ?? form\n parentElement.appendChild(statusAnnouncer)\n}\n\n/**\n * Finds an existing summary list or creates a new one\n * @param {HTMLFormElement} form - The form element\n * @param {HTMLElement} fileCountP - The file count paragraph element\n * @returns {HTMLElement} The summary list element\n */\nfunction findOrCreateSummaryList(form, fileCountP) {\n let summaryList = form.querySelector('dl.govuk-summary-list')\n\n if (!summaryList) {\n summaryList = document.createElement('dl')\n summaryList.className = 'govuk-summary-list govuk-summary-list--long-key'\n\n const continueButton = form.querySelector('.govuk-button')\n\n if (continueButton) {\n form.insertBefore(summaryList, continueButton)\n } else {\n form.insertBefore(summaryList, fileCountP.nextSibling)\n }\n }\n\n return /** @type {HTMLElement} */ (summaryList)\n}\n\n/**\n * Creates a file row element for the summary list\n * @param {File | null} selectedFile - The selected file\n * @param {string} statusText - The status to display\n * @returns {HTMLElement} The created row element\n */\nfunction createFileRow(selectedFile, statusText) {\n const row = document.createElement('div')\n row.className = 'govuk-summary-list__row'\n row.setAttribute('data-filename', selectedFile?.name ?? '')\n row.innerHTML = `\n <dt class=\"govuk-summary-list__key\">\n ${selectedFile?.name ?? ''}\n </dt>\n <dd class=\"govuk-summary-list__value\">\n <strong class=\"govuk-tag govuk-tag--yellow\">${statusText}</strong>\n </dd>\n <dd class=\"govuk-summary-list__actions\">\n </dd>\n `\n return row\n}\n\n/**\n * Renders or updates the file summary box for the selected file\n * @param {File | null} selectedFile - The selected file\n * @param {string} statusText - The status to display\n * @param {HTMLElement} form - The form element\n */\nfunction renderSummary(selectedFile, statusText, form) {\n const container = document.getElementById('uploadedFilesContainer')\n const uploadForm = container ? container.closest('form') : null\n\n if (!uploadForm || !(uploadForm instanceof HTMLFormElement)) {\n return\n }\n\n const fileCountP = uploadForm.querySelector('p.govuk-body')\n\n if (!fileCountP) {\n return\n }\n\n const statusAnnouncer = createOrUpdateStatusAnnouncer(\n /** @type {HTMLElement} */ (uploadForm),\n /** @type {HTMLElement | null} */ (fileCountP)\n )\n\n const fileInput = form.querySelector('input[type=\"file\"]')\n\n if (fileInput) {\n fileInput.setAttribute('aria-describedby', 'statusInformation')\n }\n\n const summaryList = findOrCreateSummaryList(\n /** @type {HTMLFormElement} */ (uploadForm),\n /** @type {HTMLElement} */ (fileCountP)\n )\n\n const existingRow = document.querySelector(\n `[data-filename=\"${selectedFile?.name}\"]`\n )\n\n if (existingRow) {\n existingRow.remove()\n }\n\n const row = createFileRow(selectedFile, statusText)\n summaryList.insertBefore(row, summaryList.firstChild)\n statusAnnouncer.textContent = `${selectedFile?.name ?? ''} ${statusText}`\n}\n\n/**\n * Shows an error message using the GOV.UK error summary component\n * @param {string} message - The error message to display\n * @param {HTMLElement | null} errorSummary - The error summary container\n * @param {HTMLInputElement} fileInput - The file input element\n * @returns {void}\n */\nfunction showError(message, errorSummary, fileInput) {\n if (errorSummary) {\n errorSummary.innerHTML = `\n <div class=\"govuk-error-summary\" data-module=\"govuk-error-summary\">\n <div role=\"alert\">\n <h2 class=\"govuk-error-summary__title\" id=\"error-summary-title\">\n There is a problem\n </h2>\n <div class=\"govuk-error-summary__body\">\n <ul class=\"govuk-list govuk-error-summary__list\">\n <li>\n <a href=\"#file-upload\">${message}</a>\n </li>\n </ul>\n </div>\n </div>\n </div>\n `\n fileInput.setAttribute('aria-describedby', 'error-summary-title')\n }\n}\n\n/**\n * Helper to safely convert an Element to HTMLElement\n * @param {Element | null} element - The element to convert\n */\nfunction asHTMLElement(element) {\n return /** @type {HTMLElement} */ (element)\n}\n\nfunction reloadPage() {\n window.history.replaceState(null, '', window.location.href)\n window.location.href = window.location.pathname\n}\n\n/**\n * Polls the upload status endpoint until the file is ready or timeout occurs\n * @param {string} uploadId - The upload ID to check\n */\nfunction pollUploadStatus(uploadId) {\n let attempts = 0\n const interval = setInterval(() => {\n attempts++\n\n if (attempts >= MAX_POLLING_DURATION) {\n clearInterval(interval)\n reloadPage()\n return\n }\n\n fetch(`/upload-status/${uploadId}`, {\n headers: {\n Accept: 'application/json'\n }\n })\n .then((response) => {\n if (!response.ok) {\n throw new Error('Network response was not ok')\n }\n return response.json()\n })\n .then((data) => {\n if (data.uploadStatus === 'ready') {\n clearInterval(interval)\n reloadPage()\n }\n })\n .catch(() => {\n clearInterval(interval)\n reloadPage()\n })\n }, 1000)\n}\n\n/**\n * Handle standard form submission for file upload\n * @param {HTMLFormElement} formElement - The form element\n * @param {HTMLInputElement} fileInput - The file input element\n * @param {HTMLButtonElement} uploadButton - The upload button\n * @param {HTMLButtonElement} continueButton - The continue button\n * @param {File | null} selectedFile - The selected file\n */\nfunction handleStandardFormSubmission(\n formElement,\n fileInput,\n uploadButton,\n continueButton,\n selectedFile\n) {\n renderSummary(selectedFile, 'Uploading…', formElement)\n\n fileInput.focus()\n\n setTimeout(() => {\n fileInput.disabled = true\n uploadButton.disabled = true\n continueButton.disabled = true\n }, 100)\n}\n\n/**\n * Handle AJAX form submission with upload ID\n * @param {Event} event - The click event\n * @param {HTMLFormElement} formElement - The form element\n * @param {HTMLInputElement} fileInput - The file input element\n * @param {HTMLButtonElement} uploadButton - The upload button\n * @param {HTMLElement | null} errorSummary - The error summary container\n * @param {string | undefined} uploadId - The upload ID\n * @returns {boolean} Whether the event was handled\n */\nfunction handleAjaxFormSubmission(\n event,\n formElement,\n fileInput,\n uploadButton,\n errorSummary,\n uploadId\n) {\n if (!formElement.action || !uploadId) {\n return false\n }\n\n event.preventDefault()\n\n const formData = new FormData(formElement)\n const isLocalDev = !!formElement.dataset.proxyUrl\n const uploadUrl = formElement.dataset.proxyUrl ?? formElement.action\n\n const fetchOptions = /** @type {RequestInit} */ ({\n method: 'POST',\n body: formData,\n redirect: isLocalDev ? 'follow' : 'manual' // follow mode if local development with the proxy\n })\n\n // no-cors mode if needed local development with the proxy\n if (isLocalDev) {\n fetchOptions.mode = 'no-cors'\n }\n\n fetch(uploadUrl, fetchOptions)\n .then(() => {\n pollUploadStatus(uploadId)\n })\n .catch(() => {\n fileInput.disabled = false\n uploadButton.disabled = false\n\n showError(\n 'There was a problem uploading the file',\n errorSummary,\n fileInput\n )\n\n return null\n })\n\n return true\n}\n\nexport function initFileUpload() {\n const form = document.querySelector('form:has(input[type=\"file\"])')\n /** @type {HTMLInputElement | null} */\n const fileInput = form ? form.querySelector('input[type=\"file\"]') : null\n /** @type {HTMLButtonElement | null} */\n const uploadButton = form ? form.querySelector('.upload-file-button') : null\n const continueButton =\n /** @type {HTMLButtonElement} */ (\n Array.from(document.querySelectorAll('button.govuk-button')).find(\n (button) => button.textContent?.trim() === 'Continue'\n )\n ) ?? null\n\n const errorSummary = document.querySelector('.govuk-error-summary-container')\n\n if (!form || !fileInput || !uploadButton) {\n return\n }\n\n const formElement = /** @type {HTMLFormElement} */ (form)\n /** @type {File | null} */\n let selectedFile = null\n let isSubmitting = false\n const uploadId = formElement.dataset.uploadId\n\n fileInput.addEventListener('change', () => {\n if (errorSummary) {\n errorSummary.innerHTML = ''\n }\n if (fileInput.files && fileInput.files.length > 0) {\n selectedFile = fileInput.files[0]\n }\n })\n\n uploadButton.addEventListener('click', (event) => {\n if (!selectedFile) {\n event.preventDefault()\n showError(\n 'Select a file',\n /** @type {HTMLElement | null} */ (errorSummary),\n fileInput\n )\n return\n }\n\n if (isSubmitting) {\n event.preventDefault()\n return\n }\n\n isSubmitting = true\n\n handleStandardFormSubmission(\n formElement,\n fileInput,\n uploadButton,\n continueButton,\n selectedFile\n )\n\n handleAjaxFormSubmission(\n event,\n formElement,\n fileInput,\n uploadButton,\n /** @type {HTMLElement | null} */ (errorSummary),\n uploadId\n )\n })\n}\n"],"names":["__webpack_require__","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","MAX_POLLING_DURATION","showError","message","errorSummary","fileInput","innerHTML","setAttribute","reloadPage","window","history","replaceState","location","href","pathname","initFileUpload","_Array$from$find","form","document","querySelector","uploadButton","continueButton","Array","from","querySelectorAll","find","button","_button$textContent","textContent","trim","formElement","selectedFile","isSubmitting","uploadId","dataset","addEventListener","files","length","event","preventDefault","statusText","_selectedFile$name3","container","getElementById","uploadForm","closest","HTMLFormElement","fileCountP","statusAnnouncer","createElement","id","className","_fileCountP$parentNod","nextSibling","parentNode","insertBefore","appendChild","addStatusAnnouncerToDOM","_unused","_unused2","body","createOrUpdateStatusAnnouncer","summaryList","findOrCreateSummaryList","existingRow","name","remove","row","_selectedFile$name","_selectedFile$name2","createFileRow","firstChild","renderSummary","focus","setTimeout","disabled","handleStandardFormSubmission","_formElement$dataset$","action","formData","FormData","isLocalDev","proxyUrl","uploadUrl","fetchOptions","method","redirect","mode","fetch","then","attempts","interval","setInterval","clearInterval","headers","Accept","response","ok","Error","json","data","uploadStatus","catch","pollUploadStatus","handleAjaxFormSubmission"],"sourceRoot":""}
1
+ {"version":3,"file":"javascripts/file-upload.min.js","mappings":"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","sources":["webpack:///webpack/bootstrap","webpack:///webpack/runtime/define property getters","webpack:///webpack/runtime/hasOwnProperty shorthand","webpack:///./javascripts/file-upload.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","export const MAX_POLLING_DURATION = 300 // 5 minutes\nconst ARIA_DESCRIBEDBY = 'aria-describedby'\nconst ERROR_SUMMARY_TITLE_ID = 'error-summary-title'\n\n/**\n * Creates or updates status announcer for screen readers\n * @param {HTMLElement | null} form - The form element\n * @param {HTMLElement | null} fileCountP - The file count paragraph element\n * @returns {HTMLElement} The status announcer element\n */\nfunction createOrUpdateStatusAnnouncer(form, fileCountP) {\n let statusAnnouncer = form?.querySelector('#statusInformation')\n\n if (!statusAnnouncer) {\n statusAnnouncer = document.createElement('div')\n statusAnnouncer.id = 'statusInformation'\n statusAnnouncer.className = 'govuk-visually-hidden'\n statusAnnouncer.setAttribute('aria-live', 'polite')\n\n // multiple fallbacks to ensure the status announcer is always added to the DOM\n // this helps with cross-browser compatibility and unexpected DOM structures encountered during QA\n try {\n addStatusAnnouncerToDOM(\n asHTMLElement(form),\n asHTMLElement(fileCountP),\n asHTMLElement(statusAnnouncer)\n )\n } catch {\n try {\n form?.appendChild(statusAnnouncer)\n } catch {\n document.body.appendChild(statusAnnouncer)\n }\n }\n }\n\n return /** @type {HTMLElement} */ (statusAnnouncer)\n}\n\n/**\n * Helper function to add the status announcer to the DOM\n * @param {HTMLElement} form - The form element\n * @param {HTMLElement | null} fileCountP - The file count paragraph element\n * @param {HTMLElement} statusAnnouncer - The status announcer element to add\n */\nfunction addStatusAnnouncerToDOM(form, fileCountP, statusAnnouncer) {\n if (fileCountP?.nextSibling && fileCountP.parentNode === form) {\n form.insertBefore(statusAnnouncer, fileCountP.nextSibling)\n return\n }\n\n const parentElement = fileCountP?.parentNode ?? form\n parentElement.appendChild(statusAnnouncer)\n}\n\n/**\n * Finds an existing summary list or creates a new one\n * @param {HTMLFormElement} form - The form element\n * @param {HTMLElement} fileCountP - The file count paragraph element\n * @returns {HTMLElement} The summary list element\n */\nfunction findOrCreateSummaryList(form, fileCountP) {\n let summaryList = form.querySelector('dl.govuk-summary-list')\n\n if (!summaryList) {\n summaryList = document.createElement('dl')\n summaryList.className = 'govuk-summary-list govuk-summary-list--long-key'\n\n const continueButton = form.querySelector('.govuk-button')\n\n if (continueButton) {\n form.insertBefore(summaryList, continueButton)\n } else {\n form.insertBefore(summaryList, fileCountP.nextSibling)\n }\n }\n\n return /** @type {HTMLElement} */ (summaryList)\n}\n\n/**\n * Creates a file row element for the summary list\n * @param {File | null} selectedFile - The selected file\n * @param {string} statusText - The status to display\n * @returns {HTMLElement} The created row element\n */\nfunction createFileRow(selectedFile, statusText) {\n const row = document.createElement('div')\n row.className = 'govuk-summary-list__row'\n row.setAttribute('data-filename', selectedFile?.name ?? '')\n row.innerHTML = `\n <dt class=\"govuk-summary-list__key\">\n ${selectedFile?.name ?? ''}\n </dt>\n <dd class=\"govuk-summary-list__value\">\n <strong class=\"govuk-tag govuk-tag--yellow\">${statusText}</strong>\n </dd>\n <dd class=\"govuk-summary-list__actions\">\n </dd>\n `\n return row\n}\n\n/**\n * Renders or updates the file summary box for the selected file\n * @param {File | null} selectedFile - The selected file\n * @param {string} statusText - The status to display\n * @param {HTMLElement} form - The form element\n */\nfunction renderSummary(selectedFile, statusText, form) {\n const container = document.getElementById('uploadedFilesContainer')\n const uploadForm = container ? container.closest('form') : null\n\n if (!uploadForm || !(uploadForm instanceof HTMLFormElement)) {\n return\n }\n\n const fileCountP = uploadForm.querySelector('p.govuk-body')\n\n if (!fileCountP) {\n return\n }\n\n const statusAnnouncer = createOrUpdateStatusAnnouncer(\n /** @type {HTMLElement} */ (uploadForm),\n /** @type {HTMLElement | null} */ (fileCountP)\n )\n\n const fileInput = form.querySelector('input[type=\"file\"]')\n\n if (fileInput) {\n fileInput.setAttribute(ARIA_DESCRIBEDBY, 'statusInformation')\n }\n\n const summaryList = findOrCreateSummaryList(\n /** @type {HTMLFormElement} */ (uploadForm),\n /** @type {HTMLElement} */ (fileCountP)\n )\n\n const existingRow = document.querySelector(\n `[data-filename=\"${selectedFile?.name}\"]`\n )\n\n if (existingRow) {\n existingRow.remove()\n }\n\n const row = createFileRow(selectedFile, statusText)\n summaryList.insertBefore(row, summaryList.firstChild)\n statusAnnouncer.textContent = `${selectedFile?.name ?? ''} ${statusText}`\n}\n\n/**\n * Shows an error message using the GOV.UK error summary component\n * and adds inline error styling to the file input\n * @param {string} message - The error message to display\n * @param {HTMLElement | null} errorSummary - The error summary container\n * @param {HTMLInputElement} fileInput - The file input element\n * @returns {void}\n */\nfunction showError(message, errorSummary, fileInput) {\n const topErrorSummary = document.querySelector('.govuk-error-summary')\n\n if (topErrorSummary) {\n const titleElement = document.getElementById(ERROR_SUMMARY_TITLE_ID)\n if (titleElement) {\n fileInput.setAttribute(ARIA_DESCRIBEDBY, ERROR_SUMMARY_TITLE_ID)\n } else {\n fileInput.removeAttribute(ARIA_DESCRIBEDBY)\n }\n return\n }\n\n if (errorSummary) {\n errorSummary.innerHTML = `\n <div class=\"govuk-error-summary\" data-module=\"govuk-error-summary\">\n <div role=\"alert\">\n <h2 class=\"govuk-error-summary__title\" id=\"${ERROR_SUMMARY_TITLE_ID}\">\n There is a problem\n </h2>\n <div class=\"govuk-error-summary__body\">\n <ul class=\"govuk-list govuk-error-summary__list\">\n <li>\n <a href=\"#file-upload\">${message}</a>\n </li>\n </ul>\n </div>\n </div>\n </div>\n `\n\n fileInput.setAttribute(ARIA_DESCRIBEDBY, ERROR_SUMMARY_TITLE_ID)\n }\n\n const formGroup = fileInput.closest('.govuk-form-group')\n if (formGroup) {\n formGroup.classList.add('govuk-form-group--error')\n fileInput.classList.add('govuk-file-upload--error')\n\n const inputId = fileInput.id\n let errorMessage = document.getElementById(`${inputId}-error`)\n\n if (!errorMessage) {\n errorMessage = document.createElement('p')\n errorMessage.id = `${inputId}-error`\n errorMessage.className = 'govuk-error-message'\n errorMessage.innerHTML = `<span class=\"govuk-visually-hidden\">Error:</span> ${message}`\n formGroup.insertBefore(errorMessage, fileInput)\n }\n\n fileInput.setAttribute(\n ARIA_DESCRIBEDBY,\n `error-summary-title ${inputId}-error`\n )\n }\n}\n\n/**\n * Helper to safely convert an Element to HTMLElement\n * @param {Element | null} element - The element to convert\n */\nfunction asHTMLElement(element) {\n return /** @type {HTMLElement} */ (element)\n}\n\nfunction reloadPage() {\n window.history.replaceState(null, '', window.location.href)\n window.location.href = window.location.pathname\n}\n\n/**\n * Build the upload status URL given the current pathname and the upload ID.\n * @param {string} pathname – e.g. window.location.pathname\n * @param {string} uploadId\n * @returns {string} e.g. \"/form/upload-status/abc123\"\n */\nexport function buildUploadStatusUrl(pathname, uploadId) {\n const pathSegments = pathname.split('/').filter((segment) => segment)\n const prefix = pathSegments.length > 0 ? `/${pathSegments[0]}` : ''\n return `${prefix}/upload-status/${uploadId}`\n}\n\n/**\n * Polls the upload status endpoint until the file is ready or timeout occurs\n * @param {string} uploadId - The upload ID to check\n */\nfunction pollUploadStatus(uploadId) {\n let attempts = 0\n const interval = setInterval(() => {\n attempts++\n\n if (attempts >= MAX_POLLING_DURATION) {\n clearInterval(interval)\n reloadPage()\n return\n }\n\n const uploadStatusUrl = buildUploadStatusUrl(\n window.location.pathname,\n uploadId\n )\n\n fetch(uploadStatusUrl, {\n headers: {\n Accept: 'application/json'\n }\n })\n .then((response) => {\n if (!response.ok) {\n throw new Error('Network response was not ok')\n }\n return response.json()\n })\n .then((data) => {\n if (data.uploadStatus === 'ready') {\n clearInterval(interval)\n reloadPage()\n }\n })\n .catch(() => {\n clearInterval(interval)\n reloadPage()\n })\n }, 1000)\n}\n\n/**\n * Handle standard form submission for file upload\n * @param {HTMLFormElement} formElement - The form element\n * @param {HTMLInputElement} fileInput - The file input element\n * @param {HTMLButtonElement} uploadButton - The upload button\n * @param {HTMLButtonElement} continueButton - The continue button\n * @param {File | null} selectedFile - The selected file\n */\nfunction handleStandardFormSubmission(\n formElement,\n fileInput,\n uploadButton,\n continueButton,\n selectedFile\n) {\n renderSummary(selectedFile, 'Uploading…', formElement)\n\n fileInput.focus()\n\n setTimeout(() => {\n fileInput.disabled = true\n uploadButton.disabled = true\n continueButton.disabled = true\n }, 100)\n}\n\n/**\n * Handle AJAX form submission with upload ID\n * @param {Event} event - The click event\n * @param {HTMLFormElement} formElement - The form element\n * @param {HTMLInputElement} fileInput - The file input element\n * @param {HTMLButtonElement} uploadButton - The upload button\n * @param {HTMLElement | null} errorSummary - The error summary container\n * @param {string | undefined} uploadId - The upload ID\n * @returns {boolean} Whether the event was handled\n */\nfunction handleAjaxFormSubmission(\n event,\n formElement,\n fileInput,\n uploadButton,\n errorSummary,\n uploadId\n) {\n if (!formElement.action || !uploadId) {\n return false\n }\n\n event.preventDefault()\n\n const formData = new FormData(formElement)\n const isLocalDev = !!formElement.dataset.proxyUrl\n const uploadUrl = formElement.dataset.proxyUrl ?? formElement.action\n\n const fetchOptions = /** @type {RequestInit} */ ({\n method: 'POST',\n body: formData,\n redirect: isLocalDev ? 'follow' : 'manual' // follow mode if local development with the proxy\n })\n\n // no-cors mode if needed local development with the proxy\n if (isLocalDev) {\n fetchOptions.mode = 'no-cors'\n }\n\n fetch(uploadUrl, fetchOptions)\n .then(() => {\n pollUploadStatus(uploadId)\n })\n .catch(() => {\n fileInput.disabled = false\n uploadButton.disabled = false\n\n showError(\n 'There was a problem uploading the file',\n errorSummary,\n fileInput\n )\n\n return null\n })\n\n return true\n}\n\nexport function initFileUpload() {\n const form = document.querySelector('form:has(input[type=\"file\"])')\n /** @type {HTMLInputElement | null} */\n const fileInput = form ? form.querySelector('input[type=\"file\"]') : null\n /** @type {HTMLButtonElement | null} */\n const uploadButton = form ? form.querySelector('.upload-file-button') : null\n const continueButton =\n /** @type {HTMLButtonElement} */ (\n Array.from(document.querySelectorAll('button.govuk-button')).find(\n (button) => button.textContent?.trim() === 'Continue'\n )\n ) ?? null\n\n const errorSummary = document.querySelector('.govuk-error-summary-container')\n\n if (!form || !fileInput || !uploadButton) {\n return\n }\n\n const formElement = /** @type {HTMLFormElement} */ (form)\n /** @type {File | null} */\n let selectedFile = null\n let isSubmitting = false\n const uploadId = formElement.dataset.uploadId\n\n fileInput.addEventListener('change', () => {\n if (errorSummary) {\n errorSummary.innerHTML = ''\n }\n\n if (fileInput.files && fileInput.files.length > 0) {\n selectedFile = fileInput.files[0]\n }\n })\n\n uploadButton.addEventListener('click', (event) => {\n if (!selectedFile) {\n event.preventDefault()\n showError(\n 'Select a file',\n /** @type {HTMLElement | null} */ (errorSummary),\n fileInput\n )\n return\n }\n\n if (isSubmitting) {\n event.preventDefault()\n return\n }\n\n isSubmitting = true\n\n handleStandardFormSubmission(\n formElement,\n fileInput,\n uploadButton,\n continueButton,\n selectedFile\n )\n\n handleAjaxFormSubmission(\n event,\n formElement,\n fileInput,\n uploadButton,\n /** @type {HTMLElement | null} */ (errorSummary),\n uploadId\n )\n })\n}\n"],"names":["__webpack_require__","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","MAX_POLLING_DURATION","ARIA_DESCRIBEDBY","ERROR_SUMMARY_TITLE_ID","showError","message","errorSummary","fileInput","document","querySelector","getElementById","setAttribute","removeAttribute","innerHTML","formGroup","closest","classList","add","inputId","id","errorMessage","createElement","className","insertBefore","reloadPage","window","history","replaceState","location","href","pathname","buildUploadStatusUrl","uploadId","pathSegments","split","filter","segment","length","initFileUpload","_Array$from$find","form","uploadButton","continueButton","Array","from","querySelectorAll","find","button","_button$textContent","textContent","trim","formElement","selectedFile","isSubmitting","dataset","addEventListener","files","event","preventDefault","statusText","_selectedFile$name3","container","uploadForm","HTMLFormElement","fileCountP","statusAnnouncer","_fileCountP$parentNod","nextSibling","parentNode","appendChild","addStatusAnnouncerToDOM","_unused","_unused2","body","createOrUpdateStatusAnnouncer","summaryList","findOrCreateSummaryList","existingRow","name","remove","row","_selectedFile$name","_selectedFile$name2","createFileRow","firstChild","renderSummary","focus","setTimeout","disabled","handleStandardFormSubmission","_formElement$dataset$","action","formData","FormData","isLocalDev","proxyUrl","uploadUrl","fetchOptions","method","redirect","mode","fetch","then","attempts","interval","setInterval","clearInterval","uploadStatusUrl","headers","Accept","response","ok","Error","json","data","uploadStatus","catch","pollUploadStatus","handleAjaxFormSubmission"],"sourceRoot":""}
@@ -1,4 +1,6 @@
1
1
  export const MAX_POLLING_DURATION = 300; // 5 minutes
2
+ const ARIA_DESCRIBEDBY = 'aria-describedby';
3
+ const ERROR_SUMMARY_TITLE_ID = 'error-summary-title';
2
4
 
3
5
  /**
4
6
  * Creates or updates status announcer for screen readers
@@ -107,7 +109,7 @@ function renderSummary(selectedFile, statusText, form) {
107
109
  const statusAnnouncer = createOrUpdateStatusAnnouncer(/** @type {HTMLElement} */uploadForm, /** @type {HTMLElement | null} */fileCountP);
108
110
  const fileInput = form.querySelector('input[type="file"]');
109
111
  if (fileInput) {
110
- fileInput.setAttribute('aria-describedby', 'statusInformation');
112
+ fileInput.setAttribute(ARIA_DESCRIBEDBY, 'statusInformation');
111
113
  }
112
114
  const summaryList = findOrCreateSummaryList(/** @type {HTMLFormElement} */uploadForm, /** @type {HTMLElement} */fileCountP);
113
115
  const existingRow = document.querySelector(`[data-filename="${selectedFile?.name}"]`);
@@ -121,17 +123,28 @@ function renderSummary(selectedFile, statusText, form) {
121
123
 
122
124
  /**
123
125
  * Shows an error message using the GOV.UK error summary component
126
+ * and adds inline error styling to the file input
124
127
  * @param {string} message - The error message to display
125
128
  * @param {HTMLElement | null} errorSummary - The error summary container
126
129
  * @param {HTMLInputElement} fileInput - The file input element
127
130
  * @returns {void}
128
131
  */
129
132
  function showError(message, errorSummary, fileInput) {
133
+ const topErrorSummary = document.querySelector('.govuk-error-summary');
134
+ if (topErrorSummary) {
135
+ const titleElement = document.getElementById(ERROR_SUMMARY_TITLE_ID);
136
+ if (titleElement) {
137
+ fileInput.setAttribute(ARIA_DESCRIBEDBY, ERROR_SUMMARY_TITLE_ID);
138
+ } else {
139
+ fileInput.removeAttribute(ARIA_DESCRIBEDBY);
140
+ }
141
+ return;
142
+ }
130
143
  if (errorSummary) {
131
144
  errorSummary.innerHTML = `
132
145
  <div class="govuk-error-summary" data-module="govuk-error-summary">
133
146
  <div role="alert">
134
- <h2 class="govuk-error-summary__title" id="error-summary-title">
147
+ <h2 class="govuk-error-summary__title" id="${ERROR_SUMMARY_TITLE_ID}">
135
148
  There is a problem
136
149
  </h2>
137
150
  <div class="govuk-error-summary__body">
@@ -144,7 +157,22 @@ function showError(message, errorSummary, fileInput) {
144
157
  </div>
145
158
  </div>
146
159
  `;
147
- fileInput.setAttribute('aria-describedby', 'error-summary-title');
160
+ fileInput.setAttribute(ARIA_DESCRIBEDBY, ERROR_SUMMARY_TITLE_ID);
161
+ }
162
+ const formGroup = fileInput.closest('.govuk-form-group');
163
+ if (formGroup) {
164
+ formGroup.classList.add('govuk-form-group--error');
165
+ fileInput.classList.add('govuk-file-upload--error');
166
+ const inputId = fileInput.id;
167
+ let errorMessage = document.getElementById(`${inputId}-error`);
168
+ if (!errorMessage) {
169
+ errorMessage = document.createElement('p');
170
+ errorMessage.id = `${inputId}-error`;
171
+ errorMessage.className = 'govuk-error-message';
172
+ errorMessage.innerHTML = `<span class="govuk-visually-hidden">Error:</span> ${message}`;
173
+ formGroup.insertBefore(errorMessage, fileInput);
174
+ }
175
+ fileInput.setAttribute(ARIA_DESCRIBEDBY, `error-summary-title ${inputId}-error`);
148
176
  }
149
177
  }
150
178
 
@@ -160,6 +188,18 @@ function reloadPage() {
160
188
  window.location.href = window.location.pathname;
161
189
  }
162
190
 
191
+ /**
192
+ * Build the upload status URL given the current pathname and the upload ID.
193
+ * @param {string} pathname – e.g. window.location.pathname
194
+ * @param {string} uploadId
195
+ * @returns {string} e.g. "/form/upload-status/abc123"
196
+ */
197
+ export function buildUploadStatusUrl(pathname, uploadId) {
198
+ const pathSegments = pathname.split('/').filter(segment => segment);
199
+ const prefix = pathSegments.length > 0 ? `/${pathSegments[0]}` : '';
200
+ return `${prefix}/upload-status/${uploadId}`;
201
+ }
202
+
163
203
  /**
164
204
  * Polls the upload status endpoint until the file is ready or timeout occurs
165
205
  * @param {string} uploadId - The upload ID to check
@@ -173,7 +213,8 @@ function pollUploadStatus(uploadId) {
173
213
  reloadPage();
174
214
  return;
175
215
  }
176
- fetch(`/upload-status/${uploadId}`, {
216
+ const uploadStatusUrl = buildUploadStatusUrl(window.location.pathname, uploadId);
217
+ fetch(uploadStatusUrl, {
177
218
  headers: {
178
219
  Accept: 'application/json'
179
220
  }
@@ -1 +1 @@
1
- {"version":3,"file":"file-upload.js","names":["MAX_POLLING_DURATION","createOrUpdateStatusAnnouncer","form","fileCountP","statusAnnouncer","querySelector","document","createElement","id","className","setAttribute","addStatusAnnouncerToDOM","asHTMLElement","appendChild","body","nextSibling","parentNode","insertBefore","parentElement","findOrCreateSummaryList","summaryList","continueButton","createFileRow","selectedFile","statusText","row","name","innerHTML","renderSummary","container","getElementById","uploadForm","closest","HTMLFormElement","fileInput","existingRow","remove","firstChild","textContent","showError","message","errorSummary","element","reloadPage","window","history","replaceState","location","href","pathname","pollUploadStatus","uploadId","attempts","interval","setInterval","clearInterval","fetch","headers","Accept","then","response","ok","Error","json","data","uploadStatus","catch","handleStandardFormSubmission","formElement","uploadButton","focus","setTimeout","disabled","handleAjaxFormSubmission","event","action","preventDefault","formData","FormData","isLocalDev","dataset","proxyUrl","uploadUrl","fetchOptions","method","redirect","mode","initFileUpload","Array","from","querySelectorAll","find","button","trim","isSubmitting","addEventListener","files","length"],"sources":["../../../src/client/javascripts/file-upload.js"],"sourcesContent":["export const MAX_POLLING_DURATION = 300 // 5 minutes\n\n/**\n * Creates or updates status announcer for screen readers\n * @param {HTMLElement | null} form - The form element\n * @param {HTMLElement | null} fileCountP - The file count paragraph element\n * @returns {HTMLElement} The status announcer element\n */\nfunction createOrUpdateStatusAnnouncer(form, fileCountP) {\n let statusAnnouncer = form?.querySelector('#statusInformation')\n\n if (!statusAnnouncer) {\n statusAnnouncer = document.createElement('div')\n statusAnnouncer.id = 'statusInformation'\n statusAnnouncer.className = 'govuk-visually-hidden'\n statusAnnouncer.setAttribute('aria-live', 'polite')\n\n // multiple fallbacks to ensure the status announcer is always added to the DOM\n // this helps with cross-browser compatibility and unexpected DOM structures encountered during QA\n try {\n addStatusAnnouncerToDOM(\n asHTMLElement(form),\n asHTMLElement(fileCountP),\n asHTMLElement(statusAnnouncer)\n )\n } catch {\n try {\n form?.appendChild(statusAnnouncer)\n } catch {\n document.body.appendChild(statusAnnouncer)\n }\n }\n }\n\n return /** @type {HTMLElement} */ (statusAnnouncer)\n}\n\n/**\n * Helper function to add the status announcer to the DOM\n * @param {HTMLElement} form - The form element\n * @param {HTMLElement | null} fileCountP - The file count paragraph element\n * @param {HTMLElement} statusAnnouncer - The status announcer element to add\n */\nfunction addStatusAnnouncerToDOM(form, fileCountP, statusAnnouncer) {\n if (fileCountP?.nextSibling && fileCountP.parentNode === form) {\n form.insertBefore(statusAnnouncer, fileCountP.nextSibling)\n return\n }\n\n const parentElement = fileCountP?.parentNode ?? form\n parentElement.appendChild(statusAnnouncer)\n}\n\n/**\n * Finds an existing summary list or creates a new one\n * @param {HTMLFormElement} form - The form element\n * @param {HTMLElement} fileCountP - The file count paragraph element\n * @returns {HTMLElement} The summary list element\n */\nfunction findOrCreateSummaryList(form, fileCountP) {\n let summaryList = form.querySelector('dl.govuk-summary-list')\n\n if (!summaryList) {\n summaryList = document.createElement('dl')\n summaryList.className = 'govuk-summary-list govuk-summary-list--long-key'\n\n const continueButton = form.querySelector('.govuk-button')\n\n if (continueButton) {\n form.insertBefore(summaryList, continueButton)\n } else {\n form.insertBefore(summaryList, fileCountP.nextSibling)\n }\n }\n\n return /** @type {HTMLElement} */ (summaryList)\n}\n\n/**\n * Creates a file row element for the summary list\n * @param {File | null} selectedFile - The selected file\n * @param {string} statusText - The status to display\n * @returns {HTMLElement} The created row element\n */\nfunction createFileRow(selectedFile, statusText) {\n const row = document.createElement('div')\n row.className = 'govuk-summary-list__row'\n row.setAttribute('data-filename', selectedFile?.name ?? '')\n row.innerHTML = `\n <dt class=\"govuk-summary-list__key\">\n ${selectedFile?.name ?? ''}\n </dt>\n <dd class=\"govuk-summary-list__value\">\n <strong class=\"govuk-tag govuk-tag--yellow\">${statusText}</strong>\n </dd>\n <dd class=\"govuk-summary-list__actions\">\n </dd>\n `\n return row\n}\n\n/**\n * Renders or updates the file summary box for the selected file\n * @param {File | null} selectedFile - The selected file\n * @param {string} statusText - The status to display\n * @param {HTMLElement} form - The form element\n */\nfunction renderSummary(selectedFile, statusText, form) {\n const container = document.getElementById('uploadedFilesContainer')\n const uploadForm = container ? container.closest('form') : null\n\n if (!uploadForm || !(uploadForm instanceof HTMLFormElement)) {\n return\n }\n\n const fileCountP = uploadForm.querySelector('p.govuk-body')\n\n if (!fileCountP) {\n return\n }\n\n const statusAnnouncer = createOrUpdateStatusAnnouncer(\n /** @type {HTMLElement} */ (uploadForm),\n /** @type {HTMLElement | null} */ (fileCountP)\n )\n\n const fileInput = form.querySelector('input[type=\"file\"]')\n\n if (fileInput) {\n fileInput.setAttribute('aria-describedby', 'statusInformation')\n }\n\n const summaryList = findOrCreateSummaryList(\n /** @type {HTMLFormElement} */ (uploadForm),\n /** @type {HTMLElement} */ (fileCountP)\n )\n\n const existingRow = document.querySelector(\n `[data-filename=\"${selectedFile?.name}\"]`\n )\n\n if (existingRow) {\n existingRow.remove()\n }\n\n const row = createFileRow(selectedFile, statusText)\n summaryList.insertBefore(row, summaryList.firstChild)\n statusAnnouncer.textContent = `${selectedFile?.name ?? ''} ${statusText}`\n}\n\n/**\n * Shows an error message using the GOV.UK error summary component\n * @param {string} message - The error message to display\n * @param {HTMLElement | null} errorSummary - The error summary container\n * @param {HTMLInputElement} fileInput - The file input element\n * @returns {void}\n */\nfunction showError(message, errorSummary, fileInput) {\n if (errorSummary) {\n errorSummary.innerHTML = `\n <div class=\"govuk-error-summary\" data-module=\"govuk-error-summary\">\n <div role=\"alert\">\n <h2 class=\"govuk-error-summary__title\" id=\"error-summary-title\">\n There is a problem\n </h2>\n <div class=\"govuk-error-summary__body\">\n <ul class=\"govuk-list govuk-error-summary__list\">\n <li>\n <a href=\"#file-upload\">${message}</a>\n </li>\n </ul>\n </div>\n </div>\n </div>\n `\n fileInput.setAttribute('aria-describedby', 'error-summary-title')\n }\n}\n\n/**\n * Helper to safely convert an Element to HTMLElement\n * @param {Element | null} element - The element to convert\n */\nfunction asHTMLElement(element) {\n return /** @type {HTMLElement} */ (element)\n}\n\nfunction reloadPage() {\n window.history.replaceState(null, '', window.location.href)\n window.location.href = window.location.pathname\n}\n\n/**\n * Polls the upload status endpoint until the file is ready or timeout occurs\n * @param {string} uploadId - The upload ID to check\n */\nfunction pollUploadStatus(uploadId) {\n let attempts = 0\n const interval = setInterval(() => {\n attempts++\n\n if (attempts >= MAX_POLLING_DURATION) {\n clearInterval(interval)\n reloadPage()\n return\n }\n\n fetch(`/upload-status/${uploadId}`, {\n headers: {\n Accept: 'application/json'\n }\n })\n .then((response) => {\n if (!response.ok) {\n throw new Error('Network response was not ok')\n }\n return response.json()\n })\n .then((data) => {\n if (data.uploadStatus === 'ready') {\n clearInterval(interval)\n reloadPage()\n }\n })\n .catch(() => {\n clearInterval(interval)\n reloadPage()\n })\n }, 1000)\n}\n\n/**\n * Handle standard form submission for file upload\n * @param {HTMLFormElement} formElement - The form element\n * @param {HTMLInputElement} fileInput - The file input element\n * @param {HTMLButtonElement} uploadButton - The upload button\n * @param {HTMLButtonElement} continueButton - The continue button\n * @param {File | null} selectedFile - The selected file\n */\nfunction handleStandardFormSubmission(\n formElement,\n fileInput,\n uploadButton,\n continueButton,\n selectedFile\n) {\n renderSummary(selectedFile, 'Uploading…', formElement)\n\n fileInput.focus()\n\n setTimeout(() => {\n fileInput.disabled = true\n uploadButton.disabled = true\n continueButton.disabled = true\n }, 100)\n}\n\n/**\n * Handle AJAX form submission with upload ID\n * @param {Event} event - The click event\n * @param {HTMLFormElement} formElement - The form element\n * @param {HTMLInputElement} fileInput - The file input element\n * @param {HTMLButtonElement} uploadButton - The upload button\n * @param {HTMLElement | null} errorSummary - The error summary container\n * @param {string | undefined} uploadId - The upload ID\n * @returns {boolean} Whether the event was handled\n */\nfunction handleAjaxFormSubmission(\n event,\n formElement,\n fileInput,\n uploadButton,\n errorSummary,\n uploadId\n) {\n if (!formElement.action || !uploadId) {\n return false\n }\n\n event.preventDefault()\n\n const formData = new FormData(formElement)\n const isLocalDev = !!formElement.dataset.proxyUrl\n const uploadUrl = formElement.dataset.proxyUrl ?? formElement.action\n\n const fetchOptions = /** @type {RequestInit} */ ({\n method: 'POST',\n body: formData,\n redirect: isLocalDev ? 'follow' : 'manual' // follow mode if local development with the proxy\n })\n\n // no-cors mode if needed local development with the proxy\n if (isLocalDev) {\n fetchOptions.mode = 'no-cors'\n }\n\n fetch(uploadUrl, fetchOptions)\n .then(() => {\n pollUploadStatus(uploadId)\n })\n .catch(() => {\n fileInput.disabled = false\n uploadButton.disabled = false\n\n showError(\n 'There was a problem uploading the file',\n errorSummary,\n fileInput\n )\n\n return null\n })\n\n return true\n}\n\nexport function initFileUpload() {\n const form = document.querySelector('form:has(input[type=\"file\"])')\n /** @type {HTMLInputElement | null} */\n const fileInput = form ? form.querySelector('input[type=\"file\"]') : null\n /** @type {HTMLButtonElement | null} */\n const uploadButton = form ? form.querySelector('.upload-file-button') : null\n const continueButton =\n /** @type {HTMLButtonElement} */ (\n Array.from(document.querySelectorAll('button.govuk-button')).find(\n (button) => button.textContent?.trim() === 'Continue'\n )\n ) ?? null\n\n const errorSummary = document.querySelector('.govuk-error-summary-container')\n\n if (!form || !fileInput || !uploadButton) {\n return\n }\n\n const formElement = /** @type {HTMLFormElement} */ (form)\n /** @type {File | null} */\n let selectedFile = null\n let isSubmitting = false\n const uploadId = formElement.dataset.uploadId\n\n fileInput.addEventListener('change', () => {\n if (errorSummary) {\n errorSummary.innerHTML = ''\n }\n if (fileInput.files && fileInput.files.length > 0) {\n selectedFile = fileInput.files[0]\n }\n })\n\n uploadButton.addEventListener('click', (event) => {\n if (!selectedFile) {\n event.preventDefault()\n showError(\n 'Select a file',\n /** @type {HTMLElement | null} */ (errorSummary),\n fileInput\n )\n return\n }\n\n if (isSubmitting) {\n event.preventDefault()\n return\n }\n\n isSubmitting = true\n\n handleStandardFormSubmission(\n formElement,\n fileInput,\n uploadButton,\n continueButton,\n selectedFile\n )\n\n handleAjaxFormSubmission(\n event,\n formElement,\n fileInput,\n uploadButton,\n /** @type {HTMLElement | null} */ (errorSummary),\n uploadId\n )\n })\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"file-upload.js","names":["MAX_POLLING_DURATION","ARIA_DESCRIBEDBY","ERROR_SUMMARY_TITLE_ID","createOrUpdateStatusAnnouncer","form","fileCountP","statusAnnouncer","querySelector","document","createElement","id","className","setAttribute","addStatusAnnouncerToDOM","asHTMLElement","appendChild","body","nextSibling","parentNode","insertBefore","parentElement","findOrCreateSummaryList","summaryList","continueButton","createFileRow","selectedFile","statusText","row","name","innerHTML","renderSummary","container","getElementById","uploadForm","closest","HTMLFormElement","fileInput","existingRow","remove","firstChild","textContent","showError","message","errorSummary","topErrorSummary","titleElement","removeAttribute","formGroup","classList","add","inputId","errorMessage","element","reloadPage","window","history","replaceState","location","href","pathname","buildUploadStatusUrl","uploadId","pathSegments","split","filter","segment","prefix","length","pollUploadStatus","attempts","interval","setInterval","clearInterval","uploadStatusUrl","fetch","headers","Accept","then","response","ok","Error","json","data","uploadStatus","catch","handleStandardFormSubmission","formElement","uploadButton","focus","setTimeout","disabled","handleAjaxFormSubmission","event","action","preventDefault","formData","FormData","isLocalDev","dataset","proxyUrl","uploadUrl","fetchOptions","method","redirect","mode","initFileUpload","Array","from","querySelectorAll","find","button","trim","isSubmitting","addEventListener","files"],"sources":["../../../src/client/javascripts/file-upload.js"],"sourcesContent":["export const MAX_POLLING_DURATION = 300 // 5 minutes\nconst ARIA_DESCRIBEDBY = 'aria-describedby'\nconst ERROR_SUMMARY_TITLE_ID = 'error-summary-title'\n\n/**\n * Creates or updates status announcer for screen readers\n * @param {HTMLElement | null} form - The form element\n * @param {HTMLElement | null} fileCountP - The file count paragraph element\n * @returns {HTMLElement} The status announcer element\n */\nfunction createOrUpdateStatusAnnouncer(form, fileCountP) {\n let statusAnnouncer = form?.querySelector('#statusInformation')\n\n if (!statusAnnouncer) {\n statusAnnouncer = document.createElement('div')\n statusAnnouncer.id = 'statusInformation'\n statusAnnouncer.className = 'govuk-visually-hidden'\n statusAnnouncer.setAttribute('aria-live', 'polite')\n\n // multiple fallbacks to ensure the status announcer is always added to the DOM\n // this helps with cross-browser compatibility and unexpected DOM structures encountered during QA\n try {\n addStatusAnnouncerToDOM(\n asHTMLElement(form),\n asHTMLElement(fileCountP),\n asHTMLElement(statusAnnouncer)\n )\n } catch {\n try {\n form?.appendChild(statusAnnouncer)\n } catch {\n document.body.appendChild(statusAnnouncer)\n }\n }\n }\n\n return /** @type {HTMLElement} */ (statusAnnouncer)\n}\n\n/**\n * Helper function to add the status announcer to the DOM\n * @param {HTMLElement} form - The form element\n * @param {HTMLElement | null} fileCountP - The file count paragraph element\n * @param {HTMLElement} statusAnnouncer - The status announcer element to add\n */\nfunction addStatusAnnouncerToDOM(form, fileCountP, statusAnnouncer) {\n if (fileCountP?.nextSibling && fileCountP.parentNode === form) {\n form.insertBefore(statusAnnouncer, fileCountP.nextSibling)\n return\n }\n\n const parentElement = fileCountP?.parentNode ?? form\n parentElement.appendChild(statusAnnouncer)\n}\n\n/**\n * Finds an existing summary list or creates a new one\n * @param {HTMLFormElement} form - The form element\n * @param {HTMLElement} fileCountP - The file count paragraph element\n * @returns {HTMLElement} The summary list element\n */\nfunction findOrCreateSummaryList(form, fileCountP) {\n let summaryList = form.querySelector('dl.govuk-summary-list')\n\n if (!summaryList) {\n summaryList = document.createElement('dl')\n summaryList.className = 'govuk-summary-list govuk-summary-list--long-key'\n\n const continueButton = form.querySelector('.govuk-button')\n\n if (continueButton) {\n form.insertBefore(summaryList, continueButton)\n } else {\n form.insertBefore(summaryList, fileCountP.nextSibling)\n }\n }\n\n return /** @type {HTMLElement} */ (summaryList)\n}\n\n/**\n * Creates a file row element for the summary list\n * @param {File | null} selectedFile - The selected file\n * @param {string} statusText - The status to display\n * @returns {HTMLElement} The created row element\n */\nfunction createFileRow(selectedFile, statusText) {\n const row = document.createElement('div')\n row.className = 'govuk-summary-list__row'\n row.setAttribute('data-filename', selectedFile?.name ?? '')\n row.innerHTML = `\n <dt class=\"govuk-summary-list__key\">\n ${selectedFile?.name ?? ''}\n </dt>\n <dd class=\"govuk-summary-list__value\">\n <strong class=\"govuk-tag govuk-tag--yellow\">${statusText}</strong>\n </dd>\n <dd class=\"govuk-summary-list__actions\">\n </dd>\n `\n return row\n}\n\n/**\n * Renders or updates the file summary box for the selected file\n * @param {File | null} selectedFile - The selected file\n * @param {string} statusText - The status to display\n * @param {HTMLElement} form - The form element\n */\nfunction renderSummary(selectedFile, statusText, form) {\n const container = document.getElementById('uploadedFilesContainer')\n const uploadForm = container ? container.closest('form') : null\n\n if (!uploadForm || !(uploadForm instanceof HTMLFormElement)) {\n return\n }\n\n const fileCountP = uploadForm.querySelector('p.govuk-body')\n\n if (!fileCountP) {\n return\n }\n\n const statusAnnouncer = createOrUpdateStatusAnnouncer(\n /** @type {HTMLElement} */ (uploadForm),\n /** @type {HTMLElement | null} */ (fileCountP)\n )\n\n const fileInput = form.querySelector('input[type=\"file\"]')\n\n if (fileInput) {\n fileInput.setAttribute(ARIA_DESCRIBEDBY, 'statusInformation')\n }\n\n const summaryList = findOrCreateSummaryList(\n /** @type {HTMLFormElement} */ (uploadForm),\n /** @type {HTMLElement} */ (fileCountP)\n )\n\n const existingRow = document.querySelector(\n `[data-filename=\"${selectedFile?.name}\"]`\n )\n\n if (existingRow) {\n existingRow.remove()\n }\n\n const row = createFileRow(selectedFile, statusText)\n summaryList.insertBefore(row, summaryList.firstChild)\n statusAnnouncer.textContent = `${selectedFile?.name ?? ''} ${statusText}`\n}\n\n/**\n * Shows an error message using the GOV.UK error summary component\n * and adds inline error styling to the file input\n * @param {string} message - The error message to display\n * @param {HTMLElement | null} errorSummary - The error summary container\n * @param {HTMLInputElement} fileInput - The file input element\n * @returns {void}\n */\nfunction showError(message, errorSummary, fileInput) {\n const topErrorSummary = document.querySelector('.govuk-error-summary')\n\n if (topErrorSummary) {\n const titleElement = document.getElementById(ERROR_SUMMARY_TITLE_ID)\n if (titleElement) {\n fileInput.setAttribute(ARIA_DESCRIBEDBY, ERROR_SUMMARY_TITLE_ID)\n } else {\n fileInput.removeAttribute(ARIA_DESCRIBEDBY)\n }\n return\n }\n\n if (errorSummary) {\n errorSummary.innerHTML = `\n <div class=\"govuk-error-summary\" data-module=\"govuk-error-summary\">\n <div role=\"alert\">\n <h2 class=\"govuk-error-summary__title\" id=\"${ERROR_SUMMARY_TITLE_ID}\">\n There is a problem\n </h2>\n <div class=\"govuk-error-summary__body\">\n <ul class=\"govuk-list govuk-error-summary__list\">\n <li>\n <a href=\"#file-upload\">${message}</a>\n </li>\n </ul>\n </div>\n </div>\n </div>\n `\n\n fileInput.setAttribute(ARIA_DESCRIBEDBY, ERROR_SUMMARY_TITLE_ID)\n }\n\n const formGroup = fileInput.closest('.govuk-form-group')\n if (formGroup) {\n formGroup.classList.add('govuk-form-group--error')\n fileInput.classList.add('govuk-file-upload--error')\n\n const inputId = fileInput.id\n let errorMessage = document.getElementById(`${inputId}-error`)\n\n if (!errorMessage) {\n errorMessage = document.createElement('p')\n errorMessage.id = `${inputId}-error`\n errorMessage.className = 'govuk-error-message'\n errorMessage.innerHTML = `<span class=\"govuk-visually-hidden\">Error:</span> ${message}`\n formGroup.insertBefore(errorMessage, fileInput)\n }\n\n fileInput.setAttribute(\n ARIA_DESCRIBEDBY,\n `error-summary-title ${inputId}-error`\n )\n }\n}\n\n/**\n * Helper to safely convert an Element to HTMLElement\n * @param {Element | null} element - The element to convert\n */\nfunction asHTMLElement(element) {\n return /** @type {HTMLElement} */ (element)\n}\n\nfunction reloadPage() {\n window.history.replaceState(null, '', window.location.href)\n window.location.href = window.location.pathname\n}\n\n/**\n * Build the upload status URL given the current pathname and the upload ID.\n * @param {string} pathname – e.g. window.location.pathname\n * @param {string} uploadId\n * @returns {string} e.g. \"/form/upload-status/abc123\"\n */\nexport function buildUploadStatusUrl(pathname, uploadId) {\n const pathSegments = pathname.split('/').filter((segment) => segment)\n const prefix = pathSegments.length > 0 ? `/${pathSegments[0]}` : ''\n return `${prefix}/upload-status/${uploadId}`\n}\n\n/**\n * Polls the upload status endpoint until the file is ready or timeout occurs\n * @param {string} uploadId - The upload ID to check\n */\nfunction pollUploadStatus(uploadId) {\n let attempts = 0\n const interval = setInterval(() => {\n attempts++\n\n if (attempts >= MAX_POLLING_DURATION) {\n clearInterval(interval)\n reloadPage()\n return\n }\n\n const uploadStatusUrl = buildUploadStatusUrl(\n window.location.pathname,\n uploadId\n )\n\n fetch(uploadStatusUrl, {\n headers: {\n Accept: 'application/json'\n }\n })\n .then((response) => {\n if (!response.ok) {\n throw new Error('Network response was not ok')\n }\n return response.json()\n })\n .then((data) => {\n if (data.uploadStatus === 'ready') {\n clearInterval(interval)\n reloadPage()\n }\n })\n .catch(() => {\n clearInterval(interval)\n reloadPage()\n })\n }, 1000)\n}\n\n/**\n * Handle standard form submission for file upload\n * @param {HTMLFormElement} formElement - The form element\n * @param {HTMLInputElement} fileInput - The file input element\n * @param {HTMLButtonElement} uploadButton - The upload button\n * @param {HTMLButtonElement} continueButton - The continue button\n * @param {File | null} selectedFile - The selected file\n */\nfunction handleStandardFormSubmission(\n formElement,\n fileInput,\n uploadButton,\n continueButton,\n selectedFile\n) {\n renderSummary(selectedFile, 'Uploading…', formElement)\n\n fileInput.focus()\n\n setTimeout(() => {\n fileInput.disabled = true\n uploadButton.disabled = true\n continueButton.disabled = true\n }, 100)\n}\n\n/**\n * Handle AJAX form submission with upload ID\n * @param {Event} event - The click event\n * @param {HTMLFormElement} formElement - The form element\n * @param {HTMLInputElement} fileInput - The file input element\n * @param {HTMLButtonElement} uploadButton - The upload button\n * @param {HTMLElement | null} errorSummary - The error summary container\n * @param {string | undefined} uploadId - The upload ID\n * @returns {boolean} Whether the event was handled\n */\nfunction handleAjaxFormSubmission(\n event,\n formElement,\n fileInput,\n uploadButton,\n errorSummary,\n uploadId\n) {\n if (!formElement.action || !uploadId) {\n return false\n }\n\n event.preventDefault()\n\n const formData = new FormData(formElement)\n const isLocalDev = !!formElement.dataset.proxyUrl\n const uploadUrl = formElement.dataset.proxyUrl ?? formElement.action\n\n const fetchOptions = /** @type {RequestInit} */ ({\n method: 'POST',\n body: formData,\n redirect: isLocalDev ? 'follow' : 'manual' // follow mode if local development with the proxy\n })\n\n // no-cors mode if needed local development with the proxy\n if (isLocalDev) {\n fetchOptions.mode = 'no-cors'\n }\n\n fetch(uploadUrl, fetchOptions)\n .then(() => {\n pollUploadStatus(uploadId)\n })\n .catch(() => {\n fileInput.disabled = false\n uploadButton.disabled = false\n\n showError(\n 'There was a problem uploading the file',\n errorSummary,\n fileInput\n )\n\n return null\n })\n\n return true\n}\n\nexport function initFileUpload() {\n const form = document.querySelector('form:has(input[type=\"file\"])')\n /** @type {HTMLInputElement | null} */\n const fileInput = form ? form.querySelector('input[type=\"file\"]') : null\n /** @type {HTMLButtonElement | null} */\n const uploadButton = form ? form.querySelector('.upload-file-button') : null\n const continueButton =\n /** @type {HTMLButtonElement} */ (\n Array.from(document.querySelectorAll('button.govuk-button')).find(\n (button) => button.textContent?.trim() === 'Continue'\n )\n ) ?? null\n\n const errorSummary = document.querySelector('.govuk-error-summary-container')\n\n if (!form || !fileInput || !uploadButton) {\n return\n }\n\n const formElement = /** @type {HTMLFormElement} */ (form)\n /** @type {File | null} */\n let selectedFile = null\n let isSubmitting = false\n const uploadId = formElement.dataset.uploadId\n\n fileInput.addEventListener('change', () => {\n if (errorSummary) {\n errorSummary.innerHTML = ''\n }\n\n if (fileInput.files && fileInput.files.length > 0) {\n selectedFile = fileInput.files[0]\n }\n })\n\n uploadButton.addEventListener('click', (event) => {\n if (!selectedFile) {\n event.preventDefault()\n showError(\n 'Select a file',\n /** @type {HTMLElement | null} */ (errorSummary),\n fileInput\n )\n return\n }\n\n if (isSubmitting) {\n event.preventDefault()\n return\n }\n\n isSubmitting = true\n\n handleStandardFormSubmission(\n formElement,\n fileInput,\n uploadButton,\n continueButton,\n selectedFile\n )\n\n handleAjaxFormSubmission(\n event,\n formElement,\n fileInput,\n uploadButton,\n /** @type {HTMLElement | null} */ (errorSummary),\n uploadId\n )\n })\n}\n"],"mappings":"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","ignoreList":[]}
@@ -1,2 +1,4 @@
1
1
  export const PREVIEW_PATH_PREFIX = '/preview';
2
+ export const ERROR_PREVIEW_PATH_PREFIX = '/error-preview';
3
+ export const FORM_PREFIX = '';
2
4
  //# sourceMappingURL=constants.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.js","names":["PREVIEW_PATH_PREFIX"],"sources":["../../src/server/constants.js"],"sourcesContent":["export const PREVIEW_PATH_PREFIX = '/preview'\n"],"mappings":"AAAA,OAAO,MAAMA,mBAAmB,GAAG,UAAU","ignoreList":[]}
1
+ {"version":3,"file":"constants.js","names":["PREVIEW_PATH_PREFIX","ERROR_PREVIEW_PATH_PREFIX","FORM_PREFIX"],"sources":["../../src/server/constants.js"],"sourcesContent":["export const PREVIEW_PATH_PREFIX = '/preview'\nexport const ERROR_PREVIEW_PATH_PREFIX = '/error-preview'\nexport const FORM_PREFIX = ''\n"],"mappings":"AAAA,OAAO,MAAMA,mBAAmB,GAAG,UAAU;AAC7C,OAAO,MAAMC,yBAAyB,GAAG,gBAAgB;AACzD,OAAO,MAAMC,WAAW,GAAG,EAAE","ignoreList":[]}
@@ -72,6 +72,7 @@ export async function createServer(routeConfig) {
72
72
  await server.register(inert);
73
73
  await server.register(Scooter);
74
74
  await server.register(pluginCrumb);
75
+ await server.register(pluginEngine);
75
76
  server.ext('onPreResponse', (request, h) => {
76
77
  const {
77
78
  response
@@ -90,7 +91,6 @@ export async function createServer(routeConfig) {
90
91
  return h.continue;
91
92
  });
92
93
  await server.register(pluginViews);
93
- await server.register(pluginEngine);
94
94
  await server.register({
95
95
  plugin: {
96
96
  name: 'router',
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["Engine","CatboxMemory","CatboxRedis","hapi","inert","Scooter","Wreck","blipp","ProxyAgent","config","requestLogger","requestTracing","buildRedisClient","configureCrumbPlugin","configureEnginePlugin","pluginErrorPages","plugin","pluginViews","pluginPulse","pluginSession","publicRoutes","prepareSecureContext","proxyAgent","agents","https","http","httpsAllowUnauthorized","serverOptions","debug","request","get","port","router","stripTrailingSlash","routes","validate","options","abortEarly","security","hsts","maxAge","includeSubDomains","preload","xss","noSniff","xframe","cache","name","engine","client","createServer","routeConfig","server","register","pluginCrumb","pluginEngine","ext","h","response","continue","header","path","startsWith","route"],"sources":["../../src/server/index.ts"],"sourcesContent":["import { Engine as CatboxMemory } from '@hapi/catbox-memory'\nimport { Engine as CatboxRedis } from '@hapi/catbox-redis'\nimport hapi, {\n type Request,\n type ResponseToolkit,\n type ServerOptions\n} from '@hapi/hapi'\nimport inert from '@hapi/inert'\nimport Scooter from '@hapi/scooter'\nimport Wreck from '@hapi/wreck'\nimport blipp from 'blipp'\nimport { ProxyAgent } from 'proxy-agent'\n\nimport { config } from '~/src/config/index.js'\nimport { requestLogger } from '~/src/server/common/helpers/logging/request-logger.js'\nimport { requestTracing } from '~/src/server/common/helpers/logging/request-tracing.js'\nimport { buildRedisClient } from '~/src/server/common/helpers/redis-client.js'\nimport { configureCrumbPlugin } from '~/src/server/plugins/crumb.js'\nimport { configureEnginePlugin } from '~/src/server/plugins/engine/configureEnginePlugin.js'\nimport pluginErrorPages from '~/src/server/plugins/errorPages.js'\nimport { plugin as pluginViews } from '~/src/server/plugins/nunjucks/index.js'\nimport pluginPulse from '~/src/server/plugins/pulse.js'\nimport pluginSession from '~/src/server/plugins/session.js'\nimport { publicRoutes } from '~/src/server/routes/index.js'\nimport { prepareSecureContext } from '~/src/server/secure-context.js'\nimport { type RouteConfig } from '~/src/server/types.js'\n\nconst proxyAgent = new ProxyAgent()\n\nWreck.agents = {\n https: proxyAgent,\n http: proxyAgent,\n httpsAllowUnauthorized: proxyAgent\n}\n\nconst serverOptions = (): ServerOptions => {\n const serverOptions: ServerOptions = {\n debug: { request: [`${config.get('isDevelopment')}`] },\n port: config.get('port'),\n router: {\n stripTrailingSlash: true\n },\n routes: {\n validate: {\n options: {\n abortEarly: false\n }\n },\n security: {\n hsts: {\n maxAge: 31536000,\n includeSubDomains: true,\n preload: false\n },\n xss: 'enabled',\n noSniff: true,\n xframe: true\n }\n },\n cache: [\n {\n name: 'session',\n engine: config.get('isTest')\n ? new CatboxMemory()\n : new CatboxRedis({\n client: buildRedisClient()\n })\n }\n ]\n }\n\n return serverOptions\n}\n\nexport async function createServer(routeConfig?: RouteConfig) {\n const server = hapi.server(serverOptions())\n\n await server.register(requestLogger)\n\n if (config.get('isProduction')) {\n prepareSecureContext(server)\n }\n\n const pluginCrumb = configureCrumbPlugin(routeConfig)\n const pluginEngine = await configureEnginePlugin(routeConfig)\n\n await server.register(pluginSession)\n await server.register(pluginPulse)\n await server.register(inert)\n await server.register(Scooter)\n await server.register(pluginCrumb)\n\n server.ext('onPreResponse', (request: Request, h: ResponseToolkit) => {\n const { response } = request\n\n if ('isBoom' in response) {\n return h.continue\n }\n\n // Prevent search engine indexing\n response.header('x-robots-tag', 'noindex, nofollow')\n\n // Disable cache to ensure back/foward navigation updates progress\n if (\n !request.path.startsWith('/javascripts/') &&\n !request.path.startsWith('/stylesheets/') &&\n !request.path.startsWith('/assets/')\n ) {\n response.header('cache-control', 'no-store')\n }\n\n return h.continue\n })\n\n await server.register(pluginViews)\n await server.register(pluginEngine)\n\n await server.register({\n plugin: {\n name: 'router',\n register: (server) => {\n server.route(publicRoutes)\n }\n }\n })\n\n await server.register(pluginErrorPages)\n await server.register(blipp)\n await server.register(requestTracing)\n\n return server\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"index.js","names":["Engine","CatboxMemory","CatboxRedis","hapi","inert","Scooter","Wreck","blipp","ProxyAgent","config","requestLogger","requestTracing","buildRedisClient","configureCrumbPlugin","configureEnginePlugin","pluginErrorPages","plugin","pluginViews","pluginPulse","pluginSession","publicRoutes","prepareSecureContext","proxyAgent","agents","https","http","httpsAllowUnauthorized","serverOptions","debug","request","get","port","router","stripTrailingSlash","routes","validate","options","abortEarly","security","hsts","maxAge","includeSubDomains","preload","xss","noSniff","xframe","cache","name","engine","client","createServer","routeConfig","server","register","pluginCrumb","pluginEngine","ext","h","response","continue","header","path","startsWith","route"],"sources":["../../src/server/index.ts"],"sourcesContent":["import { Engine as CatboxMemory } from '@hapi/catbox-memory'\nimport { Engine as CatboxRedis } from '@hapi/catbox-redis'\nimport hapi, {\n type Request,\n type ResponseToolkit,\n type ServerOptions\n} from '@hapi/hapi'\nimport inert from '@hapi/inert'\nimport Scooter from '@hapi/scooter'\nimport Wreck from '@hapi/wreck'\nimport blipp from 'blipp'\nimport { ProxyAgent } from 'proxy-agent'\n\nimport { config } from '~/src/config/index.js'\nimport { requestLogger } from '~/src/server/common/helpers/logging/request-logger.js'\nimport { requestTracing } from '~/src/server/common/helpers/logging/request-tracing.js'\nimport { buildRedisClient } from '~/src/server/common/helpers/redis-client.js'\nimport { configureCrumbPlugin } from '~/src/server/plugins/crumb.js'\nimport { configureEnginePlugin } from '~/src/server/plugins/engine/configureEnginePlugin.js'\nimport pluginErrorPages from '~/src/server/plugins/errorPages.js'\nimport { plugin as pluginViews } from '~/src/server/plugins/nunjucks/index.js'\nimport pluginPulse from '~/src/server/plugins/pulse.js'\nimport pluginSession from '~/src/server/plugins/session.js'\nimport { publicRoutes } from '~/src/server/routes/index.js'\nimport { prepareSecureContext } from '~/src/server/secure-context.js'\nimport { type RouteConfig } from '~/src/server/types.js'\n\nconst proxyAgent = new ProxyAgent()\n\nWreck.agents = {\n https: proxyAgent,\n http: proxyAgent,\n httpsAllowUnauthorized: proxyAgent\n}\n\nconst serverOptions = (): ServerOptions => {\n const serverOptions: ServerOptions = {\n debug: { request: [`${config.get('isDevelopment')}`] },\n port: config.get('port'),\n router: {\n stripTrailingSlash: true\n },\n routes: {\n validate: {\n options: {\n abortEarly: false\n }\n },\n security: {\n hsts: {\n maxAge: 31536000,\n includeSubDomains: true,\n preload: false\n },\n xss: 'enabled',\n noSniff: true,\n xframe: true\n }\n },\n cache: [\n {\n name: 'session',\n engine: config.get('isTest')\n ? new CatboxMemory()\n : new CatboxRedis({\n client: buildRedisClient()\n })\n }\n ]\n }\n\n return serverOptions\n}\n\nexport async function createServer(routeConfig?: RouteConfig) {\n const server = hapi.server(serverOptions())\n\n await server.register(requestLogger)\n\n if (config.get('isProduction')) {\n prepareSecureContext(server)\n }\n\n const pluginCrumb = configureCrumbPlugin(routeConfig)\n const pluginEngine = await configureEnginePlugin(routeConfig)\n\n await server.register(pluginSession)\n await server.register(pluginPulse)\n await server.register(inert)\n await server.register(Scooter)\n await server.register(pluginCrumb)\n\n await server.register(pluginEngine)\n\n server.ext('onPreResponse', (request: Request, h: ResponseToolkit) => {\n const { response } = request\n\n if ('isBoom' in response) {\n return h.continue\n }\n\n // Prevent search engine indexing\n response.header('x-robots-tag', 'noindex, nofollow')\n\n // Disable cache to ensure back/foward navigation updates progress\n if (\n !request.path.startsWith('/javascripts/') &&\n !request.path.startsWith('/stylesheets/') &&\n !request.path.startsWith('/assets/')\n ) {\n response.header('cache-control', 'no-store')\n }\n\n return h.continue\n })\n\n await server.register(pluginViews)\n\n await server.register({\n plugin: {\n name: 'router',\n register: (server) => {\n server.route(publicRoutes)\n }\n }\n })\n\n await server.register(pluginErrorPages)\n await server.register(blipp)\n await server.register(requestTracing)\n\n return server\n}\n"],"mappings":"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","ignoreList":[]}
@@ -12,7 +12,9 @@ export class AutocompleteField extends SelectField {
12
12
  if (options.required !== false) {
13
13
  const messages = options.customValidationMessages;
14
14
  formSchema = formSchema.messages({
15
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
15
16
  'any.only': messages?.['any.only'] ?? messageTemplate.required,
17
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
16
18
  'any.required': messages?.['any.required'] ?? messageTemplate.required
17
19
  });
18
20
  }
@@ -1 +1 @@
1
- {"version":3,"file":"AutocompleteField.js","names":["SelectField","messageTemplate","AutocompleteField","constructor","def","props","options","formSchema","required","messages","customValidationMessages","getViewModel","payload","errors","viewModel","formGroup","attributes"],"sources":["../../../../../src/server/plugins/engine/components/AutocompleteField.ts"],"sourcesContent":["import { type AutocompleteFieldComponent } from '@defra/forms-model'\n\nimport { SelectField } from '~/src/server/plugins/engine/components/SelectField.js'\nimport { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'\nimport {\n type FormPayload,\n type FormSubmissionError\n} from '~/src/server/plugins/engine/types.js'\n\nexport class AutocompleteField extends SelectField {\n declare options: AutocompleteFieldComponent['options']\n\n constructor(\n def: AutocompleteFieldComponent,\n props: ConstructorParameters<typeof SelectField>[1]\n ) {\n super(def, props)\n\n const { options } = def\n let { formSchema } = this\n\n if (options.required !== false) {\n const messages = options.customValidationMessages\n\n formSchema = formSchema.messages({\n 'any.only': messages?.['any.only'] ?? messageTemplate.required,\n 'any.required': messages?.['any.required'] ?? messageTemplate.required\n })\n }\n\n this.options = options\n this.formSchema = formSchema\n }\n\n getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {\n const viewModel = super.getViewModel(payload, errors)\n let { formGroup } = viewModel\n\n formGroup ??= {}\n formGroup.attributes = {\n 'data-module': 'govuk-accessible-autocomplete'\n }\n\n return {\n ...viewModel,\n formGroup\n }\n }\n}\n"],"mappings":"AAEA,SAASA,WAAW;AACpB,SAASC,eAAe;AAMxB,OAAO,MAAMC,iBAAiB,SAASF,WAAW,CAAC;EAGjDG,WAAWA,CACTC,GAA+B,EAC/BC,KAAmD,EACnD;IACA,KAAK,CAACD,GAAG,EAAEC,KAAK,CAAC;IAEjB,MAAM;MAAEC;IAAQ,CAAC,GAAGF,GAAG;IACvB,IAAI;MAAEG;IAAW,CAAC,GAAG,IAAI;IAEzB,IAAID,OAAO,CAACE,QAAQ,KAAK,KAAK,EAAE;MAC9B,MAAMC,QAAQ,GAAGH,OAAO,CAACI,wBAAwB;MAEjDH,UAAU,GAAGA,UAAU,CAACE,QAAQ,CAAC;QAC/B,UAAU,EAAEA,QAAQ,GAAG,UAAU,CAAC,IAAIR,eAAe,CAACO,QAAQ;QAC9D,cAAc,EAAEC,QAAQ,GAAG,cAAc,CAAC,IAAIR,eAAe,CAACO;MAChE,CAAC,CAAC;IACJ;IAEA,IAAI,CAACF,OAAO,GAAGA,OAAO;IACtB,IAAI,CAACC,UAAU,GAAGA,UAAU;EAC9B;EAEAI,YAAYA,CAACC,OAAoB,EAAEC,MAA8B,EAAE;IACjE,MAAMC,SAAS,GAAG,KAAK,CAACH,YAAY,CAACC,OAAO,EAAEC,MAAM,CAAC;IACrD,IAAI;MAAEE;IAAU,CAAC,GAAGD,SAAS;IAE7BC,SAAS,KAAK,CAAC,CAAC;IAChBA,SAAS,CAACC,UAAU,GAAG;MACrB,aAAa,EAAE;IACjB,CAAC;IAED,OAAO;MACL,GAAGF,SAAS;MACZC;IACF,CAAC;EACH;AACF","ignoreList":[]}
1
+ {"version":3,"file":"AutocompleteField.js","names":["SelectField","messageTemplate","AutocompleteField","constructor","def","props","options","formSchema","required","messages","customValidationMessages","getViewModel","payload","errors","viewModel","formGroup","attributes"],"sources":["../../../../../src/server/plugins/engine/components/AutocompleteField.ts"],"sourcesContent":["import { type AutocompleteFieldComponent } from '@defra/forms-model'\n\nimport { SelectField } from '~/src/server/plugins/engine/components/SelectField.js'\nimport { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'\nimport {\n type FormPayload,\n type FormSubmissionError\n} from '~/src/server/plugins/engine/types.js'\n\nexport class AutocompleteField extends SelectField {\n declare options: AutocompleteFieldComponent['options']\n\n constructor(\n def: AutocompleteFieldComponent,\n props: ConstructorParameters<typeof SelectField>[1]\n ) {\n super(def, props)\n\n const { options } = def\n let { formSchema } = this\n\n if (options.required !== false) {\n const messages = options.customValidationMessages\n\n formSchema = formSchema.messages({\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n 'any.only':\n messages?.['any.only'] ?? (messageTemplate.required as string),\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n 'any.required':\n messages?.['any.required'] ?? (messageTemplate.required as string)\n })\n }\n\n this.options = options\n this.formSchema = formSchema\n }\n\n getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {\n const viewModel = super.getViewModel(payload, errors)\n let { formGroup } = viewModel\n\n formGroup ??= {}\n formGroup.attributes = {\n 'data-module': 'govuk-accessible-autocomplete'\n }\n\n return {\n ...viewModel,\n formGroup\n }\n }\n}\n"],"mappings":"AAEA,SAASA,WAAW;AACpB,SAASC,eAAe;AAMxB,OAAO,MAAMC,iBAAiB,SAASF,WAAW,CAAC;EAGjDG,WAAWA,CACTC,GAA+B,EAC/BC,KAAmD,EACnD;IACA,KAAK,CAACD,GAAG,EAAEC,KAAK,CAAC;IAEjB,MAAM;MAAEC;IAAQ,CAAC,GAAGF,GAAG;IACvB,IAAI;MAAEG;IAAW,CAAC,GAAG,IAAI;IAEzB,IAAID,OAAO,CAACE,QAAQ,KAAK,KAAK,EAAE;MAC9B,MAAMC,QAAQ,GAAGH,OAAO,CAACI,wBAAwB;MAEjDH,UAAU,GAAGA,UAAU,CAACE,QAAQ,CAAC;QAC/B;QACA,UAAU,EACRA,QAAQ,GAAG,UAAU,CAAC,IAAKR,eAAe,CAACO,QAAmB;QAChE;QACA,cAAc,EACZC,QAAQ,GAAG,cAAc,CAAC,IAAKR,eAAe,CAACO;MACnD,CAAC,CAAC;IACJ;IAEA,IAAI,CAACF,OAAO,GAAGA,OAAO;IACtB,IAAI,CAACC,UAAU,GAAGA,UAAU;EAC9B;EAEAI,YAAYA,CAACC,OAAoB,EAAEC,MAA8B,EAAE;IACjE,MAAMC,SAAS,GAAG,KAAK,CAACH,YAAY,CAACC,OAAO,EAAEC,MAAM,CAAC;IACrD,IAAI;MAAEE;IAAU,CAAC,GAAGD,SAAS;IAE7BC,SAAS,KAAK,CAAC,CAAC;IAChBA,SAAS,CAACC,UAAU,GAAG;MACrB,aAAa,EAAE;IACjB,CAAC;IAED,OAAO;MACL,GAAGF,SAAS;MACZC;IACF,CAAC;EACH;AACF","ignoreList":[]}
@@ -8,12 +8,11 @@ export class CheckboxesField extends SelectionControlField {
8
8
  listType: type
9
9
  } = this;
10
10
  const {
11
- options,
12
- title
11
+ options
13
12
  } = def;
14
13
  let formSchema = type === 'string' ? joi.array() : joi.array();
15
- const itemsSchema = joi[type]().valid(...this.values).label(title);
16
- formSchema = formSchema.items(itemsSchema).single().label(title).required();
14
+ const itemsSchema = joi[type]().valid(...this.values).label(this.label);
15
+ formSchema = formSchema.items(itemsSchema).single().label(this.label).required();
17
16
  if (options.required === false) {
18
17
  formSchema = formSchema.optional();
19
18
  }
@@ -1 +1 @@
1
- {"version":3,"file":"CheckboxesField.js","names":["joi","isFormValue","SelectionControlField","CheckboxesField","constructor","def","props","listType","type","options","title","formSchema","array","itemsSchema","valid","values","label","items","single","required","optional","default","stateSchema","allow","getFormValueFromState","state","name","getFormValue","selected","filter","item","includes","value","map","length","undefined","isValue","getDisplayStringFromState","text","join","getContextValueFromState","Array","isArray","every"],"sources":["../../../../../src/server/plugins/engine/components/CheckboxesField.ts"],"sourcesContent":["import { type CheckboxesFieldComponent, type Item } from '@defra/forms-model'\nimport joi, { type ArraySchema } from 'joi'\n\nimport { isFormValue } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { SelectionControlField } from '~/src/server/plugins/engine/components/SelectionControlField.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'\nimport { type QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport {\n type FormState,\n type FormStateValue,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\n\nexport class CheckboxesField extends SelectionControlField {\n declare options: CheckboxesFieldComponent['options']\n declare formSchema: ArraySchema<string> | ArraySchema<number>\n declare stateSchema: ArraySchema<string> | ArraySchema<number>\n\n constructor(\n def: CheckboxesFieldComponent,\n props: ConstructorParameters<typeof SelectionControlField>[1]\n ) {\n super(def, props)\n\n const { listType: type } = this\n const { options, title } = def\n\n let formSchema =\n type === 'string' ? joi.array<string>() : joi.array<number>()\n\n const itemsSchema = joi[type]()\n .valid(...this.values)\n .label(title)\n\n formSchema = formSchema.items(itemsSchema).single().label(title).required()\n\n if (options.required === false) {\n formSchema = formSchema.optional()\n }\n\n this.formSchema = formSchema.default([])\n this.stateSchema = formSchema.default(null).allow(null)\n this.options = options\n }\n\n getFormValueFromState(state: FormSubmissionState) {\n const { items, name } = this\n\n // State checkbox values\n const values = this.getFormValue(state[name]) ?? []\n\n // Map (or discard) state values to item values\n const selected = items\n .filter((item) => values.includes(item.value))\n .map((item) => item.value)\n\n return selected.length ? selected : undefined\n }\n\n getFormValue(value?: FormStateValue | FormState) {\n return this.isValue(value) ? value : undefined\n }\n\n getDisplayStringFromState(state: FormSubmissionState) {\n const { items } = this\n\n // Selected checkbox values\n const selected = this.getFormValueFromState(state) ?? []\n\n // Map selected values to text\n return items\n .filter((item) => selected.includes(item.value))\n .map((item) => item.text)\n .join(', ')\n }\n\n getContextValueFromState(state: FormSubmissionState) {\n const values = this.getFormValueFromState(state)\n\n /**\n * For evaluation context purposes, optional {@link CheckboxesField}\n * with an undefined value (i.e. nothing selected) should default to [].\n * This way conditions are not evaluated against `undefined` which throws errors.\n * Currently these errors are caught and the evaluation returns default `false`.\n * @see {@link QuestionPageController.getNextPath} for `undefined` return value\n * @see {@link FormModel.makeCondition} for try/catch block with default `false`\n * For negative conditions this is a problem because E.g.\n * The condition: 'selectedchecks' does not contain 'someval'\n * should return true IF 'selectedchecks' is undefined, not throw and return false.\n */\n return values ?? []\n }\n\n isValue(value?: FormStateValue | FormState): value is Item['value'][] {\n if (!Array.isArray(value)) {\n return false\n }\n\n // Skip checks when empty\n if (!value.length) {\n return true\n }\n\n return value.every(isFormValue)\n }\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"CheckboxesField.js","names":["joi","isFormValue","SelectionControlField","CheckboxesField","constructor","def","props","listType","type","options","formSchema","array","itemsSchema","valid","values","label","items","single","required","optional","default","stateSchema","allow","getFormValueFromState","state","name","getFormValue","selected","filter","item","includes","value","map","length","undefined","isValue","getDisplayStringFromState","text","join","getContextValueFromState","Array","isArray","every"],"sources":["../../../../../src/server/plugins/engine/components/CheckboxesField.ts"],"sourcesContent":["import { type CheckboxesFieldComponent, type Item } from '@defra/forms-model'\nimport joi, { type ArraySchema } from 'joi'\n\nimport { isFormValue } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { SelectionControlField } from '~/src/server/plugins/engine/components/SelectionControlField.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'\nimport { type QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport {\n type FormState,\n type FormStateValue,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\n\nexport class CheckboxesField extends SelectionControlField {\n declare options: CheckboxesFieldComponent['options']\n declare formSchema: ArraySchema<string> | ArraySchema<number>\n declare stateSchema: ArraySchema<string> | ArraySchema<number>\n\n constructor(\n def: CheckboxesFieldComponent,\n props: ConstructorParameters<typeof SelectionControlField>[1]\n ) {\n super(def, props)\n\n const { listType: type } = this\n const { options } = def\n\n let formSchema =\n type === 'string' ? joi.array<string>() : joi.array<number>()\n\n const itemsSchema = joi[type]()\n .valid(...this.values)\n .label(this.label)\n\n formSchema = formSchema\n .items(itemsSchema)\n .single()\n .label(this.label)\n .required()\n\n if (options.required === false) {\n formSchema = formSchema.optional()\n }\n\n this.formSchema = formSchema.default([])\n this.stateSchema = formSchema.default(null).allow(null)\n this.options = options\n }\n\n getFormValueFromState(state: FormSubmissionState) {\n const { items, name } = this\n\n // State checkbox values\n const values = this.getFormValue(state[name]) ?? []\n\n // Map (or discard) state values to item values\n const selected = items\n .filter((item) => values.includes(item.value))\n .map((item) => item.value)\n\n return selected.length ? selected : undefined\n }\n\n getFormValue(value?: FormStateValue | FormState) {\n return this.isValue(value) ? value : undefined\n }\n\n getDisplayStringFromState(state: FormSubmissionState) {\n const { items } = this\n\n // Selected checkbox values\n const selected = this.getFormValueFromState(state) ?? []\n\n // Map selected values to text\n return items\n .filter((item) => selected.includes(item.value))\n .map((item) => item.text)\n .join(', ')\n }\n\n getContextValueFromState(state: FormSubmissionState) {\n const values = this.getFormValueFromState(state)\n\n /**\n * For evaluation context purposes, optional {@link CheckboxesField}\n * with an undefined value (i.e. nothing selected) should default to [].\n * This way conditions are not evaluated against `undefined` which throws errors.\n * Currently these errors are caught and the evaluation returns default `false`.\n * @see {@link QuestionPageController.getNextPath} for `undefined` return value\n * @see {@link FormModel.makeCondition} for try/catch block with default `false`\n * For negative conditions this is a problem because E.g.\n * The condition: 'selectedchecks' does not contain 'someval'\n * should return true IF 'selectedchecks' is undefined, not throw and return false.\n */\n return values ?? []\n }\n\n isValue(value?: FormStateValue | FormState): value is Item['value'][] {\n if (!Array.isArray(value)) {\n return false\n }\n\n // Skip checks when empty\n if (!value.length) {\n return true\n }\n\n return value.every(isFormValue)\n }\n}\n"],"mappings":"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","ignoreList":[]}
@@ -68,7 +68,7 @@ export class ComponentCollection {
68
68
  }
69
69
 
70
70
  // Update error with parent title
71
- error.local.title ??= parent?.title;
71
+ error.local.title ??= parent?.label;
72
72
  return error;
73
73
  });
74
74
  });
@@ -139,23 +139,21 @@ export class ComponentCollection {
139
139
  }
140
140
  return context;
141
141
  }
142
+
143
+ /**
144
+ * Get all errors for all fields in this collection
145
+ */
142
146
  getErrors(errors) {
143
- const {
144
- fields
145
- } = this;
146
- const list = [];
147
+ return this.getFieldErrors(field => field.getErrors(errors), errors);
148
+ }
147
149
 
148
- // Add only one error per field
149
- for (const field of fields) {
150
- const error = field.getError(errors);
151
- if (error) {
152
- list.push(error);
153
- }
154
- }
155
- if (!list.length) {
156
- return;
157
- }
158
- return list;
150
+ /**
151
+ * Get view errors for all fields in this collection.
152
+ * For most fields this means filtering to the first error in the list.
153
+ * Composite fields like UKAddress can choose to return more than one error.
154
+ */
155
+ getViewErrors(errors) {
156
+ return this.getFieldErrors(field => field.getViewErrors(errors), errors);
159
157
  }
160
158
  getViewModel(payload, errors, query = {}) {
161
159
  const {
@@ -187,6 +185,29 @@ export class ComponentCollection {
187
185
  errors: this.page?.getErrors(details) ?? getErrors(details)
188
186
  };
189
187
  }
188
+
189
+ /**
190
+ * Helper to get errors from all fields
191
+ */
192
+ getFieldErrors(callback, errors) {
193
+ const {
194
+ fields
195
+ } = this;
196
+ if (!errors?.length) {
197
+ return;
198
+ }
199
+ const list = [];
200
+ for (const field of fields) {
201
+ const fieldErrors = callback(field);
202
+ if (fieldErrors?.length) {
203
+ list.push(...fieldErrors);
204
+ }
205
+ }
206
+ if (!list.length) {
207
+ return;
208
+ }
209
+ return list;
210
+ }
190
211
  }
191
212
 
192
213
  /**