@ministryofjustice/frontend 5.0.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/moj/all.bundle.js +1549 -1062
  2. package/moj/all.bundle.js.map +1 -1
  3. package/moj/all.bundle.mjs +1845 -1054
  4. package/moj/all.bundle.mjs.map +1 -1
  5. package/moj/all.mjs +7 -90
  6. package/moj/all.mjs.map +1 -1
  7. package/moj/all.scss +1 -0
  8. package/moj/all.scss.map +1 -1
  9. package/moj/common/index.mjs +57 -0
  10. package/moj/common/index.mjs.map +1 -0
  11. package/moj/common/moj-frontend-version.mjs +14 -0
  12. package/moj/common/moj-frontend-version.mjs.map +1 -0
  13. package/moj/components/add-another/add-another.bundle.js +105 -76
  14. package/moj/components/add-another/add-another.bundle.js.map +1 -1
  15. package/moj/components/add-another/add-another.bundle.mjs +222 -71
  16. package/moj/components/add-another/add-another.bundle.mjs.map +1 -1
  17. package/moj/components/add-another/add-another.mjs +103 -72
  18. package/moj/components/add-another/add-another.mjs.map +1 -1
  19. package/moj/components/alert/alert.bundle.js +115 -191
  20. package/moj/components/alert/alert.bundle.js.map +1 -1
  21. package/moj/components/alert/alert.bundle.mjs +354 -186
  22. package/moj/components/alert/alert.bundle.mjs.map +1 -1
  23. package/moj/components/alert/alert.mjs +55 -140
  24. package/moj/components/alert/alert.mjs.map +1 -1
  25. package/moj/components/button-menu/README.md +3 -1
  26. package/moj/components/button-menu/button-menu.bundle.js +91 -120
  27. package/moj/components/button-menu/button-menu.bundle.js.map +1 -1
  28. package/moj/components/button-menu/button-menu.bundle.mjs +329 -114
  29. package/moj/components/button-menu/button-menu.bundle.mjs.map +1 -1
  30. package/moj/components/button-menu/button-menu.mjs +89 -116
  31. package/moj/components/button-menu/button-menu.mjs.map +1 -1
  32. package/moj/components/date-picker/date-picker.bundle.js +174 -154
  33. package/moj/components/date-picker/date-picker.bundle.js.map +1 -1
  34. package/moj/components/date-picker/date-picker.bundle.mjs +411 -147
  35. package/moj/components/date-picker/date-picker.bundle.mjs.map +1 -1
  36. package/moj/components/date-picker/date-picker.mjs +172 -150
  37. package/moj/components/date-picker/date-picker.mjs.map +1 -1
  38. package/moj/components/filter/template.njk +1 -1
  39. package/moj/components/filter-toggle-button/filter-toggle-button.bundle.js +133 -44
  40. package/moj/components/filter-toggle-button/filter-toggle-button.bundle.js.map +1 -1
  41. package/moj/components/filter-toggle-button/filter-toggle-button.bundle.mjs +374 -41
  42. package/moj/components/filter-toggle-button/filter-toggle-button.bundle.mjs.map +1 -1
  43. package/moj/components/filter-toggle-button/filter-toggle-button.mjs +131 -40
  44. package/moj/components/filter-toggle-button/filter-toggle-button.mjs.map +1 -1
  45. package/moj/components/form-validator/form-validator.bundle.js +159 -69
  46. package/moj/components/form-validator/form-validator.bundle.js.map +1 -1
  47. package/moj/components/form-validator/form-validator.bundle.mjs +399 -65
  48. package/moj/components/form-validator/form-validator.bundle.mjs.map +1 -1
  49. package/moj/components/form-validator/form-validator.mjs +134 -54
  50. package/moj/components/form-validator/form-validator.mjs.map +1 -1
  51. package/moj/components/multi-file-upload/multi-file-upload.bundle.js +291 -117
  52. package/moj/components/multi-file-upload/multi-file-upload.bundle.js.map +1 -1
  53. package/moj/components/multi-file-upload/multi-file-upload.bundle.mjs +527 -109
  54. package/moj/components/multi-file-upload/multi-file-upload.bundle.mjs.map +1 -1
  55. package/moj/components/multi-file-upload/multi-file-upload.mjs +288 -101
  56. package/moj/components/multi-file-upload/multi-file-upload.mjs.map +1 -1
  57. package/moj/components/multi-file-upload/template.njk +1 -1
  58. package/moj/components/multi-select/multi-select.bundle.js +106 -41
  59. package/moj/components/multi-select/multi-select.bundle.js.map +1 -1
  60. package/moj/components/multi-select/multi-select.bundle.mjs +346 -37
  61. package/moj/components/multi-select/multi-select.bundle.mjs.map +1 -1
  62. package/moj/components/multi-select/multi-select.mjs +104 -37
  63. package/moj/components/multi-select/multi-select.mjs.map +1 -1
  64. package/moj/components/password-reveal/_password-reveal.scss +3 -1
  65. package/moj/components/password-reveal/_password-reveal.scss.map +1 -1
  66. package/moj/components/password-reveal/password-reveal.bundle.js +32 -29
  67. package/moj/components/password-reveal/password-reveal.bundle.js.map +1 -1
  68. package/moj/components/password-reveal/password-reveal.bundle.mjs +149 -24
  69. package/moj/components/password-reveal/password-reveal.bundle.mjs.map +1 -1
  70. package/moj/components/password-reveal/password-reveal.mjs +30 -25
  71. package/moj/components/password-reveal/password-reveal.mjs.map +1 -1
  72. package/moj/components/rich-text-editor/README.md +4 -3
  73. package/moj/components/rich-text-editor/rich-text-editor.bundle.js +127 -62
  74. package/moj/components/rich-text-editor/rich-text-editor.bundle.js.map +1 -1
  75. package/moj/components/rich-text-editor/rich-text-editor.bundle.mjs +367 -58
  76. package/moj/components/rich-text-editor/rich-text-editor.bundle.mjs.map +1 -1
  77. package/moj/components/rich-text-editor/rich-text-editor.mjs +125 -58
  78. package/moj/components/rich-text-editor/rich-text-editor.mjs.map +1 -1
  79. package/moj/components/search-toggle/search-toggle.bundle.js +94 -26
  80. package/moj/components/search-toggle/search-toggle.bundle.js.map +1 -1
  81. package/moj/components/search-toggle/search-toggle.bundle.mjs +334 -22
  82. package/moj/components/search-toggle/search-toggle.bundle.mjs.map +1 -1
  83. package/moj/components/search-toggle/search-toggle.mjs +92 -22
  84. package/moj/components/search-toggle/search-toggle.mjs.map +1 -1
  85. package/moj/components/sortable-table/sortable-table.bundle.js +151 -83
  86. package/moj/components/sortable-table/sortable-table.bundle.js.map +1 -1
  87. package/moj/components/sortable-table/sortable-table.bundle.mjs +390 -78
  88. package/moj/components/sortable-table/sortable-table.bundle.mjs.map +1 -1
  89. package/moj/components/sortable-table/sortable-table.mjs +149 -79
  90. package/moj/components/sortable-table/sortable-table.mjs.map +1 -1
  91. package/moj/core/_all.scss +3 -0
  92. package/moj/core/_all.scss.map +1 -0
  93. package/moj/core/_moj-frontend-properties.scss +7 -0
  94. package/moj/core/_moj-frontend-properties.scss.map +1 -0
  95. package/moj/filters/prototype-kit-13-filters.js +4 -3
  96. package/moj/helpers.bundle.js +22 -77
  97. package/moj/helpers.bundle.js.map +1 -1
  98. package/moj/helpers.bundle.mjs +23 -74
  99. package/moj/helpers.bundle.mjs.map +1 -1
  100. package/moj/helpers.mjs +23 -74
  101. package/moj/helpers.mjs.map +1 -1
  102. package/moj/moj-frontend.min.css +1 -1
  103. package/moj/moj-frontend.min.css.map +1 -1
  104. package/moj/moj-frontend.min.js +1 -1
  105. package/moj/moj-frontend.min.js.map +1 -1
  106. package/package.json +1 -1
  107. package/moj/version.bundle.js +0 -12
  108. package/moj/version.bundle.js.map +0 -1
  109. package/moj/version.bundle.mjs +0 -4
  110. package/moj/version.bundle.mjs.map +0 -1
  111. package/moj/version.mjs +0 -4
  112. package/moj/version.mjs.map +0 -1
