@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.
- package/moj/all.bundle.js +1549 -1062
- package/moj/all.bundle.js.map +1 -1
- package/moj/all.bundle.mjs +1845 -1054
- package/moj/all.bundle.mjs.map +1 -1
- package/moj/all.mjs +7 -90
- package/moj/all.mjs.map +1 -1
- package/moj/all.scss +1 -0
- package/moj/all.scss.map +1 -1
- package/moj/common/index.mjs +57 -0
- package/moj/common/index.mjs.map +1 -0
- package/moj/common/moj-frontend-version.mjs +14 -0
- package/moj/common/moj-frontend-version.mjs.map +1 -0
- package/moj/components/add-another/add-another.bundle.js +105 -76
- package/moj/components/add-another/add-another.bundle.js.map +1 -1
- package/moj/components/add-another/add-another.bundle.mjs +222 -71
- package/moj/components/add-another/add-another.bundle.mjs.map +1 -1
- package/moj/components/add-another/add-another.mjs +103 -72
- package/moj/components/add-another/add-another.mjs.map +1 -1
- package/moj/components/alert/alert.bundle.js +115 -191
- package/moj/components/alert/alert.bundle.js.map +1 -1
- package/moj/components/alert/alert.bundle.mjs +354 -186
- package/moj/components/alert/alert.bundle.mjs.map +1 -1
- package/moj/components/alert/alert.mjs +55 -140
- package/moj/components/alert/alert.mjs.map +1 -1
- package/moj/components/button-menu/README.md +3 -1
- package/moj/components/button-menu/button-menu.bundle.js +91 -120
- package/moj/components/button-menu/button-menu.bundle.js.map +1 -1
- package/moj/components/button-menu/button-menu.bundle.mjs +329 -114
- package/moj/components/button-menu/button-menu.bundle.mjs.map +1 -1
- package/moj/components/button-menu/button-menu.mjs +89 -116
- package/moj/components/button-menu/button-menu.mjs.map +1 -1
- package/moj/components/date-picker/date-picker.bundle.js +174 -154
- package/moj/components/date-picker/date-picker.bundle.js.map +1 -1
- package/moj/components/date-picker/date-picker.bundle.mjs +411 -147
- package/moj/components/date-picker/date-picker.bundle.mjs.map +1 -1
- package/moj/components/date-picker/date-picker.mjs +172 -150
- package/moj/components/date-picker/date-picker.mjs.map +1 -1
- package/moj/components/filter/template.njk +1 -1
- package/moj/components/filter-toggle-button/filter-toggle-button.bundle.js +133 -44
- package/moj/components/filter-toggle-button/filter-toggle-button.bundle.js.map +1 -1
- package/moj/components/filter-toggle-button/filter-toggle-button.bundle.mjs +374 -41
- package/moj/components/filter-toggle-button/filter-toggle-button.bundle.mjs.map +1 -1
- package/moj/components/filter-toggle-button/filter-toggle-button.mjs +131 -40
- package/moj/components/filter-toggle-button/filter-toggle-button.mjs.map +1 -1
- package/moj/components/form-validator/form-validator.bundle.js +159 -69
- package/moj/components/form-validator/form-validator.bundle.js.map +1 -1
- package/moj/components/form-validator/form-validator.bundle.mjs +399 -65
- package/moj/components/form-validator/form-validator.bundle.mjs.map +1 -1
- package/moj/components/form-validator/form-validator.mjs +134 -54
- package/moj/components/form-validator/form-validator.mjs.map +1 -1
- package/moj/components/multi-file-upload/multi-file-upload.bundle.js +291 -117
- package/moj/components/multi-file-upload/multi-file-upload.bundle.js.map +1 -1
- package/moj/components/multi-file-upload/multi-file-upload.bundle.mjs +527 -109
- package/moj/components/multi-file-upload/multi-file-upload.bundle.mjs.map +1 -1
- package/moj/components/multi-file-upload/multi-file-upload.mjs +288 -101
- package/moj/components/multi-file-upload/multi-file-upload.mjs.map +1 -1
- package/moj/components/multi-file-upload/template.njk +1 -1
- package/moj/components/multi-select/multi-select.bundle.js +106 -41
- package/moj/components/multi-select/multi-select.bundle.js.map +1 -1
- package/moj/components/multi-select/multi-select.bundle.mjs +346 -37
- package/moj/components/multi-select/multi-select.bundle.mjs.map +1 -1
- package/moj/components/multi-select/multi-select.mjs +104 -37
- package/moj/components/multi-select/multi-select.mjs.map +1 -1
- package/moj/components/password-reveal/_password-reveal.scss +3 -1
- package/moj/components/password-reveal/_password-reveal.scss.map +1 -1
- package/moj/components/password-reveal/password-reveal.bundle.js +32 -29
- package/moj/components/password-reveal/password-reveal.bundle.js.map +1 -1
- package/moj/components/password-reveal/password-reveal.bundle.mjs +149 -24
- package/moj/components/password-reveal/password-reveal.bundle.mjs.map +1 -1
- package/moj/components/password-reveal/password-reveal.mjs +30 -25
- package/moj/components/password-reveal/password-reveal.mjs.map +1 -1
- package/moj/components/rich-text-editor/README.md +4 -3
- package/moj/components/rich-text-editor/rich-text-editor.bundle.js +127 -62
- package/moj/components/rich-text-editor/rich-text-editor.bundle.js.map +1 -1
- package/moj/components/rich-text-editor/rich-text-editor.bundle.mjs +367 -58
- package/moj/components/rich-text-editor/rich-text-editor.bundle.mjs.map +1 -1
- package/moj/components/rich-text-editor/rich-text-editor.mjs +125 -58
- package/moj/components/rich-text-editor/rich-text-editor.mjs.map +1 -1
- package/moj/components/search-toggle/search-toggle.bundle.js +94 -26
- package/moj/components/search-toggle/search-toggle.bundle.js.map +1 -1
- package/moj/components/search-toggle/search-toggle.bundle.mjs +334 -22
- package/moj/components/search-toggle/search-toggle.bundle.mjs.map +1 -1
- package/moj/components/search-toggle/search-toggle.mjs +92 -22
- package/moj/components/search-toggle/search-toggle.mjs.map +1 -1
- package/moj/components/sortable-table/sortable-table.bundle.js +151 -83
- package/moj/components/sortable-table/sortable-table.bundle.js.map +1 -1
- package/moj/components/sortable-table/sortable-table.bundle.mjs +390 -78
- package/moj/components/sortable-table/sortable-table.bundle.mjs.map +1 -1
- package/moj/components/sortable-table/sortable-table.mjs +149 -79
- package/moj/components/sortable-table/sortable-table.mjs.map +1 -1
- package/moj/core/_all.scss +3 -0
- package/moj/core/_all.scss.map +1 -0
- package/moj/core/_moj-frontend-properties.scss +7 -0
- package/moj/core/_moj-frontend-properties.scss.map +1 -0
- package/moj/filters/prototype-kit-13-filters.js +4 -3
- package/moj/helpers.bundle.js +22 -77
- package/moj/helpers.bundle.js.map +1 -1
- package/moj/helpers.bundle.mjs +23 -74
- package/moj/helpers.bundle.mjs.map +1 -1
- package/moj/helpers.mjs +23 -74
- package/moj/helpers.mjs.map +1 -1
- package/moj/moj-frontend.min.css +1 -1
- package/moj/moj-frontend.min.css.map +1 -1
- package/moj/moj-frontend.min.js +1 -1
- package/moj/moj-frontend.min.js.map +1 -1
- package/package.json +1 -1
- package/moj/version.bundle.js +0 -12
- package/moj/version.bundle.js.map +0 -1
- package/moj/version.bundle.mjs +0 -4
- package/moj/version.bundle.mjs.map +0 -1
- package/moj/version.mjs +0 -4
- package/moj/version.mjs.map +0 -1
|
@@ -1,173 +1,432 @@
|
|
|
1
|
-
function
|
|
2
|
-
|
|
3
|
-
return typeof div.ondrop !== 'undefined';
|
|
1
|
+
function isInitialised($root, moduleName) {
|
|
2
|
+
return $root instanceof HTMLElement && $root.hasAttribute(`data-${moduleName}-init`);
|
|
4
3
|
}
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* @augments {ConfigurableComponent<MultiFileUploadConfig>}
|
|
250
|
+
*/
|
|
251
|
+
class MultiFileUpload extends ConfigurableComponent {
|
|
17
252
|
/**
|
|
18
|
-
* @param {
|
|
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(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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.
|
|
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
|
|
49
|
-
this
|
|
50
|
-
this
|
|
51
|
-
this
|
|
52
|
-
this
|
|
53
|
-
this
|
|
54
|
-
this
|
|
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
|
|
59
|
-
label.classList.add('govuk-button', 'govuk-button--secondary');
|
|
60
|
-
label.textContent = this.
|
|
61
|
-
const hint = document.createElement('p');
|
|
62
|
-
hint.classList.add('govuk-body');
|
|
63
|
-
hint.textContent = this.
|
|
64
|
-
this
|
|
65
|
-
this
|
|
66
|
-
this
|
|
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
|
|
70
|
-
this.
|
|
71
|
-
this
|
|
72
|
-
this
|
|
73
|
-
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
|
|
77
|
-
this
|
|
78
|
-
this
|
|
79
|
-
this
|
|
80
|
-
this
|
|
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
|
|
315
|
+
this.$dropzone.classList.add('moj-multi-file-upload--dragover');
|
|
85
316
|
}
|
|
86
317
|
onDragLeave() {
|
|
87
|
-
this
|
|
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
|
|
92
|
-
this
|
|
93
|
-
this
|
|
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
|
|
103
|
-
this
|
|
104
|
-
this.uploadFiles(this
|
|
105
|
-
const fileInput = this
|
|
106
|
-
if (
|
|
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
|
|
348
|
+
$fileInput.value = '';
|
|
349
|
+
this.$fileInput.replaceWith($fileInput);
|
|
111
350
|
this.setupFileInput();
|
|
112
|
-
this
|
|
351
|
+
this.$fileInput.focus();
|
|
113
352
|
}
|
|
114
353
|
onFileFocus() {
|
|
115
|
-
this
|
|
354
|
+
this.$label.classList.add('moj-multi-file-upload--focused');
|
|
116
355
|
}
|
|
117
356
|
onFileBlur() {
|
|
118
|
-
this
|
|
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.
|
|
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
|
|
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
|
|
163
|
-
actions.append(this.getDeleteButton(xhr.response.file));
|
|
164
|
-
this.
|
|
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
|
|
170
|
-
this.
|
|
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.
|
|
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 (
|
|
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
|
|
198
|
-
if (rows.length === 1) {
|
|
199
|
-
this
|
|
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
|
|
202
|
-
if (
|
|
203
|
-
this.
|
|
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.
|
|
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
|