@@ -1,173 +1,432 @@
1
- function dragAndDropSupported() {
2
- const div = document.createElement('div');
3
- return typeof div.ondrop !== 'undefined';
1
+ function isInitialised($root, moduleName) {
2
+ return $root instanceof HTMLElement && $root.hasAttribute(`data-${moduleName}-init`);
4
3
  }
5
- function formDataSupported() {
6
- return typeof FormData === 'function';
4
+
5
+ /**
6
+ * Checks if GOV.UK Frontend is supported on this page
7
+ *
8
+ * Some browsers will load and run our JavaScript but GOV.UK Frontend
9
+ * won't be supported.
10
+ *
11
+ * @param {HTMLElement | null} [$scope] - (internal) `<body>` HTML element checked for browser support
12
+ * @returns {boolean} Whether GOV.UK Frontend is supported on this page
13
+ */
14
+ function isSupported($scope = document.body) {
15
+ if (!$scope) {
16
+ return false;
17
+ }
18
+ return $scope.classList.contains('govuk-frontend-supported');
19
+ }
20
+ function isArray(option) {
21
+ return Array.isArray(option);
22
+ }
23
+ function isObject(option) {
24
+ return !!option && typeof option === 'object' && !isArray(option);
25
+ }
26
+ function formatErrorMessage(Component, message) {
27
+ return `${Component.moduleName}: ${message}`;
28
+ }
29
+
30
+ class GOVUKFrontendError extends Error {
31
+ constructor(...args) {
32
+ super(...args);
33
+ this.name = 'GOVUKFrontendError';
34
+ }
35
+ }
36
+ class SupportError extends GOVUKFrontendError {
37
+ /**
38
+ * Checks if GOV.UK Frontend is supported on this page
39
+ *
40
+ * @param {HTMLElement | null} [$scope] - HTML element `<body>` checked for browser support
41
+ */
42
+ constructor($scope = document.body) {
43
+ const supportMessage = 'noModule' in HTMLScriptElement.prototype ? 'GOV.UK Frontend initialised without `<body class="govuk-frontend-supported">` from template `<script>` snippet' : 'GOV.UK Frontend is not supported in this browser';
44
+ super($scope ? supportMessage : 'GOV.UK Frontend initialised without `<script type="module">`');
45
+ this.name = 'SupportError';
46
+ }
47
+ }
48
+ class ConfigError extends GOVUKFrontendError {
49
+ constructor(...args) {
50
+ super(...args);
51
+ this.name = 'ConfigError';
52
+ }
7
53
  }
8
- function fileApiSupported() {
9
- const input = document.createElement('input');
10
- input.type = 'file';
11
- return typeof input.files !== 'undefined';
54
+ class ElementError extends GOVUKFrontendError {
55
+ constructor(messageOrOptions) {
56
+ let message = typeof messageOrOptions === 'string' ? messageOrOptions : '';
57
+ if (typeof messageOrOptions === 'object') {
58
+ const {
59
+ component,
60
+ identifier,
61
+ element,
62
+ expectedType
63
+ } = messageOrOptions;
64
+ message = identifier;
65
+ message += element ? ` is not of type ${expectedType != null ? expectedType : 'HTMLElement'}` : ' not found';
66
+ message = formatErrorMessage(component, message);
67
+ }
68
+ super(message);
69
+ this.name = 'ElementError';
70
+ }
71
+ }
72
+ class InitError extends GOVUKFrontendError {
73
+ constructor(componentOrMessage) {
74
+ const message = typeof componentOrMessage === 'string' ? componentOrMessage : formatErrorMessage(componentOrMessage, `Root element (\`$root\`) already initialised`);
75
+ super(message);
76
+ this.name = 'InitError';
77
+ }
78
+ }
79
+
80
+ class Component {
81
+ /**
82
+ * Returns the root element of the component
83
+ *
84
+ * @protected
85
+ * @returns {RootElementType} - the root element of component
86
+ */
87
+ get $root() {
88
+ return this._$root;
89
+ }
90
+ constructor($root) {
91
+ this._$root = void 0;
92
+ const childConstructor = this.constructor;
93
+ if (typeof childConstructor.moduleName !== 'string') {
94
+ throw new InitError(`\`moduleName\` not defined in component`);
95
+ }
96
+ if (!($root instanceof childConstructor.elementType)) {
97
+ throw new ElementError({
98
+ element: $root,
99
+ component: childConstructor,
100
+ identifier: 'Root element (`$root`)',
101
+ expectedType: childConstructor.elementType.name
102
+ });
103
+ } else {
104
+ this._$root = $root;
105
+ }
106
+ childConstructor.checkSupport();
107
+ this.checkInitialised();
108
+ const moduleName = childConstructor.moduleName;
109
+ this.$root.setAttribute(`data-${moduleName}-init`, '');
110
+ }
111
+ checkInitialised() {
112
+ const constructor = this.constructor;
113
+ const moduleName = constructor.moduleName;
114
+ if (moduleName && isInitialised(this.$root, moduleName)) {
115
+ throw new InitError(constructor);
116
+ }
117
+ }
118
+ static checkSupport() {
119
+ if (!isSupported()) {
120
+ throw new SupportError();
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * @typedef ChildClass
127
+ * @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component
128
+ */
129
+
130
+ /**
131
+ * @typedef {typeof Component & ChildClass} ChildClassConstructor
132
+ */
133
+ Component.elementType = HTMLElement;
134
+
135
+ const configOverride = Symbol.for('configOverride');
136
+ class ConfigurableComponent extends Component {
137
+ [configOverride](param) {
138
+ return {};
139
+ }
140
+
141
+ /**
142
+ * Returns the root element of the component
143
+ *
144
+ * @protected
145
+ * @returns {ConfigurationType} - the root element of component
146
+ */
147
+ get config() {
148
+ return this._config;
149
+ }
150
+ constructor($root, config) {
151
+ super($root);
152
+ this._config = void 0;
153
+ const childConstructor = this.constructor;
154
+ if (!isObject(childConstructor.defaults)) {
155
+ throw new ConfigError(formatErrorMessage(childConstructor, 'Config passed as parameter into constructor but no defaults defined'));
156
+ }
157
+ const datasetConfig = normaliseDataset(childConstructor, this._$root.dataset);
158
+ this._config = mergeConfigs(childConstructor.defaults, config != null ? config : {}, this[configOverride](datasetConfig), datasetConfig);
159
+ }
160
+ }
161
+ function normaliseString(value, property) {
162
+ const trimmedValue = value ? value.trim() : '';
163
+ let output;
164
+ let outputType = property == null ? void 0 : property.type;
165
+ if (!outputType) {
166
+ if (['true', 'false'].includes(trimmedValue)) {
167
+ outputType = 'boolean';
168
+ }
169
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
170
+ outputType = 'number';
171
+ }
172
+ }
173
+ switch (outputType) {
174
+ case 'boolean':
175
+ output = trimmedValue === 'true';
176
+ break;
177
+ case 'number':
178
+ output = Number(trimmedValue);
179
+ break;
180
+ default:
181
+ output = value;
182
+ }
183
+ return output;
184
+ }
185
+ function normaliseDataset(Component, dataset) {
186
+ if (!isObject(Component.schema)) {
187
+ throw new ConfigError(formatErrorMessage(Component, 'Config passed as parameter into constructor but no schema defined'));
188
+ }
189
+ const out = {};
190
+ const entries = Object.entries(Component.schema.properties);
191
+ for (const entry of entries) {
192
+ const [namespace, property] = entry;
193
+ const field = namespace.toString();
194
+ if (field in dataset) {
195
+ out[field] = normaliseString(dataset[field], property);
196
+ }
197
+ if ((property == null ? void 0 : property.type) === 'object') {
198
+ out[field] = extractConfigByNamespace(Component.schema, dataset, namespace);
199
+ }
200
+ }
201
+ return out;
202
+ }
203
+ function mergeConfigs(...configObjects) {
204
+ const formattedConfigObject = {};
205
+ for (const configObject of configObjects) {
206
+ for (const key of Object.keys(configObject)) {
207
+ const option = formattedConfigObject[key];
208
+ const override = configObject[key];
209
+ if (isObject(option) && isObject(override)) {
210
+ formattedConfigObject[key] = mergeConfigs(option, override);
211
+ } else {
212
+ formattedConfigObject[key] = override;
213
+ }
214
+ }
215
+ }
216
+ return formattedConfigObject;
217
+ }
218
+ function extractConfigByNamespace(schema, dataset, namespace) {
219
+ const property = schema.properties[namespace];
220
+ if ((property == null ? void 0 : property.type) !== 'object') {
221
+ return;
222
+ }
223
+ const newObject = {
224
+ [namespace]: {}
225
+ };
226
+ for (const [key, value] of Object.entries(dataset)) {
227
+ let current = newObject;
228
+ const keyParts = key.split('.');
229
+ for (const [index, name] of keyParts.entries()) {
230
+ if (isObject(current)) {
231
+ if (index < keyParts.length - 1) {
232
+ if (!isObject(current[name])) {
233
+ current[name] = {};
234
+ }
235
+ current = current[name];
236
+ } else if (key !== namespace) {
237
+ current[name] = normaliseString(value);
238
+ }
239
+ }
240
+ }
241
+ }
242
+ return newObject[namespace];
12
243
  }
13
244
 
14
245
  /* eslint-disable @typescript-eslint/no-empty-function */
15
246
 
16
- class MultiFileUpload {
247
+
248
+ /**
249
+ * @augments {ConfigurableComponent<MultiFileUploadConfig>}
250
+ */
251
+ class MultiFileUpload extends ConfigurableComponent {
17
252
  /**
18
- * @param {MultiFileUploadConfig} [params] - Multi file upload config
253
+ * @param {Element | null} $root - HTML element to use for multi file upload
254
+ * @param {MultiFileUploadConfig} [config] - Multi file upload config
19
255
  */
20
- constructor(params = {}) {
21
- const {
22
- container
23
- } = params;
24
- if (!container || !(container instanceof HTMLElement) || !(dragAndDropSupported() && formDataSupported() && fileApiSupported())) {
256
+ constructor($root, config = {}) {
257
+ var _this$config$feedback;
258
+ super($root, config);
259
+ if (!MultiFileUpload.isSupported()) {
25
260
  return this;
26
261
  }
27
- this.container = container;
28
- this.container.classList.add('moj-multi-file-upload--enhanced');
29
- this.defaultParams = {
30
- uploadFileEntryHook: () => {},
31
- uploadFileExitHook: () => {},
32
- uploadFileErrorHook: () => {},
33
- fileDeleteHook: () => {},
34
- uploadStatusText: 'Uploading files, please wait',
35
- dropzoneHintText: 'Drag and drop files here or',
36
- dropzoneButtonText: 'Choose files'
37
- };
38
- this.params = Object.assign({}, this.defaultParams, params);
39
- this.feedbackContainer = /** @type {HTMLDivElement} */
40
- this.container.querySelector('.moj-multi-file__uploaded-files');
262
+ const $feedbackContainer = (_this$config$feedback = this.config.feedbackContainer.element) != null ? _this$config$feedback : this.$root.querySelector(this.config.feedbackContainer.selector);
263
+ if (!$feedbackContainer || !($feedbackContainer instanceof HTMLElement)) {
264
+ return this;
265
+ }
266
+ this.$feedbackContainer = $feedbackContainer;
41
267
  this.setupFileInput();
42
268
  this.setupDropzone();
43
269
  this.setupLabel();
44
270
  this.setupStatusBox();
45
- this.container.addEventListener('click', this.onFileDeleteClick.bind(this));
271
+ this.$root.addEventListener('click', this.onFileDeleteClick.bind(this));
272
+ this.$root.classList.add('moj-multi-file-upload--enhanced');
46
273
  }
47
274
  setupDropzone() {
48
- this.dropzone = document.createElement('div');
49
- this.dropzone.classList.add('moj-multi-file-upload__dropzone');
50
- this.dropzone.addEventListener('dragover', this.onDragOver.bind(this));
51
- this.dropzone.addEventListener('dragleave', this.onDragLeave.bind(this));
52
- this.dropzone.addEventListener('drop', this.onDrop.bind(this));
53
- this.fileInput.replaceWith(this.dropzone);
54
- this.dropzone.appendChild(this.fileInput);
275
+ this.$dropzone = document.createElement('div');
276
+ this.$dropzone.classList.add('moj-multi-file-upload__dropzone');
277
+ this.$dropzone.addEventListener('dragover', this.onDragOver.bind(this));
278
+ this.$dropzone.addEventListener('dragleave', this.onDragLeave.bind(this));
279
+ this.$dropzone.addEventListener('drop', this.onDrop.bind(this));
280
+ this.$fileInput.replaceWith(this.$dropzone);
281
+ this.$dropzone.appendChild(this.$fileInput);
55
282
  }
56
283
  setupLabel() {
57
- const label = document.createElement('label');
58
- label.setAttribute('for', this.fileInput.id);
59
- label.classList.add('govuk-button', 'govuk-button--secondary');
60
- label.textContent = this.params.dropzoneButtonText;
61
- const hint = document.createElement('p');
62
- hint.classList.add('govuk-body');
63
- hint.textContent = this.params.dropzoneHintText;
64
- this.label = label;
65
- this.dropzone.append(hint);
66
- this.dropzone.append(label);
284
+ const $label = document.createElement('label');
285
+ $label.setAttribute('for', this.$fileInput.id);
286
+ $label.classList.add('govuk-button', 'govuk-button--secondary');
287
+ $label.textContent = this.config.dropzoneButtonText;
288
+ const $hint = document.createElement('p');
289
+ $hint.classList.add('govuk-body');
290
+ $hint.textContent = this.config.dropzoneHintText;
291
+ this.$label = $label;
292
+ this.$dropzone.append($hint);
293
+ this.$dropzone.append($label);
67
294
  }
68
295
  setupFileInput() {
69
- this.fileInput = /** @type {HTMLInputElement} */
70
- this.container.querySelector('.moj-multi-file-upload__input');
71
- this.fileInput.addEventListener('change', this.onFileChange.bind(this));
72
- this.fileInput.addEventListener('focus', this.onFileFocus.bind(this));
73
- this.fileInput.addEventListener('blur', this.onFileBlur.bind(this));
296
+ this.$fileInput = /** @type {HTMLInputElement} */
297
+ this.$root.querySelector('.moj-multi-file-upload__input');
298
+ this.$fileInput.addEventListener('change', this.onFileChange.bind(this));
299
+ this.$fileInput.addEventListener('focus', this.onFileFocus.bind(this));
300
+ this.$fileInput.addEventListener('blur', this.onFileBlur.bind(this));
74
301
  }
75
302
  setupStatusBox() {
76
- this.status = document.createElement('div');
77
- this.status.classList.add('govuk-visually-hidden');
78
- this.status.setAttribute('aria-live', 'polite');
79
- this.status.setAttribute('role', 'status');
80
- this.dropzone.append(this.status);
303
+ this.$status = document.createElement('div');
304
+ this.$status.classList.add('govuk-visually-hidden');
305
+ this.$status.setAttribute('aria-live', 'polite');
306
+ this.$status.setAttribute('role', 'status');
307
+ this.$dropzone.append(this.$status);
81
308
  }
309
+
310
+ /**
311
+ * @param {DragEvent} event - Drag event
312
+ */
82
313
  onDragOver(event) {
83
314
  event.preventDefault();
84
- this.dropzone.classList.add('moj-multi-file-upload--dragover');
315
+ this.$dropzone.classList.add('moj-multi-file-upload--dragover');
85
316
  }
86
317
  onDragLeave() {
87
- this.dropzone.classList.remove('moj-multi-file-upload--dragover');
318
+ this.$dropzone.classList.remove('moj-multi-file-upload--dragover');
88
319
  }
320
+
321
+ /**
322
+ * @param {DragEvent} event - Drag event
323
+ */
89
324
  onDrop(event) {
90
325
  event.preventDefault();
91
- this.dropzone.classList.remove('moj-multi-file-upload--dragover');
92
- this.feedbackContainer.classList.remove('moj-hidden');
93
- this.status.textContent = this.params.uploadStatusText;
326
+ this.$dropzone.classList.remove('moj-multi-file-upload--dragover');
327
+ this.$feedbackContainer.classList.remove('moj-hidden');
328
+ this.$status.textContent = this.config.uploadStatusText;
94
329
  this.uploadFiles(event.dataTransfer.files);
95
330
  }
331
+
332
+ /**
333
+ * @param {FileList} files - File list
334
+ */
96
335
  uploadFiles(files) {
97
336
  for (const file of Array.from(files)) {
98
337
  this.uploadFile(file);
99
338
  }
100
339
  }
101
340
  onFileChange() {
102
- this.feedbackContainer.classList.remove('moj-hidden');
103
- this.status.textContent = this.params.uploadStatusText;
104
- this.uploadFiles(this.fileInput.files);
105
- const fileInput = this.fileInput.cloneNode(true);
106
- if (!fileInput || !(fileInput instanceof HTMLInputElement)) {
341
+ this.$feedbackContainer.classList.remove('moj-hidden');
342
+ this.$status.textContent = this.config.uploadStatusText;
343
+ this.uploadFiles(this.$fileInput.files);
344
+ const $fileInput = this.$fileInput.cloneNode(true);
345
+ if (!$fileInput || !($fileInput instanceof HTMLInputElement)) {
107
346
  return;
108
347
  }
109
- fileInput.value = '';
110
- this.fileInput.replaceWith(fileInput);
348
+ $fileInput.value = '';
349
+ this.$fileInput.replaceWith($fileInput);
111
350
  this.setupFileInput();
112
- this.fileInput.focus();
351
+ this.$fileInput.focus();
113
352
  }
114
353
  onFileFocus() {
115
- this.label.classList.add('moj-multi-file-upload--focused');
354
+ this.$label.classList.add('moj-multi-file-upload--focused');
116
355
  }
117
356
  onFileBlur() {
118
- this.label.classList.remove('moj-multi-file-upload--focused');
357
+ this.$label.classList.remove('moj-multi-file-upload--focused');
119
358
  }
359
+
360
+ /**
361
+ * @param {UploadResponseSuccess['success']} success
362
+ */
120
363
  getSuccessHtml(success) {
121
364
  return `<span class="moj-multi-file-upload__success"> <svg class="moj-banner__icon" fill="currentColor" role="presentation" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 25" height="25" width="25"><path d="M25,6.2L8.7,23.2L0,14.1l4-4.2l4.7,4.9L21,2L25,6.2z"/></svg>${success.messageHtml}</span>`;
122
365
  }
366
+
367
+ /**
368
+ * @param {UploadResponseError['error']} error
369
+ */
123
370
  getErrorHtml(error) {
124
371
  return `<span class="moj-multi-file-upload__error"> <svg class="moj-banner__icon" fill="currentColor" role="presentation" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 25" height="25" width="25"><path d="M13.6,15.4h-2.3v-4.5h2.3V15.4z M13.6,19.8h-2.3v-2.2h2.3V19.8z M0,23.2h25L12.5,2L0,23.2z"/></svg>${error.message}</span>`;
125
372
  }
373
+
374
+ /**
375
+ * @param {File} file
376
+ */
126
377
  getFileRow(file) {
127
- const row = document.createElement('div');
128
- row.classList.add('govuk-summary-list__row', 'moj-multi-file-upload__row');
129
- row.innerHTML = `
378
+ const $row = document.createElement('div');
379
+ $row.classList.add('govuk-summary-list__row', 'moj-multi-file-upload__row');
380
+ $row.innerHTML = `
130
381
  <div class="govuk-summary-list__value moj-multi-file-upload__message">
131
382
  <span class="moj-multi-file-upload__filename">${file.name}</span>
132
383
  <span class="moj-multi-file-upload__progress">0%</span>
133
384
  </div>
134
385
  <div class="govuk-summary-list__actions moj-multi-file-upload__actions"></div>
135
386
  `;
136
- return row;
387
+ return $row;
137
388
  }
389
+
390
+ /**
391
+ * @param {UploadResponseFile} file
392
+ */
138
393
  getDeleteButton(file) {
139
- const button = document.createElement('button');
140
- button.setAttribute('type', 'button');
141
- button.setAttribute('name', 'delete');
142
- button.setAttribute('value', file.filename);
143
- button.classList.add('moj-multi-file-upload__delete', 'govuk-button', 'govuk-button--secondary', 'govuk-!-margin-bottom-0');
144
- button.innerHTML = `Delete <span class="govuk-visually-hidden">${file.originalname}</span>`;
145
- return button;
394
+ const $button = document.createElement('button');
395
+ $button.setAttribute('type', 'button');
396
+ $button.setAttribute('name', 'delete');
397
+ $button.setAttribute('value', file.filename);
398
+ $button.classList.add('moj-multi-file-upload__delete', 'govuk-button', 'govuk-button--secondary', 'govuk-!-margin-bottom-0');
399
+ $button.innerHTML = `Delete <span class="govuk-visually-hidden">${file.originalname}</span>`;
400
+ return $button;
146
401
  }
402
+
403
+ /**
404
+ * @param {File} file
405
+ */
147
406
  uploadFile(file) {
148
- this.params.uploadFileEntryHook(this, file);
149
- const item = this.getFileRow(file);
150
- const message = item.querySelector('.moj-multi-file-upload__message');
151
- const actions = item.querySelector('.moj-multi-file-upload__actions');
152
- const progress = item.querySelector('.moj-multi-file-upload__progress');
407
+ this.config.hooks.entryHook(this, file);
408
+ const $item = this.getFileRow(file);
409
+ const $message = $item.querySelector('.moj-multi-file-upload__message');
410
+ const $actions = $item.querySelector('.moj-multi-file-upload__actions');
411
+ const $progress = $item.querySelector('.moj-multi-file-upload__progress');
153
412
  const formData = new FormData();
154
413
  formData.append('documents', file);
155
- this.feedbackContainer.querySelector('.moj-multi-file-upload__list').append(item);
414
+ this.$feedbackContainer.querySelector('.moj-multi-file-upload__list').append($item);
156
415
  const xhr = new XMLHttpRequest();
157
416
  const onLoad = () => {
158
417
  if (xhr.status < 200 || xhr.status >= 300 || !('success' in xhr.response)) {
159
418
  return onError();
160
419
  }
161
- message.innerHTML = this.getSuccessHtml(xhr.response.success);
162
- this.status.textContent = xhr.response.success.messageText;
163
- actions.append(this.getDeleteButton(xhr.response.file));
164
- this.params.uploadFileExitHook(this, file, xhr, xhr.responseText);
420
+ $message.innerHTML = this.getSuccessHtml(xhr.response.success);
421
+ this.$status.textContent = xhr.response.success.messageText;
422
+ $actions.append(this.getDeleteButton(xhr.response.file));
423
+ this.config.hooks.exitHook(this, file, xhr, xhr.responseText);
165
424
  };
166
425
  const onError = () => {
167
426
  const error = new Error(xhr.response && 'error' in xhr.response ? xhr.response.error.message : xhr.statusText || 'Upload failed');
168
- message.innerHTML = this.getErrorHtml(error);
169
- this.status.textContent = error.message;
170
- this.params.uploadFileErrorHook(this, file, xhr, xhr.responseText, error);
427
+ $message.innerHTML = this.getErrorHtml(error);
428
+ this.$status.textContent = error.message;
429
+ this.config.hooks.errorHook(this, file, xhr, xhr.responseText, error);
171
430
  };
172
431
  xhr.addEventListener('load', onLoad);
173
432
  xhr.addEventListener('error', onError);
@@ -176,15 +435,19 @@ class MultiFileUpload {
176
435
  return;
177
436
  }
178
437
  const percentComplete = Math.round(event.loaded / event.total * 100);
179
- progress.textContent = ` ${percentComplete}%`;
438
+ $progress.textContent = ` ${percentComplete}%`;
180
439
  });
181
- xhr.open('POST', this.params.uploadUrl);
440
+ xhr.open('POST', this.config.uploadUrl);
182
441
  xhr.responseType = 'json';
183
442
  xhr.send(formData);
184
443
  }
444
+
445
+ /**
446
+ * @param {MouseEvent} event - Click event
447
+ */
185
448
  onFileDeleteClick(event) {
186
- const button = event.target;
187
- if (!button || !(button instanceof HTMLButtonElement) || !button.classList.contains('moj-multi-file-upload__delete')) {
449
+ const $button = event.target;
450
+ if (!$button || !($button instanceof HTMLButtonElement) || !$button.classList.contains('moj-multi-file-upload__delete')) {
188
451
  return;
189
452
  }
190
453
  event.preventDefault(); // if user refreshes page and then deletes
@@ -194,22 +457,177 @@ class MultiFileUpload {
194
457
  if (xhr.status < 200 || xhr.status >= 300) {
195
458
  return;
196
459
  }
197
- const rows = Array.from(this.feedbackContainer.querySelectorAll('.moj-multi-file-upload__row'));
198
- if (rows.length === 1) {
199
- this.feedbackContainer.classList.add('moj-hidden');
460
+ const $rows = Array.from(this.$feedbackContainer.querySelectorAll('.moj-multi-file-upload__row'));
461
+ if ($rows.length === 1) {
462
+ this.$feedbackContainer.classList.add('moj-hidden');
200
463
  }
201
- const row = rows.find(row => row.contains(button));
202
- if (row) row.remove();
203
- this.params.fileDeleteHook(this, undefined, xhr, xhr.responseText);
464
+ const $rowDelete = $rows.find($row => $row.contains($button));
465
+ if ($rowDelete) $rowDelete.remove();
466
+ this.config.hooks.deleteHook(this, undefined, xhr, xhr.responseText);
204
467
  });
205
- xhr.open('POST', this.params.deleteUrl);
468
+ xhr.open('POST', this.config.deleteUrl);
206
469
  xhr.setRequestHeader('Content-Type', 'application/json');
207
470
  xhr.responseType = 'json';
208
471
  xhr.send(JSON.stringify({
209
- [button.name]: button.value
472
+ [$button.name]: $button.value
210
473
  }));
211
474
  }
475
+ static isSupported() {
476
+ return this.isDragAndDropSupported() && this.isFormDataSupported() && this.isFileApiSupported();
477
+ }
478
+ static isDragAndDropSupported() {
479
+ const div = document.createElement('div');
480
+ return typeof div.ondrop !== 'undefined';
481
+ }
482
+ static isFormDataSupported() {
483
+ return typeof FormData === 'function';
484
+ }
485
+ static isFileApiSupported() {
486
+ const input = document.createElement('input');
487
+ input.type = 'file';
488
+ return typeof input.files !== 'undefined';
489
+ }
490
+
491
+ /**
492
+ * Name for the component used when initialising using data-module attributes.
493
+ */
212
494
  }
213
495
 
496
+ /**
497
+ * Multi file upload config
498
+ *
499
+ * @typedef {object} MultiFileUploadConfig
500
+ * @property {string} [uploadUrl] - File upload URL
501
+ * @property {string} [deleteUrl] - File delete URL
502
+ * @property {string} [uploadStatusText] - Upload status text
503
+ * @property {string} [dropzoneHintText] - Dropzone hint text
504
+ * @property {string} [dropzoneButtonText] - Dropzone button text
505
+ * @property {object} [feedbackContainer] - Feedback container config
506
+ * @property {string} [feedbackContainer.selector] - Selector for feedback container
507
+ * @property {Element | null} [feedbackContainer.element] - HTML element for feedback container
508
+ * @property {MultiFileUploadHooks} [hooks] - Upload hooks
509
+ */
510
+
511
+ /**
512
+ * Multi file upload hooks
513
+ *
514
+ * @typedef {object} MultiFileUploadHooks
515
+ * @property {OnUploadFileEntryHook} [entryHook] - File upload entry hook
516
+ * @property {OnUploadFileExitHook} [exitHook] - File upload exit hook
517
+ * @property {OnUploadFileErrorHook} [errorHook] - File upload error hook
518
+ * @property {OnUploadFileDeleteHook} [deleteHook] - File delete hook
519
+ */
520
+
521
+ /**
522
+ * Upload hook: File entry
523
+ *
524
+ * @callback OnUploadFileEntryHook
525
+ * @param {InstanceType<typeof MultiFileUpload>} upload - Multi file upload
526
+ * @param {File} file - File upload
527
+ */
528
+
529
+ /**
530
+ * Upload hook: File exit
531
+ *
532
+ * @callback OnUploadFileExitHook
533
+ * @param {InstanceType<typeof MultiFileUpload>} upload - Multi file upload
534
+ * @param {File} file - File upload
535
+ * @param {XMLHttpRequest} xhr - XMLHttpRequest
536
+ * @param {string} textStatus - Text status
537
+ */
538
+
539
+ /**
540
+ * Upload hook: File error
541
+ *
542
+ * @callback OnUploadFileErrorHook
543
+ * @param {InstanceType<typeof MultiFileUpload>} upload - Multi file upload
544
+ * @param {File} file - File upload
545
+ * @param {XMLHttpRequest} xhr - XMLHttpRequest
546
+ * @param {string} textStatus - Text status
547
+ * @param {Error} errorThrown - Error thrown
548
+ */
549
+
550
+ /**
551
+ * Upload hook: File delete
552
+ *
553
+ * @callback OnUploadFileDeleteHook
554
+ * @param {InstanceType<typeof MultiFileUpload>} upload - Multi file upload
555
+ * @param {File} [file] - File upload
556
+ * @param {XMLHttpRequest} xhr - XMLHttpRequest
557
+ * @param {string} textStatus - Text status
558
+ */
559
+
560
+ /**
561
+ * @typedef {object} UploadResponseSuccess
562
+ * @property {{ messageText: string, messageHtml: string }} success - Response success
563
+ * @property {UploadResponseFile} file - Response file
564
+ */
565
+
566
+ /**
567
+ * @typedef {object} UploadResponseError
568
+ * @property {{ message: string }} error - Response error
569
+ * @property {UploadResponseFile} file - Response file
570
+ */
571
+
572
+ /**
573
+ * @typedef {object} UploadResponseFile
574
+ * @property {string} filename - File name
575
+ * @property {string} originalname - Original file name
576
+ */
577
+
578
+ /**
579
+ * @import { Schema } from 'govuk-frontend/dist/govuk/common/configuration.mjs'
580
+ */
581
+ MultiFileUpload.moduleName = 'moj-multi-file-upload';
582
+ /**
583
+ * Multi file upload default config
584
+ *
585
+ * @type {MultiFileUploadConfig}
586
+ */
587
+ MultiFileUpload.defaults = Object.freeze({
588
+ uploadStatusText: 'Uploading files, please wait',
589
+ dropzoneHintText: 'Drag and drop files here or',
590
+ dropzoneButtonText: 'Choose files',
591
+ feedbackContainer: {
592
+ selector: '.moj-multi-file__uploaded-files'
593
+ },
594
+ hooks: {
595
+ entryHook: () => {},
596
+ exitHook: () => {},
597
+ errorHook: () => {},
598
+ deleteHook: () => {}
599
+ }
600
+ });
601
+ /**
602
+ * Multi file upload config schema
603
+ *
604
+ * @satisfies {Schema<MultiFileUploadConfig>}
605
+ */
606
+ MultiFileUpload.schema = Object.freeze(/** @type {const} */{
607
+ properties: {
608
+ uploadUrl: {
609
+ type: 'string'
610
+ },
611
+ deleteUrl: {
612
+ type: 'string'
613
+ },
614
+ uploadStatusText: {
615
+ type: 'string'
616
+ },
617
+ dropzoneHintText: {
618
+ type: 'string'
619
+ },
620
+ dropzoneButtonText: {
621
+ type: 'string'
622
+ },
623
+ feedbackContainer: {
624
+ type: 'object'
625
+ },
626
+ hooks: {
627
+ type: 'object'
628
+ }
629
+ }
630
+ });
631
+
214
632
  export { MultiFileUpload };
215
633
  //# sourceMappingURL=multi-file-upload.bundle.mjs.map