@ministryofjustice/frontend 5.0.0 → 5.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/moj/all.bundle.js +1598 -1062
- package/moj/all.bundle.js.map +1 -1
- package/moj/all.bundle.mjs +1894 -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.scss +3 -42
- package/moj/components/sortable-table/_sortable-table.scss.map +1 -1
- package/moj/components/sortable-table/sortable-table.bundle.js +200 -83
- package/moj/components/sortable-table/sortable-table.bundle.js.map +1 -1
- package/moj/components/sortable-table/sortable-table.bundle.mjs +439 -78
- package/moj/components/sortable-table/sortable-table.bundle.mjs.map +1 -1
- package/moj/components/sortable-table/sortable-table.mjs +198 -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
package/moj/all.bundle.js
CHANGED
|
@@ -1,164 +1,259 @@
|
|
|
1
1
|
(function (global, factory) {
|
|
2
|
-
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
3
|
-
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
4
|
-
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.MOJFrontend = global.MOJFrontend || {}));
|
|
5
|
-
})(this, (function (exports) { 'use strict';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
2
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('govuk-frontend')) :
|
|
3
|
+
typeof define === 'function' && define.amd ? define(['exports', 'govuk-frontend'], factory) :
|
|
4
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.MOJFrontend = global.MOJFrontend || {}, global.GOVUKFrontend));
|
|
5
|
+
})(this, (function (exports, govukFrontend) { 'use strict';
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* This variable is automatically overwritten during builds and releases.
|
|
9
|
+
* It doesn't need to be updated manually.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* MoJ Frontend release version
|
|
14
|
+
*
|
|
15
|
+
* {@link https://github.com/ministryofjustice/moj-frontend/releases}
|
|
16
|
+
*/
|
|
17
|
+
const version = '5.1.1';
|
|
18
|
+
|
|
19
|
+
class AddAnother extends govukFrontend.Component {
|
|
20
|
+
/**
|
|
21
|
+
* @param {Element | null} $root - HTML element to use for add another
|
|
22
|
+
*/
|
|
23
|
+
constructor($root) {
|
|
24
|
+
super($root);
|
|
25
|
+
this.$root.addEventListener('click', this.onRemoveButtonClick.bind(this));
|
|
26
|
+
this.$root.addEventListener('click', this.onAddButtonClick.bind(this));
|
|
27
|
+
const $buttons = this.$root.querySelectorAll('.moj-add-another__add-button, moj-add-another__remove-button');
|
|
28
|
+
$buttons.forEach($button => {
|
|
29
|
+
if (!($button instanceof HTMLButtonElement)) {
|
|
19
30
|
return;
|
|
20
31
|
}
|
|
21
|
-
button.type = 'button';
|
|
32
|
+
$button.type = 'button';
|
|
22
33
|
});
|
|
23
34
|
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {MouseEvent} event - Click event
|
|
38
|
+
*/
|
|
24
39
|
onAddButtonClick(event) {
|
|
25
|
-
const button = event.target;
|
|
26
|
-
if (
|
|
40
|
+
const $button = event.target;
|
|
41
|
+
if (!$button || !($button instanceof HTMLButtonElement) || !$button.classList.contains('moj-add-another__add-button')) {
|
|
27
42
|
return;
|
|
28
43
|
}
|
|
29
|
-
const items = this.getItems();
|
|
30
|
-
const item = this.getNewItem();
|
|
31
|
-
if (
|
|
44
|
+
const $items = this.getItems();
|
|
45
|
+
const $item = this.getNewItem();
|
|
46
|
+
if (!$item || !($item instanceof HTMLElement)) {
|
|
32
47
|
return;
|
|
33
48
|
}
|
|
34
|
-
this.updateAttributes(item, items.length);
|
|
35
|
-
this.resetItem(item);
|
|
36
|
-
const firstItem = items[0];
|
|
37
|
-
if (!this.hasRemoveButton(firstItem)) {
|
|
38
|
-
this.createRemoveButton(firstItem);
|
|
49
|
+
this.updateAttributes($item, $items.length);
|
|
50
|
+
this.resetItem($item);
|
|
51
|
+
const $firstItem = $items[0];
|
|
52
|
+
if (!this.hasRemoveButton($firstItem)) {
|
|
53
|
+
this.createRemoveButton($firstItem);
|
|
39
54
|
}
|
|
40
|
-
items[items.length - 1].after(item);
|
|
41
|
-
const input = item.querySelector('input, textarea, select');
|
|
42
|
-
if (input && input instanceof HTMLInputElement) {
|
|
43
|
-
input.focus();
|
|
55
|
+
$items[$items.length - 1].after($item);
|
|
56
|
+
const $input = $item.querySelector('input, textarea, select');
|
|
57
|
+
if ($input && $input instanceof HTMLInputElement) {
|
|
58
|
+
$input.focus();
|
|
44
59
|
}
|
|
45
60
|
}
|
|
46
|
-
|
|
47
|
-
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {HTMLElement} $item - Add another item
|
|
64
|
+
*/
|
|
65
|
+
hasRemoveButton($item) {
|
|
66
|
+
return $item.querySelectorAll('.moj-add-another__remove-button').length;
|
|
48
67
|
}
|
|
49
68
|
getItems() {
|
|
50
|
-
if (!this
|
|
69
|
+
if (!this.$root) {
|
|
51
70
|
return [];
|
|
52
71
|
}
|
|
53
|
-
const items = Array.from(this.
|
|
54
|
-
return items.filter(item => item instanceof HTMLElement);
|
|
72
|
+
const $items = Array.from(this.$root.querySelectorAll('.moj-add-another__item'));
|
|
73
|
+
return $items.filter(item => item instanceof HTMLElement);
|
|
55
74
|
}
|
|
56
75
|
getNewItem() {
|
|
57
|
-
const items = this.getItems();
|
|
58
|
-
const item = items[0].cloneNode(true);
|
|
59
|
-
if (
|
|
76
|
+
const $items = this.getItems();
|
|
77
|
+
const $item = $items[0].cloneNode(true);
|
|
78
|
+
if (!$item || !($item instanceof HTMLElement)) {
|
|
60
79
|
return;
|
|
61
80
|
}
|
|
62
|
-
if (!this.hasRemoveButton(item)) {
|
|
63
|
-
this.createRemoveButton(item);
|
|
81
|
+
if (!this.hasRemoveButton($item)) {
|
|
82
|
+
this.createRemoveButton($item);
|
|
64
83
|
}
|
|
65
|
-
return item;
|
|
84
|
+
return $item;
|
|
66
85
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {HTMLElement} $item - Add another item
|
|
89
|
+
* @param {number} index - Add another item index
|
|
90
|
+
*/
|
|
91
|
+
updateAttributes($item, index) {
|
|
92
|
+
$item.querySelectorAll('[data-name]').forEach($input => {
|
|
93
|
+
if (!($input instanceof HTMLInputElement)) {
|
|
70
94
|
return;
|
|
71
95
|
}
|
|
72
|
-
const name =
|
|
73
|
-
const id =
|
|
74
|
-
const originalId =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const label =
|
|
78
|
-
if (label && label instanceof HTMLLabelElement) {
|
|
79
|
-
label.htmlFor =
|
|
96
|
+
const name = $input.getAttribute('data-name') || '';
|
|
97
|
+
const id = $input.getAttribute('data-id') || '';
|
|
98
|
+
const originalId = $input.id;
|
|
99
|
+
$input.name = name.replace(/%index%/, `${index}`);
|
|
100
|
+
$input.id = id.replace(/%index%/, `${index}`);
|
|
101
|
+
const $label = $input.parentElement.querySelector('label') || $input.closest('label') || $item.querySelector(`[for="${originalId}"]`);
|
|
102
|
+
if ($label && $label instanceof HTMLLabelElement) {
|
|
103
|
+
$label.htmlFor = $input.id;
|
|
80
104
|
}
|
|
81
105
|
});
|
|
82
106
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {HTMLElement} $item - Add another item
|
|
110
|
+
*/
|
|
111
|
+
createRemoveButton($item) {
|
|
112
|
+
const $button = document.createElement('button');
|
|
113
|
+
$button.type = 'button';
|
|
114
|
+
$button.classList.add('govuk-button', 'govuk-button--secondary', 'moj-add-another__remove-button');
|
|
115
|
+
$button.textContent = 'Remove';
|
|
116
|
+
$item.append($button);
|
|
89
117
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param {HTMLElement} $item - Add another item
|
|
121
|
+
*/
|
|
122
|
+
resetItem($item) {
|
|
123
|
+
$item.querySelectorAll('[data-name], [data-id]').forEach($input => {
|
|
124
|
+
if (!($input instanceof HTMLInputElement)) {
|
|
93
125
|
return;
|
|
94
126
|
}
|
|
95
|
-
if (
|
|
96
|
-
|
|
127
|
+
if ($input.type === 'checkbox' || $input.type === 'radio') {
|
|
128
|
+
$input.checked = false;
|
|
97
129
|
} else {
|
|
98
|
-
|
|
130
|
+
$input.value = '';
|
|
99
131
|
}
|
|
100
132
|
});
|
|
101
133
|
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {MouseEvent} event - Click event
|
|
137
|
+
*/
|
|
102
138
|
onRemoveButtonClick(event) {
|
|
103
|
-
const button = event.target;
|
|
104
|
-
if (
|
|
139
|
+
const $button = event.target;
|
|
140
|
+
if (!$button || !($button instanceof HTMLButtonElement) || !$button.classList.contains('moj-add-another__remove-button')) {
|
|
105
141
|
return;
|
|
106
142
|
}
|
|
107
|
-
button.closest('.moj-add-another__item').remove();
|
|
108
|
-
const items = this.getItems();
|
|
109
|
-
if (items.length === 1) {
|
|
110
|
-
items[0].querySelector('.moj-add-another__remove-button').remove();
|
|
143
|
+
$button.closest('.moj-add-another__item').remove();
|
|
144
|
+
const $items = this.getItems();
|
|
145
|
+
if ($items.length === 1) {
|
|
146
|
+
$items[0].querySelector('.moj-add-another__remove-button').remove();
|
|
111
147
|
}
|
|
112
|
-
items.forEach((
|
|
113
|
-
this.updateAttributes(
|
|
148
|
+
$items.forEach(($item, index) => {
|
|
149
|
+
this.updateAttributes($item, index);
|
|
114
150
|
});
|
|
115
151
|
this.focusHeading();
|
|
116
152
|
}
|
|
117
153
|
focusHeading() {
|
|
118
|
-
const heading = this.
|
|
119
|
-
if (heading && heading instanceof HTMLElement) {
|
|
120
|
-
heading.focus();
|
|
154
|
+
const $heading = this.$root.querySelector('.moj-add-another__heading');
|
|
155
|
+
if ($heading && $heading instanceof HTMLElement) {
|
|
156
|
+
$heading.focus();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Name for the component used when initialising using data-module attributes.
|
|
162
|
+
*/
|
|
163
|
+
}
|
|
164
|
+
AddAnother.moduleName = 'moj-add-another';
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* GOV.UK Frontend helpers
|
|
168
|
+
*
|
|
169
|
+
* @todo Import from GOV.UK Frontend
|
|
170
|
+
*/
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Move focus to element
|
|
174
|
+
*
|
|
175
|
+
* Sets tabindex to -1 to make the element programmatically focusable,
|
|
176
|
+
* but removes it on blur as the element doesn't need to be focused again.
|
|
177
|
+
*
|
|
178
|
+
* @template {HTMLElement} FocusElement
|
|
179
|
+
* @param {FocusElement} $element - HTML element
|
|
180
|
+
* @param {object} [options] - Handler options
|
|
181
|
+
* @param {function(this: FocusElement): void} [options.onBeforeFocus] - Callback before focus
|
|
182
|
+
* @param {function(this: FocusElement): void} [options.onBlur] - Callback on blur
|
|
183
|
+
*/
|
|
184
|
+
function setFocus($element, options = {}) {
|
|
185
|
+
var _options$onBeforeFocu;
|
|
186
|
+
const isFocusable = $element.getAttribute('tabindex');
|
|
187
|
+
if (!isFocusable) {
|
|
188
|
+
$element.setAttribute('tabindex', '-1');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Handle element focus
|
|
193
|
+
*/
|
|
194
|
+
function onFocus() {
|
|
195
|
+
$element.addEventListener('blur', onBlur, {
|
|
196
|
+
once: true
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Handle element blur
|
|
202
|
+
*/
|
|
203
|
+
function onBlur() {
|
|
204
|
+
var _options$onBlur;
|
|
205
|
+
(_options$onBlur = options.onBlur) == null || _options$onBlur.call($element);
|
|
206
|
+
if (!isFocusable) {
|
|
207
|
+
$element.removeAttribute('tabindex');
|
|
121
208
|
}
|
|
122
209
|
}
|
|
210
|
+
|
|
211
|
+
// Add listener to reset element on blur, after focus
|
|
212
|
+
$element.addEventListener('focus', onFocus, {
|
|
213
|
+
once: true
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Focus element
|
|
217
|
+
(_options$onBeforeFocu = options.onBeforeFocus) == null || _options$onBeforeFocu.call($element);
|
|
218
|
+
$element.focus();
|
|
123
219
|
}
|
|
124
220
|
|
|
125
|
-
|
|
221
|
+
/**
|
|
222
|
+
* @param {Element} $element - Element to remove attribute value from
|
|
223
|
+
* @param {string} attr - Attribute name
|
|
224
|
+
* @param {string} value - Attribute value
|
|
225
|
+
*/
|
|
226
|
+
function removeAttributeValue($element, attr, value) {
|
|
126
227
|
let re, m;
|
|
127
|
-
if (
|
|
128
|
-
if (
|
|
129
|
-
|
|
228
|
+
if ($element.getAttribute(attr)) {
|
|
229
|
+
if ($element.getAttribute(attr) === value) {
|
|
230
|
+
$element.removeAttribute(attr);
|
|
130
231
|
} else {
|
|
131
232
|
re = new RegExp(`(^|\\s)${value}(\\s|$)`);
|
|
132
|
-
m =
|
|
233
|
+
m = $element.getAttribute(attr).match(re);
|
|
133
234
|
if (m && m.length === 3) {
|
|
134
|
-
|
|
235
|
+
$element.setAttribute(attr, $element.getAttribute(attr).replace(re, m[1] && m[2] ? ' ' : ''));
|
|
135
236
|
}
|
|
136
237
|
}
|
|
137
238
|
}
|
|
138
239
|
}
|
|
139
|
-
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* @param {Element} $element - Element to add attribute value to
|
|
243
|
+
* @param {string} attr - Attribute name
|
|
244
|
+
* @param {string} value - Attribute value
|
|
245
|
+
*/
|
|
246
|
+
function addAttributeValue($element, attr, value) {
|
|
140
247
|
let re;
|
|
141
|
-
if (
|
|
142
|
-
|
|
248
|
+
if (!$element.getAttribute(attr)) {
|
|
249
|
+
$element.setAttribute(attr, value);
|
|
143
250
|
} else {
|
|
144
251
|
re = new RegExp(`(^|\\s)${value}(\\s|$)`);
|
|
145
|
-
if (!re.test(
|
|
146
|
-
|
|
252
|
+
if (!re.test($element.getAttribute(attr))) {
|
|
253
|
+
$element.setAttribute(attr, `${$element.getAttribute(attr)} ${value}`);
|
|
147
254
|
}
|
|
148
255
|
}
|
|
149
256
|
}
|
|
150
|
-
function dragAndDropSupported() {
|
|
151
|
-
const div = document.createElement('div');
|
|
152
|
-
return typeof div.ondrop !== 'undefined';
|
|
153
|
-
}
|
|
154
|
-
function formDataSupported() {
|
|
155
|
-
return typeof FormData === 'function';
|
|
156
|
-
}
|
|
157
|
-
function fileApiSupported() {
|
|
158
|
-
const input = document.createElement('input');
|
|
159
|
-
input.type = 'file';
|
|
160
|
-
return typeof input.files !== 'undefined';
|
|
161
|
-
}
|
|
162
257
|
|
|
163
258
|
/**
|
|
164
259
|
* Find an elements preceding sibling
|
|
@@ -219,89 +314,15 @@
|
|
|
219
314
|
}
|
|
220
315
|
|
|
221
316
|
/**
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
* Sets tabindex to -1 to make the element programmatically focusable,
|
|
225
|
-
* but removes it on blur as the element doesn't need to be focused again.
|
|
226
|
-
*
|
|
227
|
-
* @param {HTMLElement} $element - HTML element
|
|
228
|
-
* @param {object} [options] - Handler options
|
|
229
|
-
* @param {function(this: HTMLElement): void} [options.onBeforeFocus] - Callback before focus
|
|
230
|
-
* @param {function(this: HTMLElement): void} [options.onBlur] - Callback on blur
|
|
317
|
+
* @augments {ConfigurableComponent<AlertConfig>}
|
|
231
318
|
*/
|
|
232
|
-
|
|
233
|
-
const isFocusable = $element.getAttribute('tabindex');
|
|
234
|
-
if (!isFocusable) {
|
|
235
|
-
$element.setAttribute('tabindex', '-1');
|
|
236
|
-
}
|
|
237
|
-
|
|
319
|
+
class Alert extends govukFrontend.ConfigurableComponent {
|
|
238
320
|
/**
|
|
239
|
-
*
|
|
240
|
-
*/
|
|
241
|
-
function onFocus() {
|
|
242
|
-
$element.addEventListener('blur', onBlur, {
|
|
243
|
-
once: true
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Handle element blur
|
|
249
|
-
*/
|
|
250
|
-
function onBlur() {
|
|
251
|
-
if (options.onBlur) {
|
|
252
|
-
options.onBlur.call($element);
|
|
253
|
-
}
|
|
254
|
-
if (!isFocusable) {
|
|
255
|
-
$element.removeAttribute('tabindex');
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Add listener to reset element on blur, after focus
|
|
260
|
-
$element.addEventListener('focus', onFocus, {
|
|
261
|
-
once: true
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
// Focus element
|
|
265
|
-
if (options.onBeforeFocus) {
|
|
266
|
-
options.onBeforeFocus.call($element);
|
|
267
|
-
}
|
|
268
|
-
$element.focus();
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
class Alert {
|
|
272
|
-
/**
|
|
273
|
-
* @param {Element | null} $module - HTML element to use for alert
|
|
321
|
+
* @param {Element | null} $root - HTML element to use for alert
|
|
274
322
|
* @param {AlertConfig} [config] - Alert config
|
|
275
323
|
*/
|
|
276
|
-
constructor($
|
|
277
|
-
|
|
278
|
-
return this;
|
|
279
|
-
}
|
|
280
|
-
const schema = Object.freeze({
|
|
281
|
-
properties: {
|
|
282
|
-
dismissible: {
|
|
283
|
-
type: 'boolean'
|
|
284
|
-
},
|
|
285
|
-
dismissText: {
|
|
286
|
-
type: 'string'
|
|
287
|
-
},
|
|
288
|
-
disableAutoFocus: {
|
|
289
|
-
type: 'boolean'
|
|
290
|
-
},
|
|
291
|
-
focusOnDismissSelector: {
|
|
292
|
-
type: 'string'
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
const defaults = {
|
|
297
|
-
dismissible: false,
|
|
298
|
-
dismissText: 'Dismiss',
|
|
299
|
-
disableAutoFocus: false
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
// data attributes override JS config, which overrides defaults
|
|
303
|
-
this.config = this.mergeConfigs(defaults, config, this.parseDataset(schema, $module.dataset));
|
|
304
|
-
this.$module = $module;
|
|
324
|
+
constructor($root, config = {}) {
|
|
325
|
+
super($root, config);
|
|
305
326
|
|
|
306
327
|
/**
|
|
307
328
|
* Focus the alert
|
|
@@ -314,14 +335,14 @@
|
|
|
314
335
|
* do this based on user research findings, or to avoid a clash with another
|
|
315
336
|
* element which should be focused when the page loads.
|
|
316
337
|
*/
|
|
317
|
-
if (this.$
|
|
318
|
-
setFocus(this.$
|
|
338
|
+
if (this.$root.getAttribute('role') === 'alert' && !this.config.disableAutoFocus) {
|
|
339
|
+
setFocus(this.$root);
|
|
319
340
|
}
|
|
320
|
-
this.$dismissButton = this.$
|
|
341
|
+
this.$dismissButton = this.$root.querySelector('.moj-alert__dismiss');
|
|
321
342
|
if (this.config.dismissible && this.$dismissButton) {
|
|
322
343
|
this.$dismissButton.innerHTML = this.config.dismissText;
|
|
323
344
|
this.$dismissButton.removeAttribute('hidden');
|
|
324
|
-
this.$
|
|
345
|
+
this.$root.addEventListener('click', event => {
|
|
325
346
|
if (event.target instanceof Node && this.$dismissButton.contains(event.target)) {
|
|
326
347
|
this.dimiss();
|
|
327
348
|
}
|
|
@@ -342,7 +363,7 @@
|
|
|
342
363
|
|
|
343
364
|
// Is the next sibling another alert
|
|
344
365
|
if (!$elementToRecieveFocus) {
|
|
345
|
-
const $nextSibling = this.$
|
|
366
|
+
const $nextSibling = this.$root.nextElementSibling;
|
|
346
367
|
if ($nextSibling && $nextSibling.matches('.moj-alert')) {
|
|
347
368
|
$elementToRecieveFocus = $nextSibling;
|
|
348
369
|
}
|
|
@@ -350,13 +371,13 @@
|
|
|
350
371
|
|
|
351
372
|
// Else try to find any preceding sibling alert or heading
|
|
352
373
|
if (!$elementToRecieveFocus) {
|
|
353
|
-
$elementToRecieveFocus = getPreviousSibling(this.$
|
|
374
|
+
$elementToRecieveFocus = getPreviousSibling(this.$root, '.moj-alert, h1, h2, h3, h4, h5, h6');
|
|
354
375
|
}
|
|
355
376
|
|
|
356
377
|
// Else find the closest ancestor heading, or fallback to main, or last resort
|
|
357
378
|
// use the body element
|
|
358
379
|
if (!$elementToRecieveFocus) {
|
|
359
|
-
$elementToRecieveFocus = findNearestMatchingElement(this.$
|
|
380
|
+
$elementToRecieveFocus = findNearestMatchingElement(this.$root, 'h1, h2, h3, h4, h5, h6, main, body');
|
|
360
381
|
}
|
|
361
382
|
|
|
362
383
|
// If we have an element, place focus on it
|
|
@@ -365,111 +386,12 @@
|
|
|
365
386
|
}
|
|
366
387
|
|
|
367
388
|
// Remove the alert
|
|
368
|
-
this.$
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Normalise string
|
|
373
|
-
*
|
|
374
|
-
* 'If it looks like a duck, and it quacks like a duck…' 🦆
|
|
375
|
-
*
|
|
376
|
-
* If the passed value looks like a boolean or a number, convert it to a boolean
|
|
377
|
-
* or number.
|
|
378
|
-
*
|
|
379
|
-
* Designed to be used to convert config passed via data attributes (which are
|
|
380
|
-
* always strings) into something sensible.
|
|
381
|
-
*
|
|
382
|
-
* @internal
|
|
383
|
-
* @param {DOMStringMap[string]} value - The value to normalise
|
|
384
|
-
* @param {SchemaProperty} [property] - Component schema property
|
|
385
|
-
* @returns {string | boolean | number | undefined} Normalised data
|
|
386
|
-
*/
|
|
387
|
-
normaliseString(value, property) {
|
|
388
|
-
const trimmedValue = value ? value.trim() : '';
|
|
389
|
-
let output;
|
|
390
|
-
let outputType;
|
|
391
|
-
if (property && property.type) {
|
|
392
|
-
outputType = property.type;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// No schema type set? Determine automatically
|
|
396
|
-
if (!outputType) {
|
|
397
|
-
if (['true', 'false'].includes(trimmedValue)) {
|
|
398
|
-
outputType = 'boolean';
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Empty / whitespace-only strings are considered finite so we need to check
|
|
402
|
-
// the length of the trimmed string as well
|
|
403
|
-
if (trimmedValue.length > 0 && Number.isFinite(Number(trimmedValue))) {
|
|
404
|
-
outputType = 'number';
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
switch (outputType) {
|
|
408
|
-
case 'boolean':
|
|
409
|
-
output = trimmedValue === 'true';
|
|
410
|
-
break;
|
|
411
|
-
case 'number':
|
|
412
|
-
output = Number(trimmedValue);
|
|
413
|
-
break;
|
|
414
|
-
default:
|
|
415
|
-
output = value;
|
|
416
|
-
}
|
|
417
|
-
return output;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* Parse dataset
|
|
422
|
-
*
|
|
423
|
-
* Loop over an object and normalise each value using {@link normaliseString},
|
|
424
|
-
* optionally expanding nested `i18n.field`
|
|
425
|
-
*
|
|
426
|
-
* @param {Schema} schema - component schema
|
|
427
|
-
* @param {DOMStringMap} dataset - HTML element dataset
|
|
428
|
-
* @returns {object} Normalised dataset
|
|
429
|
-
*/
|
|
430
|
-
parseDataset(schema, dataset) {
|
|
431
|
-
const parsed = {};
|
|
432
|
-
for (const [field, property] of Object.entries(schema.properties)) {
|
|
433
|
-
if (field in dataset) {
|
|
434
|
-
if (dataset[field]) {
|
|
435
|
-
parsed[field] = this.normaliseString(dataset[field], property);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
return parsed;
|
|
389
|
+
this.$root.remove();
|
|
440
390
|
}
|
|
441
391
|
|
|
442
392
|
/**
|
|
443
|
-
*
|
|
444
|
-
|
|
445
|
-
* Takes any number of objects and combines them together, with
|
|
446
|
-
* greatest priority on the LAST item passed in.
|
|
447
|
-
*
|
|
448
|
-
* @param {...{ [key: string]: unknown }} configObjects - Config objects to merge
|
|
449
|
-
* @returns {{ [key: string]: unknown }} A merged config object
|
|
450
|
-
*/
|
|
451
|
-
mergeConfigs(...configObjects) {
|
|
452
|
-
const formattedConfigObject = {};
|
|
453
|
-
|
|
454
|
-
// Loop through each of the passed objects
|
|
455
|
-
for (const configObject of configObjects) {
|
|
456
|
-
for (const key of Object.keys(configObject)) {
|
|
457
|
-
const option = formattedConfigObject[key];
|
|
458
|
-
const override = configObject[key];
|
|
459
|
-
|
|
460
|
-
// Push their keys one-by-one into formattedConfigObject. Any duplicate
|
|
461
|
-
// keys with object values will be merged, otherwise the new value will
|
|
462
|
-
// override the existing value.
|
|
463
|
-
if (typeof option === 'object' && typeof override === 'object') {
|
|
464
|
-
// @ts-expect-error Index signature for type 'string' is missing
|
|
465
|
-
formattedConfigObject[key] = this.mergeConfigs(option, override);
|
|
466
|
-
} else {
|
|
467
|
-
formattedConfigObject[key] = override;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
return formattedConfigObject;
|
|
472
|
-
}
|
|
393
|
+
* Name for the component used when initialising using data-module attributes.
|
|
394
|
+
*/
|
|
473
395
|
}
|
|
474
396
|
|
|
475
397
|
/**
|
|
@@ -480,71 +402,87 @@
|
|
|
480
402
|
* @property {string} [focusOnDismissSelector] - CSS Selector for element to be focused on dismiss
|
|
481
403
|
*/
|
|
482
404
|
|
|
483
|
-
|
|
405
|
+
/**
|
|
406
|
+
* @import { Schema } from 'govuk-frontend/dist/govuk/common/configuration.mjs'
|
|
407
|
+
*/
|
|
408
|
+
Alert.moduleName = 'moj-alert';
|
|
409
|
+
/**
|
|
410
|
+
* Alert default config
|
|
411
|
+
*
|
|
412
|
+
* @type {AlertConfig}
|
|
413
|
+
*/
|
|
414
|
+
Alert.defaults = Object.freeze({
|
|
415
|
+
dismissible: false,
|
|
416
|
+
dismissText: 'Dismiss',
|
|
417
|
+
disableAutoFocus: false
|
|
418
|
+
});
|
|
419
|
+
/**
|
|
420
|
+
* Alert config schema
|
|
421
|
+
*
|
|
422
|
+
* @satisfies {Schema<AlertConfig>}
|
|
423
|
+
*/
|
|
424
|
+
Alert.schema = Object.freeze(/** @type {const} */{
|
|
425
|
+
properties: {
|
|
426
|
+
dismissible: {
|
|
427
|
+
type: 'boolean'
|
|
428
|
+
},
|
|
429
|
+
dismissText: {
|
|
430
|
+
type: 'string'
|
|
431
|
+
},
|
|
432
|
+
disableAutoFocus: {
|
|
433
|
+
type: 'boolean'
|
|
434
|
+
},
|
|
435
|
+
focusOnDismissSelector: {
|
|
436
|
+
type: 'string'
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* @augments {ConfigurableComponent<ButtonMenuConfig>}
|
|
443
|
+
*/
|
|
444
|
+
class ButtonMenu extends govukFrontend.ConfigurableComponent {
|
|
484
445
|
/**
|
|
485
|
-
* @param {Element | null} $
|
|
446
|
+
* @param {Element | null} $root - HTML element to use for button menu
|
|
486
447
|
* @param {ButtonMenuConfig} [config] - Button menu config
|
|
487
448
|
*/
|
|
488
|
-
constructor($
|
|
489
|
-
|
|
490
|
-
return this;
|
|
491
|
-
}
|
|
492
|
-
const schema = Object.freeze({
|
|
493
|
-
properties: {
|
|
494
|
-
buttonText: {
|
|
495
|
-
type: 'string'
|
|
496
|
-
},
|
|
497
|
-
buttonClasses: {
|
|
498
|
-
type: 'string'
|
|
499
|
-
},
|
|
500
|
-
alignMenu: {
|
|
501
|
-
type: 'string'
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
});
|
|
505
|
-
const defaults = {
|
|
506
|
-
buttonText: 'Actions',
|
|
507
|
-
alignMenu: 'left',
|
|
508
|
-
buttonClasses: ''
|
|
509
|
-
};
|
|
510
|
-
// data attributes override JS config, which overrides defaults
|
|
511
|
-
this.config = this.mergeConfigs(defaults, config, this.parseDataset(schema, $module.dataset));
|
|
512
|
-
this.$module = $module;
|
|
449
|
+
constructor($root, config = {}) {
|
|
450
|
+
super($root, config);
|
|
513
451
|
|
|
514
452
|
// If only one button is provided, don't initiate a menu and toggle button
|
|
515
453
|
// if classes have been provided for the toggleButton, apply them to the single item
|
|
516
|
-
if (this.$
|
|
517
|
-
const button = this.$
|
|
518
|
-
button.classList.forEach(className => {
|
|
454
|
+
if (this.$root.children.length === 1) {
|
|
455
|
+
const $button = this.$root.children[0];
|
|
456
|
+
$button.classList.forEach(className => {
|
|
519
457
|
if (className.startsWith('govuk-button-')) {
|
|
520
|
-
button.classList.remove(className);
|
|
458
|
+
$button.classList.remove(className);
|
|
521
459
|
}
|
|
522
|
-
button.classList.remove('moj-button-menu__item');
|
|
523
|
-
button.classList.add('moj-button-menu__single-button');
|
|
460
|
+
$button.classList.remove('moj-button-menu__item');
|
|
461
|
+
$button.classList.add('moj-button-menu__single-button');
|
|
524
462
|
});
|
|
525
463
|
if (this.config.buttonClasses) {
|
|
526
|
-
button.classList.add(...this.config.buttonClasses.split(' '));
|
|
464
|
+
$button.classList.add(...this.config.buttonClasses.split(' '));
|
|
527
465
|
}
|
|
528
466
|
}
|
|
529
|
-
// Otherwise
|
|
530
|
-
if (this.$
|
|
467
|
+
// Otherwise initialise a button menu
|
|
468
|
+
if (this.$root.children.length > 1) {
|
|
531
469
|
this.initMenu();
|
|
532
470
|
}
|
|
533
471
|
}
|
|
534
472
|
initMenu() {
|
|
535
473
|
this.$menu = this.createMenu();
|
|
536
|
-
this.$
|
|
474
|
+
this.$root.insertAdjacentHTML('afterbegin', this.toggleTemplate());
|
|
537
475
|
this.setupMenuItems();
|
|
538
|
-
this.$menuToggle = this.$
|
|
539
|
-
this
|
|
476
|
+
this.$menuToggle = this.$root.querySelector(':scope > button');
|
|
477
|
+
this.$items = this.$menu.querySelectorAll('a, button');
|
|
540
478
|
this.$menuToggle.addEventListener('click', event => {
|
|
541
479
|
this.toggleMenu(event);
|
|
542
480
|
});
|
|
543
|
-
this.$
|
|
481
|
+
this.$root.addEventListener('keydown', event => {
|
|
544
482
|
this.handleKeyDown(event);
|
|
545
483
|
});
|
|
546
484
|
document.addEventListener('click', event => {
|
|
547
|
-
if (!this.$
|
|
485
|
+
if (event.target instanceof Node && !this.$root.contains(event.target)) {
|
|
548
486
|
this.closeMenu(false);
|
|
549
487
|
}
|
|
550
488
|
});
|
|
@@ -557,30 +495,30 @@
|
|
|
557
495
|
if (this.config.alignMenu === 'right') {
|
|
558
496
|
$menu.classList.add('moj-button-menu__wrapper--right');
|
|
559
497
|
}
|
|
560
|
-
this.$
|
|
561
|
-
while (this.$
|
|
562
|
-
$menu.appendChild(this.$
|
|
498
|
+
this.$root.appendChild($menu);
|
|
499
|
+
while (this.$root.firstChild !== $menu) {
|
|
500
|
+
$menu.appendChild(this.$root.firstChild);
|
|
563
501
|
}
|
|
564
502
|
return $menu;
|
|
565
503
|
}
|
|
566
504
|
setupMenuItems() {
|
|
567
|
-
Array.from(this.$menu.children).forEach(
|
|
505
|
+
Array.from(this.$menu.children).forEach($menuItem => {
|
|
568
506
|
// wrap item in li tag
|
|
569
|
-
const listItem = document.createElement('li');
|
|
570
|
-
this.$menu.insertBefore(listItem,
|
|
571
|
-
listItem.appendChild(
|
|
572
|
-
|
|
573
|
-
if (
|
|
574
|
-
|
|
507
|
+
const $listItem = document.createElement('li');
|
|
508
|
+
this.$menu.insertBefore($listItem, $menuItem);
|
|
509
|
+
$listItem.appendChild($menuItem);
|
|
510
|
+
$menuItem.setAttribute('tabindex', '-1');
|
|
511
|
+
if ($menuItem.tagName === 'BUTTON') {
|
|
512
|
+
$menuItem.setAttribute('type', 'button');
|
|
575
513
|
}
|
|
576
|
-
|
|
514
|
+
$menuItem.classList.forEach(className => {
|
|
577
515
|
if (className.startsWith('govuk-button')) {
|
|
578
|
-
|
|
516
|
+
$menuItem.classList.remove(className);
|
|
579
517
|
}
|
|
580
518
|
});
|
|
581
519
|
|
|
582
520
|
// add a slight delay after click before closing the menu, makes it *feel* better
|
|
583
|
-
|
|
521
|
+
$menuItem.addEventListener('click', () => {
|
|
584
522
|
setTimeout(() => {
|
|
585
523
|
this.closeMenu(false);
|
|
586
524
|
}, 50);
|
|
@@ -605,6 +543,10 @@
|
|
|
605
543
|
isOpen() {
|
|
606
544
|
return this.$menuToggle.getAttribute('aria-expanded') === 'true';
|
|
607
545
|
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* @param {MouseEvent} event - Click event
|
|
549
|
+
*/
|
|
608
550
|
toggleMenu(event) {
|
|
609
551
|
event.preventDefault();
|
|
610
552
|
|
|
@@ -650,18 +592,22 @@
|
|
|
650
592
|
* @param {number} index - the index of the item to focus
|
|
651
593
|
*/
|
|
652
594
|
focusItem(index) {
|
|
653
|
-
if (index >= this
|
|
654
|
-
if (index < 0) index = this
|
|
655
|
-
const menuItem = this
|
|
656
|
-
if (menuItem) {
|
|
657
|
-
menuItem.focus();
|
|
595
|
+
if (index >= this.$items.length) index = 0;
|
|
596
|
+
if (index < 0) index = this.$items.length - 1;
|
|
597
|
+
const $menuItem = this.$items.item(index);
|
|
598
|
+
if ($menuItem) {
|
|
599
|
+
$menuItem.focus();
|
|
658
600
|
}
|
|
659
601
|
}
|
|
660
602
|
currentFocusIndex() {
|
|
661
|
-
const activeElement = document.activeElement;
|
|
662
|
-
const menuItems = Array.from(this
|
|
663
|
-
return menuItems.indexOf(activeElement);
|
|
603
|
+
const $activeElement = document.activeElement;
|
|
604
|
+
const $menuItems = Array.from(this.$items);
|
|
605
|
+
return ($activeElement instanceof HTMLAnchorElement || $activeElement instanceof HTMLButtonElement) && $menuItems.indexOf($activeElement);
|
|
664
606
|
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* @param {KeyboardEvent} event - Keydown event
|
|
610
|
+
*/
|
|
665
611
|
handleKeyDown(event) {
|
|
666
612
|
if (event.target === this.$menuToggle) {
|
|
667
613
|
switch (event.key) {
|
|
@@ -671,11 +617,11 @@
|
|
|
671
617
|
break;
|
|
672
618
|
case 'ArrowUp':
|
|
673
619
|
event.preventDefault();
|
|
674
|
-
this.openMenu(this
|
|
620
|
+
this.openMenu(this.$items.length - 1);
|
|
675
621
|
break;
|
|
676
622
|
}
|
|
677
623
|
}
|
|
678
|
-
if (this.$menu.contains(event.target) && this.isOpen()) {
|
|
624
|
+
if (event.target instanceof Node && this.$menu.contains(event.target) && this.isOpen()) {
|
|
679
625
|
switch (event.key) {
|
|
680
626
|
case 'ArrowDown':
|
|
681
627
|
event.preventDefault();
|
|
@@ -695,7 +641,7 @@
|
|
|
695
641
|
break;
|
|
696
642
|
case 'End':
|
|
697
643
|
event.preventDefault();
|
|
698
|
-
this.focusItem(this
|
|
644
|
+
this.focusItem(this.$items.length - 1);
|
|
699
645
|
break;
|
|
700
646
|
}
|
|
701
647
|
}
|
|
@@ -708,58 +654,8 @@
|
|
|
708
654
|
}
|
|
709
655
|
|
|
710
656
|
/**
|
|
711
|
-
*
|
|
712
|
-
|
|
713
|
-
* Loop over an object and normalise each value using {@link normaliseString},
|
|
714
|
-
* optionally expanding nested `i18n.field`
|
|
715
|
-
*
|
|
716
|
-
* @param {Schema} schema - component schema
|
|
717
|
-
* @param {DOMStringMap} dataset - HTML element dataset
|
|
718
|
-
* @returns {object} Normalised dataset
|
|
719
|
-
*/
|
|
720
|
-
parseDataset(schema, dataset) {
|
|
721
|
-
const parsed = {};
|
|
722
|
-
for (const [field,,] of Object.entries(schema.properties)) {
|
|
723
|
-
if (field in dataset) {
|
|
724
|
-
if (dataset[field]) {
|
|
725
|
-
parsed[field] = dataset[field];
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
return parsed;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
/**
|
|
733
|
-
* Config merging function
|
|
734
|
-
*
|
|
735
|
-
* Takes any number of objects and combines them together, with
|
|
736
|
-
* greatest priority on the LAST item passed in.
|
|
737
|
-
*
|
|
738
|
-
* @param {...{ [key: string]: unknown }} configObjects - Config objects to merge
|
|
739
|
-
* @returns {{ [key: string]: unknown }} A merged config object
|
|
740
|
-
*/
|
|
741
|
-
mergeConfigs(...configObjects) {
|
|
742
|
-
const formattedConfigObject = {};
|
|
743
|
-
|
|
744
|
-
// Loop through each of the passed objects
|
|
745
|
-
for (const configObject of configObjects) {
|
|
746
|
-
for (const key of Object.keys(configObject)) {
|
|
747
|
-
const option = formattedConfigObject[key];
|
|
748
|
-
const override = configObject[key];
|
|
749
|
-
|
|
750
|
-
// Push their keys one-by-one into formattedConfigObject. Any duplicate
|
|
751
|
-
// keys with object values will be merged, otherwise the new value will
|
|
752
|
-
// override the existing value.
|
|
753
|
-
if (typeof option === 'object' && typeof override === 'object') {
|
|
754
|
-
// @ts-expect-error Index signature for type 'string' is missing
|
|
755
|
-
formattedConfigObject[key] = this.mergeConfigs(option, override);
|
|
756
|
-
} else {
|
|
757
|
-
formattedConfigObject[key] = override;
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
return formattedConfigObject;
|
|
762
|
-
}
|
|
657
|
+
* Name for the component used when initialising using data-module attributes.
|
|
658
|
+
*/
|
|
763
659
|
}
|
|
764
660
|
|
|
765
661
|
/**
|
|
@@ -769,69 +665,68 @@
|
|
|
769
665
|
* @property {string} [buttonClasses='govuk-button--secondary'] - css classes applied to the toggle button
|
|
770
666
|
*/
|
|
771
667
|
|
|
772
|
-
|
|
668
|
+
/**
|
|
669
|
+
* @import { Schema } from 'govuk-frontend/dist/govuk/common/configuration.mjs'
|
|
670
|
+
*/
|
|
671
|
+
ButtonMenu.moduleName = 'moj-button-menu';
|
|
672
|
+
/**
|
|
673
|
+
* Button menu config
|
|
674
|
+
*
|
|
675
|
+
* @type {ButtonMenuConfig}
|
|
676
|
+
*/
|
|
677
|
+
ButtonMenu.defaults = Object.freeze({
|
|
678
|
+
buttonText: 'Actions',
|
|
679
|
+
alignMenu: 'left',
|
|
680
|
+
buttonClasses: ''
|
|
681
|
+
});
|
|
682
|
+
/**
|
|
683
|
+
* Button menu config schema
|
|
684
|
+
*
|
|
685
|
+
* @type {Schema<ButtonMenuConfig>}
|
|
686
|
+
*/
|
|
687
|
+
ButtonMenu.schema = Object.freeze(/** @type {const} */{
|
|
688
|
+
properties: {
|
|
689
|
+
buttonText: {
|
|
690
|
+
type: 'string'
|
|
691
|
+
},
|
|
692
|
+
buttonClasses: {
|
|
693
|
+
type: 'string'
|
|
694
|
+
},
|
|
695
|
+
alignMenu: {
|
|
696
|
+
type: 'string'
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* @augments {ConfigurableComponent<DatePickerConfig>}
|
|
703
|
+
*/
|
|
704
|
+
class DatePicker extends govukFrontend.ConfigurableComponent {
|
|
773
705
|
/**
|
|
774
|
-
* @param {Element | null} $
|
|
706
|
+
* @param {Element | null} $root - HTML element to use for date picker
|
|
775
707
|
* @param {DatePickerConfig} [config] - Date picker config
|
|
776
708
|
*/
|
|
777
|
-
constructor($
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
const $input = $module.querySelector('.moj-js-datepicker-input');
|
|
782
|
-
|
|
783
|
-
// Check that required elements are present
|
|
709
|
+
constructor($root, config = {}) {
|
|
710
|
+
var _this$config$input$el;
|
|
711
|
+
super($root, config);
|
|
712
|
+
const $input = (_this$config$input$el = this.config.input.element) != null ? _this$config$input$el : this.$root.querySelector(this.config.input.selector);
|
|
784
713
|
if (!$input || !($input instanceof HTMLInputElement)) {
|
|
785
714
|
return this;
|
|
786
715
|
}
|
|
787
|
-
this.$module = $module;
|
|
788
716
|
this.$input = $input;
|
|
789
|
-
const schema = Object.freeze({
|
|
790
|
-
properties: {
|
|
791
|
-
excludedDates: {
|
|
792
|
-
type: 'string'
|
|
793
|
-
},
|
|
794
|
-
excludedDays: {
|
|
795
|
-
type: 'string'
|
|
796
|
-
},
|
|
797
|
-
leadingZeros: {
|
|
798
|
-
type: 'string'
|
|
799
|
-
},
|
|
800
|
-
maxDate: {
|
|
801
|
-
type: 'string'
|
|
802
|
-
},
|
|
803
|
-
minDate: {
|
|
804
|
-
type: 'string'
|
|
805
|
-
},
|
|
806
|
-
weekStartDay: {
|
|
807
|
-
type: 'string'
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
});
|
|
811
|
-
const defaults = {
|
|
812
|
-
leadingZeros: false,
|
|
813
|
-
weekStartDay: 'monday'
|
|
814
|
-
};
|
|
815
|
-
|
|
816
|
-
// data attributes override JS config, which overrides defaults
|
|
817
|
-
this.config = this.mergeConfigs(defaults, config, this.parseDataset(schema, $module.dataset));
|
|
818
717
|
this.dayLabels = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
|
819
718
|
this.monthLabels = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
820
719
|
this.currentDate = new Date();
|
|
821
720
|
this.currentDate.setHours(0, 0, 0, 0);
|
|
822
|
-
this.calendarDays = [];
|
|
823
|
-
this.excludedDates = [];
|
|
824
|
-
this.excludedDays = [];
|
|
721
|
+
this.calendarDays = /** @type {DSCalendarDay[]} */[];
|
|
722
|
+
this.excludedDates = /** @type {Date[]} */[];
|
|
723
|
+
this.excludedDays = /** @type {number[]} */[];
|
|
825
724
|
this.buttonClass = 'moj-datepicker__button';
|
|
826
725
|
this.selectedDayButtonClass = 'moj-datepicker__button--selected';
|
|
827
726
|
this.currentDayButtonClass = 'moj-datepicker__button--current';
|
|
828
727
|
this.todayButtonClass = 'moj-datepicker__button--today';
|
|
829
|
-
if (this.$module.dataset.initialized) {
|
|
830
|
-
return this;
|
|
831
|
-
}
|
|
832
728
|
this.setOptions();
|
|
833
729
|
this.initControls();
|
|
834
|
-
this.$module.setAttribute('data-initialized', 'true');
|
|
835
730
|
}
|
|
836
731
|
initControls() {
|
|
837
732
|
this.id = `datepicker-${this.$input.id}`;
|
|
@@ -846,15 +741,23 @@
|
|
|
846
741
|
$inputWrapper.appendChild(this.$input);
|
|
847
742
|
$inputWrapper.insertAdjacentHTML('beforeend', this.toggleTemplate());
|
|
848
743
|
$componentWrapper.insertAdjacentElement('beforeend', this.$dialog);
|
|
849
|
-
this.$calendarButton =
|
|
850
|
-
this.$
|
|
744
|
+
this.$calendarButton = /** @type {HTMLButtonElement} */
|
|
745
|
+
this.$root.querySelector('.moj-js-datepicker-toggle');
|
|
746
|
+
this.$dialogTitle = /** @type {HTMLHeadingElement} */
|
|
747
|
+
this.$dialog.querySelector('.moj-js-datepicker-month-year');
|
|
851
748
|
this.createCalendar();
|
|
852
|
-
this.$prevMonthButton =
|
|
853
|
-
this.$
|
|
854
|
-
this.$
|
|
855
|
-
this.$
|
|
856
|
-
this.$
|
|
857
|
-
this.$
|
|
749
|
+
this.$prevMonthButton = /** @type {HTMLButtonElement} */
|
|
750
|
+
this.$dialog.querySelector('.moj-js-datepicker-prev-month');
|
|
751
|
+
this.$prevYearButton = /** @type {HTMLButtonElement} */
|
|
752
|
+
this.$dialog.querySelector('.moj-js-datepicker-prev-year');
|
|
753
|
+
this.$nextMonthButton = /** @type {HTMLButtonElement} */
|
|
754
|
+
this.$dialog.querySelector('.moj-js-datepicker-next-month');
|
|
755
|
+
this.$nextYearButton = /** @type {HTMLButtonElement} */
|
|
756
|
+
this.$dialog.querySelector('.moj-js-datepicker-next-year');
|
|
757
|
+
this.$cancelButton = /** @type {HTMLButtonElement} */
|
|
758
|
+
this.$dialog.querySelector('.moj-js-datepicker-cancel');
|
|
759
|
+
this.$okButton = /** @type {HTMLButtonElement} */
|
|
760
|
+
this.$dialog.querySelector('.moj-js-datepicker-ok');
|
|
858
761
|
|
|
859
762
|
// add event listeners
|
|
860
763
|
this.$prevMonthButton.addEventListener('click', event => this.focusPreviousMonth(event, false));
|
|
@@ -868,9 +771,9 @@
|
|
|
868
771
|
this.$okButton.addEventListener('click', () => {
|
|
869
772
|
this.selectDate(this.currentDate);
|
|
870
773
|
});
|
|
871
|
-
const dialogButtons = this.$dialog.querySelectorAll('button:not([disabled="true"])');
|
|
872
|
-
this.$firstButtonInDialog = dialogButtons[0];
|
|
873
|
-
this.$lastButtonInDialog = dialogButtons[dialogButtons.length - 1];
|
|
774
|
+
const $dialogButtons = this.$dialog.querySelectorAll('button:not([disabled="true"])');
|
|
775
|
+
this.$firstButtonInDialog = $dialogButtons[0];
|
|
776
|
+
this.$lastButtonInDialog = $dialogButtons[$dialogButtons.length - 1];
|
|
874
777
|
this.$firstButtonInDialog.addEventListener('keydown', event => this.firstButtonKeydown(event));
|
|
875
778
|
this.$lastButtonInDialog.addEventListener('keydown', event => this.lastButtonKeydown(event));
|
|
876
779
|
this.$calendarButton.addEventListener('click', event => this.toggleDialog(event));
|
|
@@ -1016,7 +919,6 @@
|
|
|
1016
919
|
this.setMinAndMaxDatesOnCalendar();
|
|
1017
920
|
this.setExcludedDates();
|
|
1018
921
|
this.setExcludedDays();
|
|
1019
|
-
this.setLeadingZeros();
|
|
1020
922
|
this.setWeekStartDay();
|
|
1021
923
|
}
|
|
1022
924
|
setMinAndMaxDatesOnCalendar() {
|
|
@@ -1041,10 +943,10 @@
|
|
|
1041
943
|
}
|
|
1042
944
|
}
|
|
1043
945
|
|
|
1044
|
-
|
|
946
|
+
/**
|
|
1045
947
|
* Parses a daterange string into an array of dates
|
|
1046
|
-
*
|
|
1047
|
-
* @
|
|
948
|
+
*
|
|
949
|
+
* @param {string} datestring - A daterange string in the format "dd/mm/yyyy-dd/mm/yyyy"
|
|
1048
950
|
*/
|
|
1049
951
|
parseDateRangeString(datestring) {
|
|
1050
952
|
const dates = [];
|
|
@@ -1071,17 +973,6 @@
|
|
|
1071
973
|
this.excludedDays = this.config.excludedDays.replace(/\s+/, ' ').toLowerCase().split(' ').map(item => weekDays.indexOf(item)).filter(item => item !== -1);
|
|
1072
974
|
}
|
|
1073
975
|
}
|
|
1074
|
-
setLeadingZeros() {
|
|
1075
|
-
if (typeof this.config.leadingZeros !== 'boolean') {
|
|
1076
|
-
if (this.config.leadingZeros.toLowerCase() === 'true') {
|
|
1077
|
-
this.config.leadingZeros = true;
|
|
1078
|
-
return;
|
|
1079
|
-
}
|
|
1080
|
-
if (this.config.leadingZeros.toLowerCase() === 'false') {
|
|
1081
|
-
this.config.leadingZeros = false;
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
976
|
setWeekStartDay() {
|
|
1086
977
|
const weekStartDayParam = this.config.weekStartDay;
|
|
1087
978
|
if (weekStartDayParam && weekStartDayParam.toLowerCase() === 'sunday') {
|
|
@@ -1094,7 +985,7 @@
|
|
|
1094
985
|
}
|
|
1095
986
|
|
|
1096
987
|
/**
|
|
1097
|
-
* Determine if a date is
|
|
988
|
+
* Determine if a date is selectable
|
|
1098
989
|
*
|
|
1099
990
|
* @param {Date} date - the date to check
|
|
1100
991
|
* @returns {boolean}
|
|
@@ -1160,24 +1051,36 @@
|
|
|
1160
1051
|
/**
|
|
1161
1052
|
* Get a human readable date in the format Monday 2 March 2024
|
|
1162
1053
|
*
|
|
1163
|
-
* @param {Date} date -
|
|
1054
|
+
* @param {Date} date - Date to format
|
|
1164
1055
|
* @returns {string}
|
|
1165
1056
|
*/
|
|
1166
1057
|
formattedDateHuman(date) {
|
|
1167
1058
|
return `${this.dayLabels[(date.getDay() + 6) % 7]} ${date.getDate()} ${this.monthLabels[date.getMonth()]} ${date.getFullYear()}`;
|
|
1168
1059
|
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* @param {MouseEvent} event - Click event
|
|
1063
|
+
*/
|
|
1169
1064
|
backgroundClick(event) {
|
|
1170
1065
|
if (this.isOpen() && event.target instanceof Node && !this.$dialog.contains(event.target) && !this.$input.contains(event.target) && !this.$calendarButton.contains(event.target)) {
|
|
1171
1066
|
event.preventDefault();
|
|
1172
1067
|
this.closeDialog();
|
|
1173
1068
|
}
|
|
1174
1069
|
}
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* @param {KeyboardEvent} event - Keydown event
|
|
1073
|
+
*/
|
|
1175
1074
|
firstButtonKeydown(event) {
|
|
1176
1075
|
if (event.key === 'Tab' && event.shiftKey) {
|
|
1177
1076
|
this.$lastButtonInDialog.focus();
|
|
1178
1077
|
event.preventDefault();
|
|
1179
1078
|
}
|
|
1180
1079
|
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* @param {KeyboardEvent} event - Keydown event
|
|
1083
|
+
*/
|
|
1181
1084
|
lastButtonKeydown(event) {
|
|
1182
1085
|
if (event.key === 'Tab' && !event.shiftKey) {
|
|
1183
1086
|
this.$firstButtonInDialog.focus();
|
|
@@ -1207,49 +1110,57 @@
|
|
|
1207
1110
|
thisDay.setDate(thisDay.getDate() + 1);
|
|
1208
1111
|
}
|
|
1209
1112
|
}
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* @param {boolean} [focus] - Focus the day button
|
|
1116
|
+
*/
|
|
1210
1117
|
setCurrentDate(focus = true) {
|
|
1211
1118
|
const {
|
|
1212
1119
|
currentDate
|
|
1213
1120
|
} = this;
|
|
1214
1121
|
this.calendarDays.forEach(calendarDay => {
|
|
1215
|
-
calendarDay
|
|
1216
|
-
calendarDay
|
|
1217
|
-
calendarDay
|
|
1218
|
-
calendarDay
|
|
1122
|
+
calendarDay.$button.classList.add('moj-datepicker__button');
|
|
1123
|
+
calendarDay.$button.classList.add('moj-datepicker__calendar-day');
|
|
1124
|
+
calendarDay.$button.setAttribute('tabindex', '-1');
|
|
1125
|
+
calendarDay.$button.classList.remove(this.selectedDayButtonClass);
|
|
1219
1126
|
const calendarDayDate = calendarDay.date;
|
|
1220
1127
|
calendarDayDate.setHours(0, 0, 0, 0);
|
|
1221
1128
|
const today = new Date();
|
|
1222
1129
|
today.setHours(0, 0, 0, 0);
|
|
1223
1130
|
if (calendarDayDate.getTime() === currentDate.getTime() /* && !calendarDay.button.disabled */) {
|
|
1224
1131
|
if (focus) {
|
|
1225
|
-
calendarDay
|
|
1226
|
-
calendarDay
|
|
1227
|
-
calendarDay
|
|
1132
|
+
calendarDay.$button.setAttribute('tabindex', '0');
|
|
1133
|
+
calendarDay.$button.focus();
|
|
1134
|
+
calendarDay.$button.classList.add(this.selectedDayButtonClass);
|
|
1228
1135
|
}
|
|
1229
1136
|
}
|
|
1230
1137
|
if (this.inputDate && calendarDayDate.getTime() === this.inputDate.getTime()) {
|
|
1231
|
-
calendarDay
|
|
1232
|
-
calendarDay
|
|
1138
|
+
calendarDay.$button.classList.add(this.currentDayButtonClass);
|
|
1139
|
+
calendarDay.$button.setAttribute('aria-current', 'date');
|
|
1233
1140
|
} else {
|
|
1234
|
-
calendarDay
|
|
1235
|
-
calendarDay
|
|
1141
|
+
calendarDay.$button.classList.remove(this.currentDayButtonClass);
|
|
1142
|
+
calendarDay.$button.removeAttribute('aria-current');
|
|
1236
1143
|
}
|
|
1237
1144
|
if (calendarDayDate.getTime() === today.getTime()) {
|
|
1238
|
-
calendarDay
|
|
1145
|
+
calendarDay.$button.classList.add(this.todayButtonClass);
|
|
1239
1146
|
} else {
|
|
1240
|
-
calendarDay
|
|
1147
|
+
calendarDay.$button.classList.remove(this.todayButtonClass);
|
|
1241
1148
|
}
|
|
1242
1149
|
});
|
|
1243
1150
|
|
|
1244
1151
|
// if no date is tab-able, make the first non-disabled date tab-able
|
|
1245
1152
|
if (!focus) {
|
|
1246
1153
|
const enabledDays = this.calendarDays.filter(calendarDay => {
|
|
1247
|
-
return window.getComputedStyle(calendarDay
|
|
1154
|
+
return window.getComputedStyle(calendarDay.$button).display === 'block' && !calendarDay.$button.disabled;
|
|
1248
1155
|
});
|
|
1249
|
-
enabledDays[0]
|
|
1156
|
+
enabledDays[0].$button.setAttribute('tabindex', '0');
|
|
1250
1157
|
this.currentDate = enabledDays[0].date;
|
|
1251
1158
|
}
|
|
1252
1159
|
}
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* @param {Date} date - Date to select
|
|
1163
|
+
*/
|
|
1253
1164
|
selectDate(date) {
|
|
1254
1165
|
if (this.isExcludedDate(date)) {
|
|
1255
1166
|
return;
|
|
@@ -1266,6 +1177,10 @@
|
|
|
1266
1177
|
isOpen() {
|
|
1267
1178
|
return this.$dialog.classList.contains('moj-datepicker__dialog--open');
|
|
1268
1179
|
}
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* @param {MouseEvent} event - Click event
|
|
1183
|
+
*/
|
|
1269
1184
|
toggleDialog(event) {
|
|
1270
1185
|
event.preventDefault();
|
|
1271
1186
|
if (this.isOpen()) {
|
|
@@ -1300,6 +1215,11 @@
|
|
|
1300
1215
|
this.$calendarButton.setAttribute('aria-expanded', 'false');
|
|
1301
1216
|
this.$calendarButton.focus();
|
|
1302
1217
|
}
|
|
1218
|
+
|
|
1219
|
+
/**
|
|
1220
|
+
* @param {Date} date - Date to go to
|
|
1221
|
+
* @param {boolean} [focus] - Focus the day button
|
|
1222
|
+
*/
|
|
1303
1223
|
goToDate(date, focus) {
|
|
1304
1224
|
const current = this.currentDate;
|
|
1305
1225
|
this.currentDate = date;
|
|
@@ -1351,27 +1271,47 @@
|
|
|
1351
1271
|
this.goToDate(date);
|
|
1352
1272
|
}
|
|
1353
1273
|
|
|
1354
|
-
|
|
1274
|
+
/**
|
|
1275
|
+
* Month navigation
|
|
1276
|
+
*
|
|
1277
|
+
* @param {KeyboardEvent | MouseEvent} event - Key press or click event
|
|
1278
|
+
* @param {boolean} [focus] - Focus the day button
|
|
1279
|
+
*/
|
|
1355
1280
|
focusNextMonth(event, focus = true) {
|
|
1356
1281
|
event.preventDefault();
|
|
1357
1282
|
const date = new Date(this.currentDate);
|
|
1358
1283
|
date.setMonth(date.getMonth() + 1, 1);
|
|
1359
1284
|
this.goToDate(date, focus);
|
|
1360
1285
|
}
|
|
1361
|
-
|
|
1362
|
-
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* @param {KeyboardEvent | MouseEvent} event - Key press or click event
|
|
1289
|
+
* @param {boolean} [focus] - Focus the day button
|
|
1290
|
+
*/
|
|
1291
|
+
focusPreviousMonth(event, focus = true) {
|
|
1292
|
+
event.preventDefault();
|
|
1363
1293
|
const date = new Date(this.currentDate);
|
|
1364
1294
|
date.setMonth(date.getMonth() - 1, 1);
|
|
1365
1295
|
this.goToDate(date, focus);
|
|
1366
1296
|
}
|
|
1367
1297
|
|
|
1368
|
-
|
|
1298
|
+
/**
|
|
1299
|
+
* Year navigation
|
|
1300
|
+
*
|
|
1301
|
+
* @param {KeyboardEvent | MouseEvent} event - Key press or click event
|
|
1302
|
+
* @param {boolean} [focus] - Focus the day button
|
|
1303
|
+
*/
|
|
1369
1304
|
focusNextYear(event, focus = true) {
|
|
1370
1305
|
event.preventDefault();
|
|
1371
1306
|
const date = new Date(this.currentDate);
|
|
1372
1307
|
date.setFullYear(date.getFullYear() + 1, date.getMonth(), 1);
|
|
1373
1308
|
this.goToDate(date, focus);
|
|
1374
1309
|
}
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* @param {KeyboardEvent | MouseEvent} event - Key press or click event
|
|
1313
|
+
* @param {boolean} [focus] - Focus the day button
|
|
1314
|
+
*/
|
|
1375
1315
|
focusPreviousYear(event, focus = true) {
|
|
1376
1316
|
event.preventDefault();
|
|
1377
1317
|
const date = new Date(this.currentDate);
|
|
@@ -1380,72 +1320,70 @@
|
|
|
1380
1320
|
}
|
|
1381
1321
|
|
|
1382
1322
|
/**
|
|
1383
|
-
*
|
|
1384
|
-
|
|
1385
|
-
* @param {Schema} schema - Component class
|
|
1386
|
-
* @param {DOMStringMap} dataset - HTML element dataset
|
|
1387
|
-
* @returns {object} Normalised dataset
|
|
1388
|
-
*/
|
|
1389
|
-
parseDataset(schema, dataset) {
|
|
1390
|
-
const parsed = {};
|
|
1391
|
-
for (const [field,,] of Object.entries(schema.properties)) {
|
|
1392
|
-
if (field in dataset) {
|
|
1393
|
-
parsed[field] = dataset[field];
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
return parsed;
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
/**
|
|
1400
|
-
* Config merging function
|
|
1401
|
-
*
|
|
1402
|
-
* Takes any number of objects and combines them together, with
|
|
1403
|
-
* greatest priority on the LAST item passed in.
|
|
1404
|
-
*
|
|
1405
|
-
* @param {...{ [key: string]: unknown }} configObjects - Config objects to merge
|
|
1406
|
-
* @returns {{ [key: string]: unknown }} A merged config object
|
|
1407
|
-
*/
|
|
1408
|
-
mergeConfigs(...configObjects) {
|
|
1409
|
-
const formattedConfigObject = {};
|
|
1410
|
-
|
|
1411
|
-
// Loop through each of the passed objects
|
|
1412
|
-
for (const configObject of configObjects) {
|
|
1413
|
-
for (const key of Object.keys(configObject)) {
|
|
1414
|
-
const option = formattedConfigObject[key];
|
|
1415
|
-
const override = configObject[key];
|
|
1416
|
-
|
|
1417
|
-
// Push their keys one-by-one into formattedConfigObject. Any duplicate
|
|
1418
|
-
// keys with object values will be merged, otherwise the new value will
|
|
1419
|
-
// override the existing value.
|
|
1420
|
-
if (typeof option === 'object' && typeof override === 'object') {
|
|
1421
|
-
// @ts-expect-error Index signature for type 'string' is missing
|
|
1422
|
-
formattedConfigObject[key] = this.mergeConfigs(option, override);
|
|
1423
|
-
} else {
|
|
1424
|
-
formattedConfigObject[key] = override;
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
return formattedConfigObject;
|
|
1429
|
-
}
|
|
1323
|
+
* Name for the component used when initialising using data-module attributes.
|
|
1324
|
+
*/
|
|
1430
1325
|
}
|
|
1326
|
+
DatePicker.moduleName = 'moj-date-picker';
|
|
1327
|
+
/**
|
|
1328
|
+
* Date picker default config
|
|
1329
|
+
*
|
|
1330
|
+
* @type {DatePickerConfig}
|
|
1331
|
+
*/
|
|
1332
|
+
DatePicker.defaults = Object.freeze({
|
|
1333
|
+
leadingZeros: false,
|
|
1334
|
+
weekStartDay: 'monday',
|
|
1335
|
+
input: {
|
|
1336
|
+
selector: '.moj-js-datepicker-input'
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
/**
|
|
1340
|
+
* Date picker config schema
|
|
1341
|
+
*
|
|
1342
|
+
* @satisfies {Schema<DatePickerConfig>}
|
|
1343
|
+
*/
|
|
1344
|
+
DatePicker.schema = Object.freeze(/** @type {const} */{
|
|
1345
|
+
properties: {
|
|
1346
|
+
excludedDates: {
|
|
1347
|
+
type: 'string'
|
|
1348
|
+
},
|
|
1349
|
+
excludedDays: {
|
|
1350
|
+
type: 'string'
|
|
1351
|
+
},
|
|
1352
|
+
leadingZeros: {
|
|
1353
|
+
type: 'boolean'
|
|
1354
|
+
},
|
|
1355
|
+
maxDate: {
|
|
1356
|
+
type: 'string'
|
|
1357
|
+
},
|
|
1358
|
+
minDate: {
|
|
1359
|
+
type: 'string'
|
|
1360
|
+
},
|
|
1361
|
+
weekStartDay: {
|
|
1362
|
+
type: 'string'
|
|
1363
|
+
},
|
|
1364
|
+
input: {
|
|
1365
|
+
type: 'object'
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1431
1369
|
class DSCalendarDay {
|
|
1432
1370
|
/**
|
|
1433
1371
|
*
|
|
1434
|
-
* @param {
|
|
1372
|
+
* @param {HTMLButtonElement} $button
|
|
1435
1373
|
* @param {number} index
|
|
1436
1374
|
* @param {number} row
|
|
1437
1375
|
* @param {number} column
|
|
1438
1376
|
* @param {DatePicker} picker
|
|
1439
1377
|
*/
|
|
1440
|
-
constructor(button, index, row, column, picker) {
|
|
1378
|
+
constructor($button, index, row, column, picker) {
|
|
1441
1379
|
this.index = index;
|
|
1442
1380
|
this.row = row;
|
|
1443
1381
|
this.column = column;
|
|
1444
|
-
this
|
|
1382
|
+
this.$button = $button;
|
|
1445
1383
|
this.picker = picker;
|
|
1446
1384
|
this.date = new Date();
|
|
1447
|
-
this
|
|
1448
|
-
this
|
|
1385
|
+
this.$button.addEventListener('keydown', this.keyPress.bind(this));
|
|
1386
|
+
this.$button.addEventListener('click', this.click.bind(this));
|
|
1449
1387
|
}
|
|
1450
1388
|
|
|
1451
1389
|
/**
|
|
@@ -1457,26 +1395,34 @@
|
|
|
1457
1395
|
const label = day.getDate();
|
|
1458
1396
|
let accessibleLabel = this.picker.formattedDateHuman(day);
|
|
1459
1397
|
if (disabled) {
|
|
1460
|
-
this
|
|
1398
|
+
this.$button.setAttribute('aria-disabled', 'true');
|
|
1461
1399
|
accessibleLabel = `Excluded date, ${accessibleLabel}`;
|
|
1462
1400
|
} else {
|
|
1463
|
-
this
|
|
1401
|
+
this.$button.removeAttribute('aria-disabled');
|
|
1464
1402
|
}
|
|
1465
1403
|
if (hidden) {
|
|
1466
|
-
this
|
|
1404
|
+
this.$button.style.display = 'none';
|
|
1467
1405
|
} else {
|
|
1468
|
-
this
|
|
1406
|
+
this.$button.style.display = 'block';
|
|
1469
1407
|
}
|
|
1470
|
-
this
|
|
1471
|
-
this
|
|
1408
|
+
this.$button.setAttribute('data-testid', this.picker.formattedDateFromDate(day));
|
|
1409
|
+
this.$button.innerHTML = `<span class="govuk-visually-hidden">${accessibleLabel}</span><span aria-hidden="true">${label}</span>`;
|
|
1472
1410
|
this.date = new Date(day);
|
|
1473
1411
|
}
|
|
1412
|
+
|
|
1413
|
+
/**
|
|
1414
|
+
* @param {MouseEvent} event - Click event
|
|
1415
|
+
*/
|
|
1474
1416
|
click(event) {
|
|
1475
1417
|
this.picker.goToDate(this.date);
|
|
1476
1418
|
this.picker.selectDate(this.date);
|
|
1477
1419
|
event.stopPropagation();
|
|
1478
1420
|
event.preventDefault();
|
|
1479
1421
|
}
|
|
1422
|
+
|
|
1423
|
+
/**
|
|
1424
|
+
* @param {KeyboardEvent} event - Keydown event
|
|
1425
|
+
*/
|
|
1480
1426
|
keyPress(event) {
|
|
1481
1427
|
let calendarNavKey = true;
|
|
1482
1428
|
switch (event.key) {
|
|
@@ -1533,45 +1479,61 @@
|
|
|
1533
1479
|
* @typedef {object} DatePickerConfig
|
|
1534
1480
|
* @property {string} [excludedDates] - Dates that cannot be selected
|
|
1535
1481
|
* @property {string} [excludedDays] - Days that cannot be selected
|
|
1536
|
-
* @property {boolean} [
|
|
1482
|
+
* @property {boolean} [leadingZeros] - Whether to add leading zeroes when populating the field
|
|
1537
1483
|
* @property {string} [minDate] - The earliest available date
|
|
1538
1484
|
* @property {string} [maxDate] - The latest available date
|
|
1539
1485
|
* @property {string} [weekStartDay] - First day of the week in calendar view
|
|
1486
|
+
* @property {object} [input] - Input config
|
|
1487
|
+
* @property {string} [input.selector] - Selector for the input element
|
|
1488
|
+
* @property {Element | null} [input.element] - HTML element for the input
|
|
1540
1489
|
*/
|
|
1541
1490
|
|
|
1542
1491
|
/**
|
|
1543
|
-
* @import { Schema } from '
|
|
1492
|
+
* @import { Schema } from 'govuk-frontend/dist/govuk/common/configuration.mjs'
|
|
1544
1493
|
*/
|
|
1545
1494
|
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1495
|
+
/**
|
|
1496
|
+
* @augments {ConfigurableComponent<FilterToggleButtonConfig>}
|
|
1497
|
+
*/
|
|
1498
|
+
class FilterToggleButton extends govukFrontend.ConfigurableComponent {
|
|
1499
|
+
/**
|
|
1500
|
+
* @param {Element | null} $root - HTML element to use for filter toggle button
|
|
1501
|
+
* @param {FilterToggleButtonConfig} [config] - Filter toggle button config
|
|
1502
|
+
*/
|
|
1503
|
+
constructor($root, config = {}) {
|
|
1504
|
+
var _this$config$toggleBu, _this$config$closeBut;
|
|
1505
|
+
super($root, config);
|
|
1506
|
+
const $toggleButtonContainer = (_this$config$toggleBu = this.config.toggleButtonContainer.element) != null ? _this$config$toggleBu : document.querySelector(this.config.toggleButtonContainer.selector);
|
|
1507
|
+
const $closeButtonContainer = (_this$config$closeBut = this.config.closeButtonContainer.element) != null ? _this$config$closeBut : this.$root.querySelector(this.config.closeButtonContainer.selector);
|
|
1508
|
+
if (!($toggleButtonContainer instanceof HTMLElement && $closeButtonContainer instanceof HTMLElement)) {
|
|
1509
|
+
return this;
|
|
1510
|
+
}
|
|
1511
|
+
this.$toggleButtonContainer = $toggleButtonContainer;
|
|
1512
|
+
this.$closeButtonContainer = $closeButtonContainer;
|
|
1551
1513
|
this.createToggleButton();
|
|
1552
1514
|
this.setupResponsiveChecks();
|
|
1553
|
-
this.
|
|
1554
|
-
if (this.
|
|
1515
|
+
this.$root.setAttribute('tabindex', '-1');
|
|
1516
|
+
if (this.config.startHidden) {
|
|
1555
1517
|
this.hideMenu();
|
|
1556
1518
|
}
|
|
1557
1519
|
}
|
|
1558
1520
|
setupResponsiveChecks() {
|
|
1559
|
-
this.mq = window.matchMedia(this.
|
|
1521
|
+
this.mq = window.matchMedia(this.config.bigModeMediaQuery);
|
|
1560
1522
|
this.mq.addListener(this.checkMode.bind(this));
|
|
1561
|
-
this.checkMode(
|
|
1523
|
+
this.checkMode();
|
|
1562
1524
|
}
|
|
1563
1525
|
createToggleButton() {
|
|
1564
|
-
this
|
|
1565
|
-
this
|
|
1566
|
-
this
|
|
1567
|
-
this
|
|
1568
|
-
this
|
|
1569
|
-
this
|
|
1570
|
-
this
|
|
1571
|
-
this.
|
|
1572
|
-
}
|
|
1573
|
-
checkMode(
|
|
1574
|
-
if (mq.matches) {
|
|
1526
|
+
this.$menuButton = document.createElement('button');
|
|
1527
|
+
this.$menuButton.setAttribute('type', 'button');
|
|
1528
|
+
this.$menuButton.setAttribute('aria-haspopup', 'true');
|
|
1529
|
+
this.$menuButton.setAttribute('aria-expanded', 'false');
|
|
1530
|
+
this.$menuButton.className = `govuk-button ${this.config.toggleButton.classes}`;
|
|
1531
|
+
this.$menuButton.textContent = this.config.toggleButton.showText;
|
|
1532
|
+
this.$menuButton.addEventListener('click', this.onMenuButtonClick.bind(this));
|
|
1533
|
+
this.$toggleButtonContainer.append(this.$menuButton);
|
|
1534
|
+
}
|
|
1535
|
+
checkMode() {
|
|
1536
|
+
if (this.mq.matches) {
|
|
1575
1537
|
this.enableBigMode();
|
|
1576
1538
|
} else {
|
|
1577
1539
|
this.enableSmallMode();
|
|
@@ -1586,69 +1548,147 @@
|
|
|
1586
1548
|
this.addCloseButton();
|
|
1587
1549
|
}
|
|
1588
1550
|
addCloseButton() {
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
this.
|
|
1593
|
-
this
|
|
1594
|
-
this.closeButton
|
|
1595
|
-
this.closeButton.textContent = this.options.closeButton.text;
|
|
1596
|
-
this.closeButton.addEventListener('click', this.onCloseClick.bind(this));
|
|
1597
|
-
this.options.closeButton.container.append(this.closeButton);
|
|
1551
|
+
this.$closeButton = document.createElement('button');
|
|
1552
|
+
this.$closeButton.setAttribute('type', 'button');
|
|
1553
|
+
this.$closeButton.className = this.config.closeButton.classes;
|
|
1554
|
+
this.$closeButton.textContent = this.config.closeButton.text;
|
|
1555
|
+
this.$closeButton.addEventListener('click', this.onCloseClick.bind(this));
|
|
1556
|
+
this.$closeButtonContainer.append(this.$closeButton);
|
|
1598
1557
|
}
|
|
1599
1558
|
onCloseClick() {
|
|
1600
1559
|
this.hideMenu();
|
|
1601
|
-
this
|
|
1560
|
+
this.$menuButton.focus();
|
|
1602
1561
|
}
|
|
1603
1562
|
removeCloseButton() {
|
|
1604
|
-
if (this
|
|
1605
|
-
this
|
|
1606
|
-
this
|
|
1563
|
+
if (this.$closeButton) {
|
|
1564
|
+
this.$closeButton.remove();
|
|
1565
|
+
this.$closeButton = null;
|
|
1607
1566
|
}
|
|
1608
1567
|
}
|
|
1609
1568
|
hideMenu() {
|
|
1610
|
-
this
|
|
1611
|
-
this.
|
|
1612
|
-
this
|
|
1569
|
+
this.$menuButton.setAttribute('aria-expanded', 'false');
|
|
1570
|
+
this.$root.classList.add('moj-js-hidden');
|
|
1571
|
+
this.$menuButton.textContent = this.config.toggleButton.showText;
|
|
1613
1572
|
}
|
|
1614
1573
|
showMenu() {
|
|
1615
|
-
this
|
|
1616
|
-
this.
|
|
1617
|
-
this
|
|
1574
|
+
this.$menuButton.setAttribute('aria-expanded', 'true');
|
|
1575
|
+
this.$root.classList.remove('moj-js-hidden');
|
|
1576
|
+
this.$menuButton.textContent = this.config.toggleButton.hideText;
|
|
1618
1577
|
}
|
|
1619
1578
|
onMenuButtonClick() {
|
|
1620
1579
|
this.toggle();
|
|
1621
1580
|
}
|
|
1622
1581
|
toggle() {
|
|
1623
|
-
if (this
|
|
1582
|
+
if (this.$menuButton.getAttribute('aria-expanded') === 'false') {
|
|
1624
1583
|
this.showMenu();
|
|
1625
|
-
this.
|
|
1584
|
+
this.$root.focus();
|
|
1626
1585
|
} else {
|
|
1627
1586
|
this.hideMenu();
|
|
1628
1587
|
}
|
|
1629
1588
|
}
|
|
1589
|
+
|
|
1590
|
+
/**
|
|
1591
|
+
* Name for the component used when initialising using data-module attributes.
|
|
1592
|
+
*/
|
|
1630
1593
|
}
|
|
1631
1594
|
|
|
1632
|
-
|
|
1595
|
+
/**
|
|
1596
|
+
* @typedef {object} FilterToggleButtonConfig
|
|
1597
|
+
* @property {string} [bigModeMediaQuery] - Media query for big mode
|
|
1598
|
+
* @property {boolean} [startHidden] - Whether to start hidden
|
|
1599
|
+
* @property {object} [toggleButton] - Toggle button config
|
|
1600
|
+
* @property {string} [toggleButton.showText] - Text for show button
|
|
1601
|
+
* @property {string} [toggleButton.hideText] - Text for hide button
|
|
1602
|
+
* @property {string} [toggleButton.classes] - Classes for toggle button
|
|
1603
|
+
* @property {object} [toggleButtonContainer] - Toggle button container config
|
|
1604
|
+
* @property {string} [toggleButtonContainer.selector] - Selector for toggle button container
|
|
1605
|
+
* @property {Element | null} [toggleButtonContainer.element] - HTML element for toggle button container
|
|
1606
|
+
* @property {object} [closeButton] - Close button config
|
|
1607
|
+
* @property {string} [closeButton.text] - Text for close button
|
|
1608
|
+
* @property {string} [closeButton.classes] - Classes for close button
|
|
1609
|
+
* @property {object} [closeButtonContainer] - Close button container config
|
|
1610
|
+
* @property {string} [closeButtonContainer.selector] - Selector for close button container
|
|
1611
|
+
* @property {Element | null} [closeButtonContainer.element] - HTML element for close button container
|
|
1612
|
+
*/
|
|
1613
|
+
|
|
1614
|
+
/**
|
|
1615
|
+
* @import { Schema } from 'govuk-frontend/dist/govuk/common/configuration.mjs'
|
|
1616
|
+
*/
|
|
1617
|
+
FilterToggleButton.moduleName = 'moj-filter';
|
|
1618
|
+
/**
|
|
1619
|
+
* Filter toggle button config
|
|
1620
|
+
*
|
|
1621
|
+
* @type {FilterToggleButtonConfig}
|
|
1622
|
+
*/
|
|
1623
|
+
FilterToggleButton.defaults = Object.freeze({
|
|
1624
|
+
bigModeMediaQuery: '(min-width: 48.0625em)',
|
|
1625
|
+
startHidden: true,
|
|
1626
|
+
toggleButton: {
|
|
1627
|
+
showText: 'Show filter',
|
|
1628
|
+
hideText: 'Hide filter',
|
|
1629
|
+
classes: 'govuk-button--secondary'
|
|
1630
|
+
},
|
|
1631
|
+
toggleButtonContainer: {
|
|
1632
|
+
selector: '.moj-action-bar__filter'
|
|
1633
|
+
},
|
|
1634
|
+
closeButton: {
|
|
1635
|
+
text: 'Close',
|
|
1636
|
+
classes: 'moj-filter__close'
|
|
1637
|
+
},
|
|
1638
|
+
closeButtonContainer: {
|
|
1639
|
+
selector: '.moj-filter__header-action'
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
/**
|
|
1643
|
+
* Filter toggle button config schema
|
|
1644
|
+
*
|
|
1645
|
+
* @satisfies {Schema<FilterToggleButtonConfig>}
|
|
1646
|
+
*/
|
|
1647
|
+
FilterToggleButton.schema = Object.freeze(/** @type {const} */{
|
|
1648
|
+
properties: {
|
|
1649
|
+
bigModeMediaQuery: {
|
|
1650
|
+
type: 'string'
|
|
1651
|
+
},
|
|
1652
|
+
startHidden: {
|
|
1653
|
+
type: 'boolean'
|
|
1654
|
+
},
|
|
1655
|
+
toggleButton: {
|
|
1656
|
+
type: 'object'
|
|
1657
|
+
},
|
|
1658
|
+
toggleButtonContainer: {
|
|
1659
|
+
type: 'object'
|
|
1660
|
+
},
|
|
1661
|
+
closeButton: {
|
|
1662
|
+
type: 'object'
|
|
1663
|
+
},
|
|
1664
|
+
closeButtonContainer: {
|
|
1665
|
+
type: 'object'
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
});
|
|
1669
|
+
|
|
1670
|
+
/**
|
|
1671
|
+
* @augments {ConfigurableComponent<FormValidatorConfig, HTMLFormElement>}
|
|
1672
|
+
*/
|
|
1673
|
+
class FormValidator extends govukFrontend.ConfigurableComponent {
|
|
1633
1674
|
/**
|
|
1634
|
-
* @param {Element | null}
|
|
1635
|
-
* @param {FormValidatorConfig} [config] -
|
|
1675
|
+
* @param {Element | null} $root - HTML element to use for form validator
|
|
1676
|
+
* @param {FormValidatorConfig} [config] - Form validator config
|
|
1636
1677
|
*/
|
|
1637
|
-
constructor(
|
|
1638
|
-
|
|
1678
|
+
constructor($root, config = {}) {
|
|
1679
|
+
super($root, config);
|
|
1680
|
+
const $summary = this.config.summary.element || document.querySelector(this.config.summary.selector);
|
|
1681
|
+
if (!$summary || !($summary instanceof HTMLElement)) {
|
|
1639
1682
|
return this;
|
|
1640
1683
|
}
|
|
1641
|
-
this
|
|
1642
|
-
this.errors = [];
|
|
1643
|
-
this.validators = [];
|
|
1644
|
-
this.form.addEventListener('submit', this.onSubmit.bind(this));
|
|
1645
|
-
this.summary = config.summary || document.querySelector('.govuk-error-summary');
|
|
1684
|
+
this.$summary = $summary;
|
|
1685
|
+
this.errors = /** @type {ValidationError[]} */[];
|
|
1686
|
+
this.validators = /** @type {Validator[]} */[];
|
|
1646
1687
|
this.originalTitle = document.title;
|
|
1688
|
+
this.$root.addEventListener('submit', this.onSubmit.bind(this));
|
|
1647
1689
|
}
|
|
1648
|
-
escapeHtml(string) {
|
|
1649
|
-
return String(string).replace(/[&<>"'`=/]/g,
|
|
1650
|
-
return FormValidator.entityMap[s];
|
|
1651
|
-
});
|
|
1690
|
+
escapeHtml(string = '') {
|
|
1691
|
+
return String(string).replace(/[&<>"'`=/]/g, name => FormValidator.entityMap[name]);
|
|
1652
1692
|
}
|
|
1653
1693
|
resetTitle() {
|
|
1654
1694
|
document.title = this.originalTitle;
|
|
@@ -1657,10 +1697,10 @@
|
|
|
1657
1697
|
document.title = `${this.errors.length} errors - ${document.title}`;
|
|
1658
1698
|
}
|
|
1659
1699
|
showSummary() {
|
|
1660
|
-
this
|
|
1661
|
-
this
|
|
1662
|
-
this
|
|
1663
|
-
this
|
|
1700
|
+
this.$summary.innerHTML = this.getSummaryHtml();
|
|
1701
|
+
this.$summary.classList.remove('moj-hidden');
|
|
1702
|
+
this.$summary.setAttribute('aria-labelledby', 'errorSummary-heading');
|
|
1703
|
+
this.$summary.focus();
|
|
1664
1704
|
}
|
|
1665
1705
|
getSummaryHtml() {
|
|
1666
1706
|
let html = '<h2 id="error-summary-title" class="govuk-error-summary__title">There is a problem</h2>';
|
|
@@ -1678,9 +1718,13 @@
|
|
|
1678
1718
|
return html;
|
|
1679
1719
|
}
|
|
1680
1720
|
hideSummary() {
|
|
1681
|
-
this
|
|
1682
|
-
this
|
|
1721
|
+
this.$summary.classList.add('moj-hidden');
|
|
1722
|
+
this.$summary.removeAttribute('aria-labelledby');
|
|
1683
1723
|
}
|
|
1724
|
+
|
|
1725
|
+
/**
|
|
1726
|
+
* @param {SubmitEvent} event - Form submit event
|
|
1727
|
+
*/
|
|
1684
1728
|
onSubmit(event) {
|
|
1685
1729
|
this.removeInlineErrors();
|
|
1686
1730
|
this.hideSummary();
|
|
@@ -1697,25 +1741,29 @@
|
|
|
1697
1741
|
this.showInlineError(error);
|
|
1698
1742
|
}
|
|
1699
1743
|
}
|
|
1744
|
+
|
|
1745
|
+
/**
|
|
1746
|
+
* @param {ValidationError} error
|
|
1747
|
+
*/
|
|
1700
1748
|
showInlineError(error) {
|
|
1701
|
-
const errorSpan = document.createElement('span');
|
|
1702
|
-
errorSpan.id = `${error.fieldName}-error`;
|
|
1703
|
-
errorSpan.classList.add('govuk-error-message');
|
|
1704
|
-
errorSpan.innerHTML = this.escapeHtml(error.message);
|
|
1705
|
-
const control = document.querySelector(`#${error.fieldName}`);
|
|
1706
|
-
const fieldset = control.closest('.govuk-fieldset');
|
|
1707
|
-
const fieldContainer = (fieldset || control).closest('.govuk-form-group');
|
|
1708
|
-
const label = fieldContainer.querySelector('label');
|
|
1709
|
-
const legend = fieldContainer.querySelector('legend');
|
|
1710
|
-
fieldContainer.classList.add('govuk-form-group--error');
|
|
1711
|
-
if (fieldset && legend) {
|
|
1712
|
-
legend.after(errorSpan);
|
|
1713
|
-
fieldContainer.setAttribute('aria-invalid', 'true');
|
|
1714
|
-
addAttributeValue(fieldset, 'aria-describedby', errorSpan.id);
|
|
1715
|
-
} else if (label && control) {
|
|
1716
|
-
label.after(errorSpan);
|
|
1717
|
-
control.setAttribute('aria-invalid', 'true');
|
|
1718
|
-
addAttributeValue(control, 'aria-describedby', errorSpan.id);
|
|
1749
|
+
const $errorSpan = document.createElement('span');
|
|
1750
|
+
$errorSpan.id = `${error.fieldName}-error`;
|
|
1751
|
+
$errorSpan.classList.add('govuk-error-message');
|
|
1752
|
+
$errorSpan.innerHTML = this.escapeHtml(error.message);
|
|
1753
|
+
const $control = document.querySelector(`#${error.fieldName}`);
|
|
1754
|
+
const $fieldset = $control.closest('.govuk-fieldset');
|
|
1755
|
+
const $fieldContainer = ($fieldset || $control).closest('.govuk-form-group');
|
|
1756
|
+
const $label = $fieldContainer.querySelector('label');
|
|
1757
|
+
const $legend = $fieldContainer.querySelector('legend');
|
|
1758
|
+
$fieldContainer.classList.add('govuk-form-group--error');
|
|
1759
|
+
if ($fieldset && $legend) {
|
|
1760
|
+
$legend.after($errorSpan);
|
|
1761
|
+
$fieldContainer.setAttribute('aria-invalid', 'true');
|
|
1762
|
+
addAttributeValue($fieldset, 'aria-describedby', $errorSpan.id);
|
|
1763
|
+
} else if ($label && $control) {
|
|
1764
|
+
$label.after($errorSpan);
|
|
1765
|
+
$control.setAttribute('aria-invalid', 'true');
|
|
1766
|
+
addAttributeValue($control, 'aria-describedby', $errorSpan.id);
|
|
1719
1767
|
}
|
|
1720
1768
|
}
|
|
1721
1769
|
removeInlineErrors() {
|
|
@@ -1723,33 +1771,46 @@
|
|
|
1723
1771
|
this.removeInlineError(error);
|
|
1724
1772
|
}
|
|
1725
1773
|
}
|
|
1774
|
+
|
|
1775
|
+
/**
|
|
1776
|
+
* @param {ValidationError} error
|
|
1777
|
+
*/
|
|
1726
1778
|
removeInlineError(error) {
|
|
1727
|
-
const errorSpan = document.querySelector(`#${error.fieldName}-error`);
|
|
1728
|
-
const control = document.querySelector(`#${error.fieldName}`);
|
|
1729
|
-
const fieldset = control.closest('.govuk-fieldset');
|
|
1730
|
-
const fieldContainer = (fieldset || control).closest('.govuk-form-group');
|
|
1731
|
-
const label = fieldContainer.querySelector('label');
|
|
1732
|
-
const legend = fieldContainer.querySelector('legend');
|
|
1733
|
-
errorSpan.remove();
|
|
1734
|
-
fieldContainer.classList.remove('govuk-form-group--error');
|
|
1735
|
-
if (fieldset && legend) {
|
|
1736
|
-
fieldContainer.removeAttribute('aria-invalid');
|
|
1737
|
-
removeAttributeValue(fieldset, 'aria-describedby', errorSpan.id);
|
|
1738
|
-
} else if (label && control) {
|
|
1739
|
-
control.removeAttribute('aria-invalid');
|
|
1740
|
-
removeAttributeValue(control, 'aria-describedby', errorSpan.id);
|
|
1779
|
+
const $errorSpan = document.querySelector(`#${error.fieldName}-error`);
|
|
1780
|
+
const $control = document.querySelector(`#${error.fieldName}`);
|
|
1781
|
+
const $fieldset = $control.closest('.govuk-fieldset');
|
|
1782
|
+
const $fieldContainer = ($fieldset || $control).closest('.govuk-form-group');
|
|
1783
|
+
const $label = $fieldContainer.querySelector('label');
|
|
1784
|
+
const $legend = $fieldContainer.querySelector('legend');
|
|
1785
|
+
$errorSpan.remove();
|
|
1786
|
+
$fieldContainer.classList.remove('govuk-form-group--error');
|
|
1787
|
+
if ($fieldset && $legend) {
|
|
1788
|
+
$fieldContainer.removeAttribute('aria-invalid');
|
|
1789
|
+
removeAttributeValue($fieldset, 'aria-describedby', $errorSpan.id);
|
|
1790
|
+
} else if ($label && $control) {
|
|
1791
|
+
$control.removeAttribute('aria-invalid');
|
|
1792
|
+
removeAttributeValue($control, 'aria-describedby', $errorSpan.id);
|
|
1741
1793
|
}
|
|
1742
1794
|
}
|
|
1795
|
+
|
|
1796
|
+
/**
|
|
1797
|
+
* @param {string} fieldName - Field name
|
|
1798
|
+
* @param {ValidationRule[]} rules - Validation rules
|
|
1799
|
+
*/
|
|
1743
1800
|
addValidator(fieldName, rules) {
|
|
1744
1801
|
this.validators.push({
|
|
1745
1802
|
fieldName,
|
|
1746
1803
|
rules,
|
|
1747
|
-
field: this.
|
|
1804
|
+
field: this.$root.elements.namedItem(fieldName)
|
|
1748
1805
|
});
|
|
1749
1806
|
}
|
|
1750
1807
|
validate() {
|
|
1751
1808
|
this.errors = [];
|
|
1809
|
+
|
|
1810
|
+
/** @type {Validator | null} */
|
|
1752
1811
|
let validator = null;
|
|
1812
|
+
|
|
1813
|
+
/** @type {boolean | string} */
|
|
1753
1814
|
let validatorReturnValue = true;
|
|
1754
1815
|
let i;
|
|
1755
1816
|
let j;
|
|
@@ -1774,11 +1835,41 @@
|
|
|
1774
1835
|
}
|
|
1775
1836
|
return this.errors.length === 0;
|
|
1776
1837
|
}
|
|
1838
|
+
|
|
1839
|
+
/**
|
|
1840
|
+
* @type {Record<string, string>}
|
|
1841
|
+
*/
|
|
1777
1842
|
}
|
|
1778
1843
|
|
|
1779
1844
|
/**
|
|
1780
1845
|
* @typedef {object} FormValidatorConfig
|
|
1781
|
-
* @property {
|
|
1846
|
+
* @property {object} [summary] - Error summary config
|
|
1847
|
+
* @property {string} [summary.selector] - Selector for error summary
|
|
1848
|
+
* @property {Element | null} [summary.element] - HTML element for error summary
|
|
1849
|
+
*/
|
|
1850
|
+
|
|
1851
|
+
/**
|
|
1852
|
+
* @typedef {object} ValidationRule
|
|
1853
|
+
* @property {(field: Validator['field'], params: Record<string, Validator['field']>) => boolean | string} method - Validation method
|
|
1854
|
+
* @property {string} message - Error message
|
|
1855
|
+
* @property {Record<string, Validator['field']>} [params] - Parameters for validation
|
|
1856
|
+
*/
|
|
1857
|
+
|
|
1858
|
+
/**
|
|
1859
|
+
* @typedef {object} ValidationError
|
|
1860
|
+
* @property {string} fieldName - Name of the field
|
|
1861
|
+
* @property {string} message - Validation error message
|
|
1862
|
+
*/
|
|
1863
|
+
|
|
1864
|
+
/**
|
|
1865
|
+
* @typedef {object} Validator
|
|
1866
|
+
* @property {string} fieldName - Name of the field
|
|
1867
|
+
* @property {ValidationRule[]} rules - Validation rules
|
|
1868
|
+
* @property {Element | RadioNodeList} field - Form field
|
|
1869
|
+
*/
|
|
1870
|
+
|
|
1871
|
+
/**
|
|
1872
|
+
* @import { Schema } from 'govuk-frontend/dist/govuk/common/configuration.mjs'
|
|
1782
1873
|
*/
|
|
1783
1874
|
FormValidator.entityMap = {
|
|
1784
1875
|
'&': '&',
|
|
@@ -1790,164 +1881,218 @@
|
|
|
1790
1881
|
'`': '`',
|
|
1791
1882
|
'=': '='
|
|
1792
1883
|
};
|
|
1884
|
+
/**
|
|
1885
|
+
* Name for the component used when initialising using data-module attributes.
|
|
1886
|
+
*/
|
|
1887
|
+
FormValidator.moduleName = 'moj-form-validator';
|
|
1888
|
+
/**
|
|
1889
|
+
* Multi file upload default config
|
|
1890
|
+
*
|
|
1891
|
+
* @type {FormValidatorConfig}
|
|
1892
|
+
*/
|
|
1893
|
+
FormValidator.defaults = Object.freeze({
|
|
1894
|
+
summary: {
|
|
1895
|
+
selector: '.govuk-error-summary'
|
|
1896
|
+
}
|
|
1897
|
+
});
|
|
1898
|
+
/**
|
|
1899
|
+
* Multi file upload config schema
|
|
1900
|
+
*
|
|
1901
|
+
* @satisfies {Schema<FormValidatorConfig>}
|
|
1902
|
+
*/
|
|
1903
|
+
FormValidator.schema = Object.freeze(/** @type {const} */{
|
|
1904
|
+
properties: {
|
|
1905
|
+
summary: {
|
|
1906
|
+
type: 'object'
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
});
|
|
1793
1910
|
|
|
1794
1911
|
/* eslint-disable @typescript-eslint/no-empty-function */
|
|
1795
1912
|
|
|
1796
|
-
|
|
1913
|
+
|
|
1914
|
+
/**
|
|
1915
|
+
* @augments {ConfigurableComponent<MultiFileUploadConfig>}
|
|
1916
|
+
*/
|
|
1917
|
+
class MultiFileUpload extends govukFrontend.ConfigurableComponent {
|
|
1797
1918
|
/**
|
|
1798
|
-
* @param {
|
|
1919
|
+
* @param {Element | null} $root - HTML element to use for multi file upload
|
|
1920
|
+
* @param {MultiFileUploadConfig} [config] - Multi file upload config
|
|
1799
1921
|
*/
|
|
1800
|
-
constructor(
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
if (!container || !(container instanceof HTMLElement) || !(dragAndDropSupported() && formDataSupported() && fileApiSupported())) {
|
|
1922
|
+
constructor($root, config = {}) {
|
|
1923
|
+
var _this$config$feedback;
|
|
1924
|
+
super($root, config);
|
|
1925
|
+
if (!MultiFileUpload.isSupported()) {
|
|
1805
1926
|
return this;
|
|
1806
1927
|
}
|
|
1807
|
-
this.
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
uploadFileErrorHook: () => {},
|
|
1813
|
-
fileDeleteHook: () => {},
|
|
1814
|
-
uploadStatusText: 'Uploading files, please wait',
|
|
1815
|
-
dropzoneHintText: 'Drag and drop files here or',
|
|
1816
|
-
dropzoneButtonText: 'Choose files'
|
|
1817
|
-
};
|
|
1818
|
-
this.params = Object.assign({}, this.defaultParams, params);
|
|
1819
|
-
this.feedbackContainer = /** @type {HTMLDivElement} */
|
|
1820
|
-
this.container.querySelector('.moj-multi-file__uploaded-files');
|
|
1928
|
+
const $feedbackContainer = (_this$config$feedback = this.config.feedbackContainer.element) != null ? _this$config$feedback : this.$root.querySelector(this.config.feedbackContainer.selector);
|
|
1929
|
+
if (!$feedbackContainer || !($feedbackContainer instanceof HTMLElement)) {
|
|
1930
|
+
return this;
|
|
1931
|
+
}
|
|
1932
|
+
this.$feedbackContainer = $feedbackContainer;
|
|
1821
1933
|
this.setupFileInput();
|
|
1822
1934
|
this.setupDropzone();
|
|
1823
1935
|
this.setupLabel();
|
|
1824
1936
|
this.setupStatusBox();
|
|
1825
|
-
this.
|
|
1937
|
+
this.$root.addEventListener('click', this.onFileDeleteClick.bind(this));
|
|
1938
|
+
this.$root.classList.add('moj-multi-file-upload--enhanced');
|
|
1826
1939
|
}
|
|
1827
1940
|
setupDropzone() {
|
|
1828
|
-
this
|
|
1829
|
-
this
|
|
1830
|
-
this
|
|
1831
|
-
this
|
|
1832
|
-
this
|
|
1833
|
-
this
|
|
1834
|
-
this
|
|
1941
|
+
this.$dropzone = document.createElement('div');
|
|
1942
|
+
this.$dropzone.classList.add('moj-multi-file-upload__dropzone');
|
|
1943
|
+
this.$dropzone.addEventListener('dragover', this.onDragOver.bind(this));
|
|
1944
|
+
this.$dropzone.addEventListener('dragleave', this.onDragLeave.bind(this));
|
|
1945
|
+
this.$dropzone.addEventListener('drop', this.onDrop.bind(this));
|
|
1946
|
+
this.$fileInput.replaceWith(this.$dropzone);
|
|
1947
|
+
this.$dropzone.appendChild(this.$fileInput);
|
|
1835
1948
|
}
|
|
1836
1949
|
setupLabel() {
|
|
1837
|
-
const label = document.createElement('label');
|
|
1838
|
-
label.setAttribute('for', this
|
|
1839
|
-
label.classList.add('govuk-button', 'govuk-button--secondary');
|
|
1840
|
-
label.textContent = this.
|
|
1841
|
-
const hint = document.createElement('p');
|
|
1842
|
-
hint.classList.add('govuk-body');
|
|
1843
|
-
hint.textContent = this.
|
|
1844
|
-
this
|
|
1845
|
-
this
|
|
1846
|
-
this
|
|
1950
|
+
const $label = document.createElement('label');
|
|
1951
|
+
$label.setAttribute('for', this.$fileInput.id);
|
|
1952
|
+
$label.classList.add('govuk-button', 'govuk-button--secondary');
|
|
1953
|
+
$label.textContent = this.config.dropzoneButtonText;
|
|
1954
|
+
const $hint = document.createElement('p');
|
|
1955
|
+
$hint.classList.add('govuk-body');
|
|
1956
|
+
$hint.textContent = this.config.dropzoneHintText;
|
|
1957
|
+
this.$label = $label;
|
|
1958
|
+
this.$dropzone.append($hint);
|
|
1959
|
+
this.$dropzone.append($label);
|
|
1847
1960
|
}
|
|
1848
1961
|
setupFileInput() {
|
|
1849
|
-
this
|
|
1850
|
-
this.
|
|
1851
|
-
this
|
|
1852
|
-
this
|
|
1853
|
-
this
|
|
1962
|
+
this.$fileInput = /** @type {HTMLInputElement} */
|
|
1963
|
+
this.$root.querySelector('.moj-multi-file-upload__input');
|
|
1964
|
+
this.$fileInput.addEventListener('change', this.onFileChange.bind(this));
|
|
1965
|
+
this.$fileInput.addEventListener('focus', this.onFileFocus.bind(this));
|
|
1966
|
+
this.$fileInput.addEventListener('blur', this.onFileBlur.bind(this));
|
|
1854
1967
|
}
|
|
1855
1968
|
setupStatusBox() {
|
|
1856
|
-
this
|
|
1857
|
-
this
|
|
1858
|
-
this
|
|
1859
|
-
this
|
|
1860
|
-
this
|
|
1969
|
+
this.$status = document.createElement('div');
|
|
1970
|
+
this.$status.classList.add('govuk-visually-hidden');
|
|
1971
|
+
this.$status.setAttribute('aria-live', 'polite');
|
|
1972
|
+
this.$status.setAttribute('role', 'status');
|
|
1973
|
+
this.$dropzone.append(this.$status);
|
|
1861
1974
|
}
|
|
1975
|
+
|
|
1976
|
+
/**
|
|
1977
|
+
* @param {DragEvent} event - Drag event
|
|
1978
|
+
*/
|
|
1862
1979
|
onDragOver(event) {
|
|
1863
1980
|
event.preventDefault();
|
|
1864
|
-
this
|
|
1981
|
+
this.$dropzone.classList.add('moj-multi-file-upload--dragover');
|
|
1865
1982
|
}
|
|
1866
1983
|
onDragLeave() {
|
|
1867
|
-
this
|
|
1984
|
+
this.$dropzone.classList.remove('moj-multi-file-upload--dragover');
|
|
1868
1985
|
}
|
|
1986
|
+
|
|
1987
|
+
/**
|
|
1988
|
+
* @param {DragEvent} event - Drag event
|
|
1989
|
+
*/
|
|
1869
1990
|
onDrop(event) {
|
|
1870
1991
|
event.preventDefault();
|
|
1871
|
-
this
|
|
1872
|
-
this
|
|
1873
|
-
this
|
|
1992
|
+
this.$dropzone.classList.remove('moj-multi-file-upload--dragover');
|
|
1993
|
+
this.$feedbackContainer.classList.remove('moj-hidden');
|
|
1994
|
+
this.$status.textContent = this.config.uploadStatusText;
|
|
1874
1995
|
this.uploadFiles(event.dataTransfer.files);
|
|
1875
1996
|
}
|
|
1997
|
+
|
|
1998
|
+
/**
|
|
1999
|
+
* @param {FileList} files - File list
|
|
2000
|
+
*/
|
|
1876
2001
|
uploadFiles(files) {
|
|
1877
2002
|
for (const file of Array.from(files)) {
|
|
1878
2003
|
this.uploadFile(file);
|
|
1879
2004
|
}
|
|
1880
2005
|
}
|
|
1881
2006
|
onFileChange() {
|
|
1882
|
-
this
|
|
1883
|
-
this
|
|
1884
|
-
this.uploadFiles(this
|
|
1885
|
-
const fileInput = this
|
|
1886
|
-
if (
|
|
2007
|
+
this.$feedbackContainer.classList.remove('moj-hidden');
|
|
2008
|
+
this.$status.textContent = this.config.uploadStatusText;
|
|
2009
|
+
this.uploadFiles(this.$fileInput.files);
|
|
2010
|
+
const $fileInput = this.$fileInput.cloneNode(true);
|
|
2011
|
+
if (!$fileInput || !($fileInput instanceof HTMLInputElement)) {
|
|
1887
2012
|
return;
|
|
1888
2013
|
}
|
|
1889
|
-
fileInput.value = '';
|
|
1890
|
-
this
|
|
2014
|
+
$fileInput.value = '';
|
|
2015
|
+
this.$fileInput.replaceWith($fileInput);
|
|
1891
2016
|
this.setupFileInput();
|
|
1892
|
-
this
|
|
2017
|
+
this.$fileInput.focus();
|
|
1893
2018
|
}
|
|
1894
2019
|
onFileFocus() {
|
|
1895
|
-
this
|
|
2020
|
+
this.$label.classList.add('moj-multi-file-upload--focused');
|
|
1896
2021
|
}
|
|
1897
2022
|
onFileBlur() {
|
|
1898
|
-
this
|
|
2023
|
+
this.$label.classList.remove('moj-multi-file-upload--focused');
|
|
1899
2024
|
}
|
|
2025
|
+
|
|
2026
|
+
/**
|
|
2027
|
+
* @param {UploadResponseSuccess['success']} success
|
|
2028
|
+
*/
|
|
1900
2029
|
getSuccessHtml(success) {
|
|
1901
2030
|
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>`;
|
|
1902
2031
|
}
|
|
2032
|
+
|
|
2033
|
+
/**
|
|
2034
|
+
* @param {UploadResponseError['error']} error
|
|
2035
|
+
*/
|
|
1903
2036
|
getErrorHtml(error) {
|
|
1904
2037
|
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>`;
|
|
1905
2038
|
}
|
|
2039
|
+
|
|
2040
|
+
/**
|
|
2041
|
+
* @param {File} file
|
|
2042
|
+
*/
|
|
1906
2043
|
getFileRow(file) {
|
|
1907
|
-
const row = document.createElement('div');
|
|
1908
|
-
row.classList.add('govuk-summary-list__row', 'moj-multi-file-upload__row');
|
|
1909
|
-
row.innerHTML = `
|
|
2044
|
+
const $row = document.createElement('div');
|
|
2045
|
+
$row.classList.add('govuk-summary-list__row', 'moj-multi-file-upload__row');
|
|
2046
|
+
$row.innerHTML = `
|
|
1910
2047
|
<div class="govuk-summary-list__value moj-multi-file-upload__message">
|
|
1911
2048
|
<span class="moj-multi-file-upload__filename">${file.name}</span>
|
|
1912
2049
|
<span class="moj-multi-file-upload__progress">0%</span>
|
|
1913
2050
|
</div>
|
|
1914
2051
|
<div class="govuk-summary-list__actions moj-multi-file-upload__actions"></div>
|
|
1915
2052
|
`;
|
|
1916
|
-
return row;
|
|
2053
|
+
return $row;
|
|
1917
2054
|
}
|
|
2055
|
+
|
|
2056
|
+
/**
|
|
2057
|
+
* @param {UploadResponseFile} file
|
|
2058
|
+
*/
|
|
1918
2059
|
getDeleteButton(file) {
|
|
1919
|
-
const button = document.createElement('button');
|
|
1920
|
-
button.setAttribute('type', 'button');
|
|
1921
|
-
button.setAttribute('name', 'delete');
|
|
1922
|
-
button.setAttribute('value', file.filename);
|
|
1923
|
-
button.classList.add('moj-multi-file-upload__delete', 'govuk-button', 'govuk-button--secondary', 'govuk-!-margin-bottom-0');
|
|
1924
|
-
button.innerHTML = `Delete <span class="govuk-visually-hidden">${file.originalname}</span>`;
|
|
1925
|
-
return button;
|
|
2060
|
+
const $button = document.createElement('button');
|
|
2061
|
+
$button.setAttribute('type', 'button');
|
|
2062
|
+
$button.setAttribute('name', 'delete');
|
|
2063
|
+
$button.setAttribute('value', file.filename);
|
|
2064
|
+
$button.classList.add('moj-multi-file-upload__delete', 'govuk-button', 'govuk-button--secondary', 'govuk-!-margin-bottom-0');
|
|
2065
|
+
$button.innerHTML = `Delete <span class="govuk-visually-hidden">${file.originalname}</span>`;
|
|
2066
|
+
return $button;
|
|
1926
2067
|
}
|
|
2068
|
+
|
|
2069
|
+
/**
|
|
2070
|
+
* @param {File} file
|
|
2071
|
+
*/
|
|
1927
2072
|
uploadFile(file) {
|
|
1928
|
-
this.
|
|
1929
|
-
const item = this.getFileRow(file);
|
|
1930
|
-
const message = item.querySelector('.moj-multi-file-upload__message');
|
|
1931
|
-
const actions = item.querySelector('.moj-multi-file-upload__actions');
|
|
1932
|
-
const progress = item.querySelector('.moj-multi-file-upload__progress');
|
|
2073
|
+
this.config.hooks.entryHook(this, file);
|
|
2074
|
+
const $item = this.getFileRow(file);
|
|
2075
|
+
const $message = $item.querySelector('.moj-multi-file-upload__message');
|
|
2076
|
+
const $actions = $item.querySelector('.moj-multi-file-upload__actions');
|
|
2077
|
+
const $progress = $item.querySelector('.moj-multi-file-upload__progress');
|
|
1933
2078
|
const formData = new FormData();
|
|
1934
2079
|
formData.append('documents', file);
|
|
1935
|
-
this
|
|
2080
|
+
this.$feedbackContainer.querySelector('.moj-multi-file-upload__list').append($item);
|
|
1936
2081
|
const xhr = new XMLHttpRequest();
|
|
1937
2082
|
const onLoad = () => {
|
|
1938
2083
|
if (xhr.status < 200 || xhr.status >= 300 || !('success' in xhr.response)) {
|
|
1939
2084
|
return onError();
|
|
1940
2085
|
}
|
|
1941
|
-
message.innerHTML = this.getSuccessHtml(xhr.response.success);
|
|
1942
|
-
this
|
|
1943
|
-
actions.append(this.getDeleteButton(xhr.response.file));
|
|
1944
|
-
this.
|
|
2086
|
+
$message.innerHTML = this.getSuccessHtml(xhr.response.success);
|
|
2087
|
+
this.$status.textContent = xhr.response.success.messageText;
|
|
2088
|
+
$actions.append(this.getDeleteButton(xhr.response.file));
|
|
2089
|
+
this.config.hooks.exitHook(this, file, xhr, xhr.responseText);
|
|
1945
2090
|
};
|
|
1946
2091
|
const onError = () => {
|
|
1947
2092
|
const error = new Error(xhr.response && 'error' in xhr.response ? xhr.response.error.message : xhr.statusText || 'Upload failed');
|
|
1948
|
-
message.innerHTML = this.getErrorHtml(error);
|
|
1949
|
-
this
|
|
1950
|
-
this.
|
|
2093
|
+
$message.innerHTML = this.getErrorHtml(error);
|
|
2094
|
+
this.$status.textContent = error.message;
|
|
2095
|
+
this.config.hooks.errorHook(this, file, xhr, xhr.responseText, error);
|
|
1951
2096
|
};
|
|
1952
2097
|
xhr.addEventListener('load', onLoad);
|
|
1953
2098
|
xhr.addEventListener('error', onError);
|
|
@@ -1956,15 +2101,19 @@
|
|
|
1956
2101
|
return;
|
|
1957
2102
|
}
|
|
1958
2103
|
const percentComplete = Math.round(event.loaded / event.total * 100);
|
|
1959
|
-
progress.textContent = ` ${percentComplete}%`;
|
|
2104
|
+
$progress.textContent = ` ${percentComplete}%`;
|
|
1960
2105
|
});
|
|
1961
|
-
xhr.open('POST', this.
|
|
2106
|
+
xhr.open('POST', this.config.uploadUrl);
|
|
1962
2107
|
xhr.responseType = 'json';
|
|
1963
2108
|
xhr.send(formData);
|
|
1964
2109
|
}
|
|
2110
|
+
|
|
2111
|
+
/**
|
|
2112
|
+
* @param {MouseEvent} event - Click event
|
|
2113
|
+
*/
|
|
1965
2114
|
onFileDeleteClick(event) {
|
|
1966
|
-
const button = event.target;
|
|
1967
|
-
if (
|
|
2115
|
+
const $button = event.target;
|
|
2116
|
+
if (!$button || !($button instanceof HTMLButtonElement) || !$button.classList.contains('moj-multi-file-upload__delete')) {
|
|
1968
2117
|
return;
|
|
1969
2118
|
}
|
|
1970
2119
|
event.preventDefault(); // if user refreshes page and then deletes
|
|
@@ -1974,155 +2123,371 @@
|
|
|
1974
2123
|
if (xhr.status < 200 || xhr.status >= 300) {
|
|
1975
2124
|
return;
|
|
1976
2125
|
}
|
|
1977
|
-
const rows = Array.from(this
|
|
1978
|
-
if (rows.length === 1) {
|
|
1979
|
-
this
|
|
2126
|
+
const $rows = Array.from(this.$feedbackContainer.querySelectorAll('.moj-multi-file-upload__row'));
|
|
2127
|
+
if ($rows.length === 1) {
|
|
2128
|
+
this.$feedbackContainer.classList.add('moj-hidden');
|
|
1980
2129
|
}
|
|
1981
|
-
const
|
|
1982
|
-
if (
|
|
1983
|
-
this.
|
|
2130
|
+
const $rowDelete = $rows.find($row => $row.contains($button));
|
|
2131
|
+
if ($rowDelete) $rowDelete.remove();
|
|
2132
|
+
this.config.hooks.deleteHook(this, undefined, xhr, xhr.responseText);
|
|
1984
2133
|
});
|
|
1985
|
-
xhr.open('POST', this.
|
|
2134
|
+
xhr.open('POST', this.config.deleteUrl);
|
|
1986
2135
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
1987
2136
|
xhr.responseType = 'json';
|
|
1988
2137
|
xhr.send(JSON.stringify({
|
|
1989
|
-
[button.name]: button.value
|
|
2138
|
+
[$button.name]: $button.value
|
|
1990
2139
|
}));
|
|
1991
2140
|
}
|
|
2141
|
+
static isSupported() {
|
|
2142
|
+
return this.isDragAndDropSupported() && this.isFormDataSupported() && this.isFileApiSupported();
|
|
2143
|
+
}
|
|
2144
|
+
static isDragAndDropSupported() {
|
|
2145
|
+
const div = document.createElement('div');
|
|
2146
|
+
return typeof div.ondrop !== 'undefined';
|
|
2147
|
+
}
|
|
2148
|
+
static isFormDataSupported() {
|
|
2149
|
+
return typeof FormData === 'function';
|
|
2150
|
+
}
|
|
2151
|
+
static isFileApiSupported() {
|
|
2152
|
+
const input = document.createElement('input');
|
|
2153
|
+
input.type = 'file';
|
|
2154
|
+
return typeof input.files !== 'undefined';
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
/**
|
|
2158
|
+
* Name for the component used when initialising using data-module attributes.
|
|
2159
|
+
*/
|
|
1992
2160
|
}
|
|
1993
2161
|
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2162
|
+
/**
|
|
2163
|
+
* Multi file upload config
|
|
2164
|
+
*
|
|
2165
|
+
* @typedef {object} MultiFileUploadConfig
|
|
2166
|
+
* @property {string} [uploadUrl] - File upload URL
|
|
2167
|
+
* @property {string} [deleteUrl] - File delete URL
|
|
2168
|
+
* @property {string} [uploadStatusText] - Upload status text
|
|
2169
|
+
* @property {string} [dropzoneHintText] - Dropzone hint text
|
|
2170
|
+
* @property {string} [dropzoneButtonText] - Dropzone button text
|
|
2171
|
+
* @property {object} [feedbackContainer] - Feedback container config
|
|
2172
|
+
* @property {string} [feedbackContainer.selector] - Selector for feedback container
|
|
2173
|
+
* @property {Element | null} [feedbackContainer.element] - HTML element for feedback container
|
|
2174
|
+
* @property {MultiFileUploadHooks} [hooks] - Upload hooks
|
|
2175
|
+
*/
|
|
2176
|
+
|
|
2177
|
+
/**
|
|
2178
|
+
* Multi file upload hooks
|
|
2179
|
+
*
|
|
2180
|
+
* @typedef {object} MultiFileUploadHooks
|
|
2181
|
+
* @property {OnUploadFileEntryHook} [entryHook] - File upload entry hook
|
|
2182
|
+
* @property {OnUploadFileExitHook} [exitHook] - File upload exit hook
|
|
2183
|
+
* @property {OnUploadFileErrorHook} [errorHook] - File upload error hook
|
|
2184
|
+
* @property {OnUploadFileDeleteHook} [deleteHook] - File delete hook
|
|
2185
|
+
*/
|
|
2186
|
+
|
|
2187
|
+
/**
|
|
2188
|
+
* Upload hook: File entry
|
|
2189
|
+
*
|
|
2190
|
+
* @callback OnUploadFileEntryHook
|
|
2191
|
+
* @param {InstanceType<typeof MultiFileUpload>} upload - Multi file upload
|
|
2192
|
+
* @param {File} file - File upload
|
|
2193
|
+
*/
|
|
2194
|
+
|
|
2195
|
+
/**
|
|
2196
|
+
* Upload hook: File exit
|
|
2197
|
+
*
|
|
2198
|
+
* @callback OnUploadFileExitHook
|
|
2199
|
+
* @param {InstanceType<typeof MultiFileUpload>} upload - Multi file upload
|
|
2200
|
+
* @param {File} file - File upload
|
|
2201
|
+
* @param {XMLHttpRequest} xhr - XMLHttpRequest
|
|
2202
|
+
* @param {string} textStatus - Text status
|
|
2203
|
+
*/
|
|
2204
|
+
|
|
2205
|
+
/**
|
|
2206
|
+
* Upload hook: File error
|
|
2207
|
+
*
|
|
2208
|
+
* @callback OnUploadFileErrorHook
|
|
2209
|
+
* @param {InstanceType<typeof MultiFileUpload>} upload - Multi file upload
|
|
2210
|
+
* @param {File} file - File upload
|
|
2211
|
+
* @param {XMLHttpRequest} xhr - XMLHttpRequest
|
|
2212
|
+
* @param {string} textStatus - Text status
|
|
2213
|
+
* @param {Error} errorThrown - Error thrown
|
|
2214
|
+
*/
|
|
2215
|
+
|
|
2216
|
+
/**
|
|
2217
|
+
* Upload hook: File delete
|
|
2218
|
+
*
|
|
2219
|
+
* @callback OnUploadFileDeleteHook
|
|
2220
|
+
* @param {InstanceType<typeof MultiFileUpload>} upload - Multi file upload
|
|
2221
|
+
* @param {File} [file] - File upload
|
|
2222
|
+
* @param {XMLHttpRequest} xhr - XMLHttpRequest
|
|
2223
|
+
* @param {string} textStatus - Text status
|
|
2224
|
+
*/
|
|
2225
|
+
|
|
2226
|
+
/**
|
|
2227
|
+
* @typedef {object} UploadResponseSuccess
|
|
2228
|
+
* @property {{ messageText: string, messageHtml: string }} success - Response success
|
|
2229
|
+
* @property {UploadResponseFile} file - Response file
|
|
2230
|
+
*/
|
|
2231
|
+
|
|
2232
|
+
/**
|
|
2233
|
+
* @typedef {object} UploadResponseError
|
|
2234
|
+
* @property {{ message: string }} error - Response error
|
|
2235
|
+
* @property {UploadResponseFile} file - Response file
|
|
2236
|
+
*/
|
|
2237
|
+
|
|
2238
|
+
/**
|
|
2239
|
+
* @typedef {object} UploadResponseFile
|
|
2240
|
+
* @property {string} filename - File name
|
|
2241
|
+
* @property {string} originalname - Original file name
|
|
2242
|
+
*/
|
|
2243
|
+
|
|
2244
|
+
/**
|
|
2245
|
+
* @import { Schema } from 'govuk-frontend/dist/govuk/common/configuration.mjs'
|
|
2246
|
+
*/
|
|
2247
|
+
MultiFileUpload.moduleName = 'moj-multi-file-upload';
|
|
2248
|
+
/**
|
|
2249
|
+
* Multi file upload default config
|
|
2250
|
+
*
|
|
2251
|
+
* @type {MultiFileUploadConfig}
|
|
2252
|
+
*/
|
|
2253
|
+
MultiFileUpload.defaults = Object.freeze({
|
|
2254
|
+
uploadStatusText: 'Uploading files, please wait',
|
|
2255
|
+
dropzoneHintText: 'Drag and drop files here or',
|
|
2256
|
+
dropzoneButtonText: 'Choose files',
|
|
2257
|
+
feedbackContainer: {
|
|
2258
|
+
selector: '.moj-multi-file__uploaded-files'
|
|
2259
|
+
},
|
|
2260
|
+
hooks: {
|
|
2261
|
+
entryHook: () => {},
|
|
2262
|
+
exitHook: () => {},
|
|
2263
|
+
errorHook: () => {},
|
|
2264
|
+
deleteHook: () => {}
|
|
2265
|
+
}
|
|
2266
|
+
});
|
|
2267
|
+
/**
|
|
2268
|
+
* Multi file upload config schema
|
|
2269
|
+
*
|
|
2270
|
+
* @satisfies {Schema<MultiFileUploadConfig>}
|
|
2271
|
+
*/
|
|
2272
|
+
MultiFileUpload.schema = Object.freeze(/** @type {const} */{
|
|
2273
|
+
properties: {
|
|
2274
|
+
uploadUrl: {
|
|
2275
|
+
type: 'string'
|
|
2276
|
+
},
|
|
2277
|
+
deleteUrl: {
|
|
2278
|
+
type: 'string'
|
|
2279
|
+
},
|
|
2280
|
+
uploadStatusText: {
|
|
2281
|
+
type: 'string'
|
|
2282
|
+
},
|
|
2283
|
+
dropzoneHintText: {
|
|
2284
|
+
type: 'string'
|
|
2285
|
+
},
|
|
2286
|
+
dropzoneButtonText: {
|
|
2287
|
+
type: 'string'
|
|
2288
|
+
},
|
|
2289
|
+
feedbackContainer: {
|
|
2290
|
+
type: 'object'
|
|
2291
|
+
},
|
|
2292
|
+
hooks: {
|
|
2293
|
+
type: 'object'
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
});
|
|
2297
|
+
|
|
2298
|
+
/**
|
|
2299
|
+
* @augments {ConfigurableComponent<MultiSelectConfig>}
|
|
2300
|
+
*/
|
|
2301
|
+
class MultiSelect extends govukFrontend.ConfigurableComponent {
|
|
2302
|
+
/**
|
|
2303
|
+
* @param {Element | null} $root - HTML element to use for multi select
|
|
2304
|
+
* @param {MultiSelectConfig} [config] - Multi select config
|
|
2305
|
+
*/
|
|
2306
|
+
constructor($root, config = {}) {
|
|
2307
|
+
var _this$config$checkbox;
|
|
2308
|
+
super($root, config);
|
|
2309
|
+
const $container = this.$root.querySelector(`#${this.config.idPrefix}select-all`);
|
|
2310
|
+
const $checkboxes = /** @type {NodeListOf<HTMLInputElement>} */(_this$config$checkbox = this.config.checkboxes.items) != null ? _this$config$checkbox : this.$root.querySelectorAll(this.config.checkboxes.selector);
|
|
2311
|
+
if (!$container || !($container instanceof HTMLElement) || !$checkboxes.length) {
|
|
1998
2312
|
return this;
|
|
1999
2313
|
}
|
|
2000
|
-
this.
|
|
2001
|
-
|
|
2002
|
-
this.
|
|
2003
|
-
this
|
|
2004
|
-
this.
|
|
2005
|
-
this.
|
|
2006
|
-
this.
|
|
2007
|
-
this.
|
|
2008
|
-
this.checked = options.checked || false;
|
|
2314
|
+
this.setupToggle(this.config.idPrefix);
|
|
2315
|
+
this.$toggleButton = this.$toggle.querySelector('input');
|
|
2316
|
+
this.$toggleButton.addEventListener('click', this.onButtonClick.bind(this));
|
|
2317
|
+
this.$container = $container;
|
|
2318
|
+
this.$container.append(this.$toggle);
|
|
2319
|
+
this.$checkboxes = Array.from($checkboxes);
|
|
2320
|
+
this.$checkboxes.forEach($input => $input.addEventListener('click', this.onCheckboxClick.bind(this)));
|
|
2321
|
+
this.checked = config.checked || false;
|
|
2009
2322
|
}
|
|
2010
2323
|
setupToggle(idPrefix = '') {
|
|
2011
2324
|
const id = `${idPrefix}checkboxes-all`;
|
|
2012
|
-
const toggle = document.createElement('div');
|
|
2013
|
-
const label = document.createElement('label');
|
|
2014
|
-
const input = document.createElement('input');
|
|
2015
|
-
const span = document.createElement('span');
|
|
2016
|
-
toggle.classList.add('govuk-checkboxes__item', 'govuk-checkboxes--small', 'moj-multi-select__checkbox');
|
|
2017
|
-
input.id = id;
|
|
2018
|
-
input.type = 'checkbox';
|
|
2019
|
-
input.classList.add('govuk-checkboxes__input');
|
|
2020
|
-
label.setAttribute('for', id);
|
|
2021
|
-
label.classList.add('govuk-label', 'govuk-checkboxes__label', 'moj-multi-select__toggle-label');
|
|
2022
|
-
span.classList.add('govuk-visually-hidden');
|
|
2023
|
-
span.textContent = 'Select all';
|
|
2024
|
-
label.append(span);
|
|
2025
|
-
toggle.append(input, label);
|
|
2026
|
-
this
|
|
2325
|
+
const $toggle = document.createElement('div');
|
|
2326
|
+
const $label = document.createElement('label');
|
|
2327
|
+
const $input = document.createElement('input');
|
|
2328
|
+
const $span = document.createElement('span');
|
|
2329
|
+
$toggle.classList.add('govuk-checkboxes__item', 'govuk-checkboxes--small', 'moj-multi-select__checkbox');
|
|
2330
|
+
$input.id = id;
|
|
2331
|
+
$input.type = 'checkbox';
|
|
2332
|
+
$input.classList.add('govuk-checkboxes__input');
|
|
2333
|
+
$label.setAttribute('for', id);
|
|
2334
|
+
$label.classList.add('govuk-label', 'govuk-checkboxes__label', 'moj-multi-select__toggle-label');
|
|
2335
|
+
$span.classList.add('govuk-visually-hidden');
|
|
2336
|
+
$span.textContent = 'Select all';
|
|
2337
|
+
$label.append($span);
|
|
2338
|
+
$toggle.append($input, $label);
|
|
2339
|
+
this.$toggle = $toggle;
|
|
2027
2340
|
}
|
|
2028
2341
|
onButtonClick() {
|
|
2029
2342
|
if (this.checked) {
|
|
2030
2343
|
this.uncheckAll();
|
|
2031
|
-
this
|
|
2344
|
+
this.$toggleButton.checked = false;
|
|
2032
2345
|
} else {
|
|
2033
2346
|
this.checkAll();
|
|
2034
|
-
this
|
|
2347
|
+
this.$toggleButton.checked = true;
|
|
2035
2348
|
}
|
|
2036
2349
|
}
|
|
2037
2350
|
checkAll() {
|
|
2038
|
-
this
|
|
2039
|
-
|
|
2351
|
+
this.$checkboxes.forEach($input => {
|
|
2352
|
+
$input.checked = true;
|
|
2040
2353
|
});
|
|
2041
2354
|
this.checked = true;
|
|
2042
2355
|
}
|
|
2043
2356
|
uncheckAll() {
|
|
2044
|
-
this
|
|
2045
|
-
|
|
2357
|
+
this.$checkboxes.forEach($input => {
|
|
2358
|
+
$input.checked = false;
|
|
2046
2359
|
});
|
|
2047
2360
|
this.checked = false;
|
|
2048
2361
|
}
|
|
2362
|
+
|
|
2363
|
+
/**
|
|
2364
|
+
* @param {MouseEvent} event - Click event
|
|
2365
|
+
*/
|
|
2049
2366
|
onCheckboxClick(event) {
|
|
2367
|
+
if (!(event.target instanceof HTMLInputElement)) {
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2050
2370
|
if (!event.target.checked) {
|
|
2051
|
-
this
|
|
2371
|
+
this.$toggleButton.checked = false;
|
|
2052
2372
|
this.checked = false;
|
|
2053
2373
|
} else {
|
|
2054
|
-
if (this
|
|
2055
|
-
this
|
|
2374
|
+
if (this.$checkboxes.filter($input => $input.checked).length === this.$checkboxes.length) {
|
|
2375
|
+
this.$toggleButton.checked = true;
|
|
2056
2376
|
this.checked = true;
|
|
2057
2377
|
}
|
|
2058
2378
|
}
|
|
2059
2379
|
}
|
|
2060
|
-
}
|
|
2061
2380
|
|
|
2062
|
-
class PasswordReveal {
|
|
2063
2381
|
/**
|
|
2064
|
-
*
|
|
2382
|
+
* Name for the component used when initialising using data-module attributes.
|
|
2065
2383
|
*/
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
/**
|
|
2387
|
+
* Multi select config
|
|
2388
|
+
*
|
|
2389
|
+
* @typedef {object} MultiSelectConfig
|
|
2390
|
+
* @property {string} [idPrefix] - Prefix for the Select all" checkbox `id` attribute
|
|
2391
|
+
* @property {boolean} [checked] - Whether the "Select all" checkbox is checked
|
|
2392
|
+
* @property {object} [checkboxes] - Checkboxes config
|
|
2393
|
+
* @property {string} [checkboxes.selector] - Checkboxes query selector
|
|
2394
|
+
* @property {NodeListOf<HTMLInputElement>} [checkboxes.items] - Checkboxes query selector results
|
|
2395
|
+
*/
|
|
2396
|
+
|
|
2397
|
+
/**
|
|
2398
|
+
* @import { Schema } from 'govuk-frontend/dist/govuk/common/configuration.mjs'
|
|
2399
|
+
*/
|
|
2400
|
+
MultiSelect.moduleName = 'moj-multi-select';
|
|
2401
|
+
/**
|
|
2402
|
+
* Multi select config
|
|
2403
|
+
*
|
|
2404
|
+
* @type {MultiSelectConfig}
|
|
2405
|
+
*/
|
|
2406
|
+
MultiSelect.defaults = Object.freeze({
|
|
2407
|
+
idPrefix: '',
|
|
2408
|
+
checkboxes: {
|
|
2409
|
+
selector: 'tbody input.govuk-checkboxes__input'
|
|
2410
|
+
}
|
|
2411
|
+
});
|
|
2412
|
+
/**
|
|
2413
|
+
* Multi select config schema
|
|
2414
|
+
*
|
|
2415
|
+
* @satisfies {Schema<MultiSelectConfig>}
|
|
2416
|
+
*/
|
|
2417
|
+
MultiSelect.schema = Object.freeze(/** @type {const} */{
|
|
2418
|
+
properties: {
|
|
2419
|
+
idPrefix: {
|
|
2420
|
+
type: 'string'
|
|
2421
|
+
},
|
|
2422
|
+
checked: {
|
|
2423
|
+
type: 'boolean'
|
|
2424
|
+
},
|
|
2425
|
+
checkboxes: {
|
|
2426
|
+
type: 'object'
|
|
2069
2427
|
}
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2428
|
+
}
|
|
2429
|
+
});
|
|
2430
|
+
|
|
2431
|
+
class PasswordReveal extends govukFrontend.Component {
|
|
2432
|
+
/**
|
|
2433
|
+
* @param {Element | null} $root - HTML element to use for password reveal
|
|
2434
|
+
*/
|
|
2435
|
+
constructor($root) {
|
|
2436
|
+
super($root);
|
|
2437
|
+
const $input = this.$root.querySelector('.govuk-input');
|
|
2438
|
+
if (!$input || !($input instanceof HTMLInputElement)) {
|
|
2073
2439
|
return this;
|
|
2074
2440
|
}
|
|
2075
|
-
this
|
|
2076
|
-
this.
|
|
2441
|
+
this.$input = $input;
|
|
2442
|
+
this.$input.setAttribute('spellcheck', 'false');
|
|
2077
2443
|
this.createButton();
|
|
2078
2444
|
}
|
|
2079
2445
|
createButton() {
|
|
2080
|
-
this
|
|
2081
|
-
this
|
|
2082
|
-
this
|
|
2083
|
-
this.
|
|
2084
|
-
this.
|
|
2085
|
-
this.button
|
|
2086
|
-
this
|
|
2087
|
-
this.
|
|
2088
|
-
this.
|
|
2446
|
+
this.$group = document.createElement('div');
|
|
2447
|
+
this.$button = document.createElement('button');
|
|
2448
|
+
this.$button.setAttribute('type', 'button');
|
|
2449
|
+
this.$root.classList.add('moj-password-reveal');
|
|
2450
|
+
this.$group.classList.add('moj-password-reveal__wrapper');
|
|
2451
|
+
this.$button.classList.add('govuk-button', 'govuk-button--secondary', 'moj-password-reveal__button');
|
|
2452
|
+
this.$button.innerHTML = 'Show <span class="govuk-visually-hidden">password</span>';
|
|
2453
|
+
this.$button.addEventListener('click', this.onButtonClick.bind(this));
|
|
2454
|
+
this.$group.append(this.$input, this.$button);
|
|
2455
|
+
this.$root.append(this.$group);
|
|
2089
2456
|
}
|
|
2090
2457
|
onButtonClick() {
|
|
2091
|
-
if (this.
|
|
2092
|
-
this.
|
|
2093
|
-
this
|
|
2458
|
+
if (this.$input.type === 'password') {
|
|
2459
|
+
this.$input.type = 'text';
|
|
2460
|
+
this.$button.innerHTML = 'Hide <span class="govuk-visually-hidden">password</span>';
|
|
2094
2461
|
} else {
|
|
2095
|
-
this.
|
|
2096
|
-
this
|
|
2462
|
+
this.$input.type = 'password';
|
|
2463
|
+
this.$button.innerHTML = 'Show <span class="govuk-visually-hidden">password</span>';
|
|
2097
2464
|
}
|
|
2098
2465
|
}
|
|
2466
|
+
|
|
2467
|
+
/**
|
|
2468
|
+
* Name for the component used when initialising using data-module attributes.
|
|
2469
|
+
*/
|
|
2099
2470
|
}
|
|
2471
|
+
PasswordReveal.moduleName = 'moj-password-reveal';
|
|
2100
2472
|
|
|
2101
|
-
|
|
2473
|
+
/**
|
|
2474
|
+
* @augments {ConfigurableComponent<RichTextEditorConfig>}
|
|
2475
|
+
*/
|
|
2476
|
+
class RichTextEditor extends govukFrontend.ConfigurableComponent {
|
|
2102
2477
|
/**
|
|
2103
|
-
* @param {
|
|
2478
|
+
* @param {Element | null} $root - HTML element to use for rich text editor
|
|
2479
|
+
* @param {RichTextEditorConfig} config
|
|
2104
2480
|
*/
|
|
2105
|
-
constructor(
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
} = options;
|
|
2109
|
-
if (!textarea || !textarea.parentElement || !(textarea instanceof HTMLTextAreaElement) || !('contentEditable' in document.documentElement)) {
|
|
2481
|
+
constructor($root, config = {}) {
|
|
2482
|
+
super($root, config);
|
|
2483
|
+
if (!RichTextEditor.isSupported()) {
|
|
2110
2484
|
return this;
|
|
2111
2485
|
}
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
italic: false,
|
|
2115
|
-
underline: false,
|
|
2116
|
-
bullets: true,
|
|
2117
|
-
numbers: true
|
|
2118
|
-
};
|
|
2119
|
-
this.textarea = textarea;
|
|
2120
|
-
this.container = this.textarea.parentElement;
|
|
2121
|
-
this.options = options;
|
|
2122
|
-
if (this.container.hasAttribute('data-rich-text-editor-init')) {
|
|
2486
|
+
const $textarea = this.$root.querySelector('.govuk-textarea');
|
|
2487
|
+
if (!$textarea || !($textarea instanceof HTMLTextAreaElement)) {
|
|
2123
2488
|
return this;
|
|
2124
2489
|
}
|
|
2125
|
-
this
|
|
2490
|
+
this.$textarea = $textarea;
|
|
2126
2491
|
this.createToolbar();
|
|
2127
2492
|
this.hideDefault();
|
|
2128
2493
|
this.configureToolbar();
|
|
@@ -2132,34 +2497,42 @@
|
|
|
2132
2497
|
up: 38,
|
|
2133
2498
|
down: 40
|
|
2134
2499
|
};
|
|
2135
|
-
this
|
|
2136
|
-
this.
|
|
2137
|
-
this
|
|
2500
|
+
this.$content.addEventListener('input', this.onEditorInput.bind(this));
|
|
2501
|
+
this.$root.querySelector('label').addEventListener('click', this.onLabelClick.bind(this));
|
|
2502
|
+
this.$toolbar.addEventListener('keydown', this.onToolbarKeydown.bind(this));
|
|
2138
2503
|
}
|
|
2504
|
+
|
|
2505
|
+
/**
|
|
2506
|
+
* @param {KeyboardEvent} event - Click event
|
|
2507
|
+
*/
|
|
2139
2508
|
onToolbarKeydown(event) {
|
|
2140
|
-
let focusableButton;
|
|
2509
|
+
let $focusableButton;
|
|
2141
2510
|
switch (event.keyCode) {
|
|
2142
2511
|
case this.keys.right:
|
|
2143
2512
|
case this.keys.down:
|
|
2144
2513
|
{
|
|
2145
|
-
focusableButton = this
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
nextButton
|
|
2149
|
-
|
|
2150
|
-
|
|
2514
|
+
$focusableButton = this.$buttons.find(button => button.getAttribute('tabindex') === '0');
|
|
2515
|
+
if ($focusableButton) {
|
|
2516
|
+
const $nextButton = $focusableButton.nextElementSibling;
|
|
2517
|
+
if ($nextButton && $nextButton instanceof HTMLButtonElement) {
|
|
2518
|
+
$nextButton.focus();
|
|
2519
|
+
$focusableButton.setAttribute('tabindex', '-1');
|
|
2520
|
+
$nextButton.setAttribute('tabindex', '0');
|
|
2521
|
+
}
|
|
2151
2522
|
}
|
|
2152
2523
|
break;
|
|
2153
2524
|
}
|
|
2154
2525
|
case this.keys.left:
|
|
2155
2526
|
case this.keys.up:
|
|
2156
2527
|
{
|
|
2157
|
-
focusableButton = this
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
previousButton
|
|
2161
|
-
|
|
2162
|
-
|
|
2528
|
+
$focusableButton = this.$buttons.find(button => button.getAttribute('tabindex') === '0');
|
|
2529
|
+
if ($focusableButton) {
|
|
2530
|
+
const $previousButton = $focusableButton.previousElementSibling;
|
|
2531
|
+
if ($previousButton && $previousButton instanceof HTMLButtonElement) {
|
|
2532
|
+
$previousButton.focus();
|
|
2533
|
+
$focusableButton.setAttribute('tabindex', '-1');
|
|
2534
|
+
$previousButton.setAttribute('tabindex', '0');
|
|
2535
|
+
}
|
|
2163
2536
|
}
|
|
2164
2537
|
break;
|
|
2165
2538
|
}
|
|
@@ -2168,19 +2541,19 @@
|
|
|
2168
2541
|
getToolbarHtml() {
|
|
2169
2542
|
let html = '';
|
|
2170
2543
|
html += '<div class="moj-rich-text-editor__toolbar" role="toolbar">';
|
|
2171
|
-
if (this.
|
|
2544
|
+
if (this.config.toolbar.bold) {
|
|
2172
2545
|
html += '<button class="moj-rich-text-editor__toolbar-button moj-rich-text-editor__toolbar-button--bold" type="button" data-command="bold"><span class="govuk-visually-hidden">Bold</span></button>';
|
|
2173
2546
|
}
|
|
2174
|
-
if (this.
|
|
2547
|
+
if (this.config.toolbar.italic) {
|
|
2175
2548
|
html += '<button class="moj-rich-text-editor__toolbar-button moj-rich-text-editor__toolbar-button--italic" type="button" data-command="italic"><span class="govuk-visually-hidden">Italic</span></button>';
|
|
2176
2549
|
}
|
|
2177
|
-
if (this.
|
|
2550
|
+
if (this.config.toolbar.underline) {
|
|
2178
2551
|
html += '<button class="moj-rich-text-editor__toolbar-button moj-rich-text-editor__toolbar-button--underline" type="button" data-command="underline"><span class="govuk-visually-hidden">Underline</span></button>';
|
|
2179
2552
|
}
|
|
2180
|
-
if (this.
|
|
2553
|
+
if (this.config.toolbar.bullets) {
|
|
2181
2554
|
html += '<button class="moj-rich-text-editor__toolbar-button moj-rich-text-editor__toolbar-button--unordered-list" type="button" data-command="insertUnorderedList"><span class="govuk-visually-hidden">Unordered list</span></button>';
|
|
2182
2555
|
}
|
|
2183
|
-
if (this.
|
|
2556
|
+
if (this.config.toolbar.numbers) {
|
|
2184
2557
|
html += '<button class="moj-rich-text-editor__toolbar-button moj-rich-text-editor__toolbar-button--ordered-list" type="button" data-command="insertOrderedList"><span class="govuk-visually-hidden">Ordered list</span></button>';
|
|
2185
2558
|
}
|
|
2186
2559
|
html += '</div>';
|
|
@@ -2190,27 +2563,31 @@
|
|
|
2190
2563
|
return `${this.getToolbarHtml()}<div class="govuk-textarea moj-rich-text-editor__content" contenteditable="true" spellcheck="false"></div>`;
|
|
2191
2564
|
}
|
|
2192
2565
|
hideDefault() {
|
|
2193
|
-
this
|
|
2194
|
-
this
|
|
2195
|
-
this
|
|
2566
|
+
this.$textarea.classList.add('govuk-visually-hidden');
|
|
2567
|
+
this.$textarea.setAttribute('aria-hidden', 'true');
|
|
2568
|
+
this.$textarea.setAttribute('tabindex', '-1');
|
|
2196
2569
|
}
|
|
2197
2570
|
createToolbar() {
|
|
2198
|
-
this
|
|
2199
|
-
this
|
|
2200
|
-
this
|
|
2201
|
-
this.
|
|
2202
|
-
this
|
|
2203
|
-
this.
|
|
2204
|
-
this
|
|
2571
|
+
this.$toolbar = document.createElement('div');
|
|
2572
|
+
this.$toolbar.className = 'moj-rich-text-editor';
|
|
2573
|
+
this.$toolbar.innerHTML = this.getEnhancedHtml();
|
|
2574
|
+
this.$root.append(this.$toolbar);
|
|
2575
|
+
this.$content = /** @type {HTMLElement} */
|
|
2576
|
+
this.$root.querySelector('.moj-rich-text-editor__content');
|
|
2577
|
+
this.$content.innerHTML = this.$textarea.value;
|
|
2205
2578
|
}
|
|
2206
2579
|
configureToolbar() {
|
|
2207
|
-
this
|
|
2208
|
-
this.
|
|
2209
|
-
this
|
|
2210
|
-
button.setAttribute('tabindex', !index ? '0' : '-1');
|
|
2211
|
-
button.addEventListener('click', this.onButtonClick.bind(this));
|
|
2580
|
+
this.$buttons = Array.from(/** @type {NodeListOf<HTMLButtonElement>} */
|
|
2581
|
+
this.$root.querySelectorAll('.moj-rich-text-editor__toolbar-button'));
|
|
2582
|
+
this.$buttons.forEach(($button, index) => {
|
|
2583
|
+
$button.setAttribute('tabindex', !index ? '0' : '-1');
|
|
2584
|
+
$button.addEventListener('click', this.onButtonClick.bind(this));
|
|
2212
2585
|
});
|
|
2213
2586
|
}
|
|
2587
|
+
|
|
2588
|
+
/**
|
|
2589
|
+
* @param {MouseEvent} event - Click event
|
|
2590
|
+
*/
|
|
2214
2591
|
onButtonClick(event) {
|
|
2215
2592
|
if (!(event.currentTarget instanceof HTMLElement)) {
|
|
2216
2593
|
return;
|
|
@@ -2218,290 +2595,449 @@
|
|
|
2218
2595
|
document.execCommand(event.currentTarget.getAttribute('data-command'), false, undefined);
|
|
2219
2596
|
}
|
|
2220
2597
|
getContent() {
|
|
2221
|
-
return this
|
|
2598
|
+
return this.$content.innerHTML;
|
|
2222
2599
|
}
|
|
2223
2600
|
onEditorInput() {
|
|
2224
2601
|
this.updateTextarea();
|
|
2225
2602
|
}
|
|
2226
2603
|
updateTextarea() {
|
|
2227
2604
|
document.execCommand('defaultParagraphSeparator', false, 'p');
|
|
2228
|
-
this
|
|
2605
|
+
this.$textarea.value = this.getContent();
|
|
2229
2606
|
}
|
|
2607
|
+
|
|
2608
|
+
/**
|
|
2609
|
+
* @param {MouseEvent} event - Click event
|
|
2610
|
+
*/
|
|
2230
2611
|
onLabelClick(event) {
|
|
2231
2612
|
event.preventDefault();
|
|
2232
|
-
this
|
|
2613
|
+
this.$content.focus();
|
|
2614
|
+
}
|
|
2615
|
+
static isSupported() {
|
|
2616
|
+
return 'contentEditable' in document.documentElement;
|
|
2233
2617
|
}
|
|
2618
|
+
|
|
2619
|
+
/**
|
|
2620
|
+
* Name for the component used when initialising using data-module attributes.
|
|
2621
|
+
*/
|
|
2234
2622
|
}
|
|
2235
2623
|
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2624
|
+
/**
|
|
2625
|
+
* Rich text editor config
|
|
2626
|
+
*
|
|
2627
|
+
* @typedef {object} RichTextEditorConfig
|
|
2628
|
+
* @property {RichTextEditorToolbar} [toolbar] - Toolbar options
|
|
2629
|
+
*/
|
|
2630
|
+
|
|
2631
|
+
/**
|
|
2632
|
+
* Rich text editor toolbar options
|
|
2633
|
+
*
|
|
2634
|
+
* @typedef {object} RichTextEditorToolbar
|
|
2635
|
+
* @property {boolean} [bold] - Show the bold button
|
|
2636
|
+
* @property {boolean} [italic] - Show the italic button
|
|
2637
|
+
* @property {boolean} [underline] - Show the underline button
|
|
2638
|
+
* @property {boolean} [bullets] - Show the bullets button
|
|
2639
|
+
* @property {boolean} [numbers] - Show the numbers button
|
|
2640
|
+
*/
|
|
2641
|
+
|
|
2642
|
+
/**
|
|
2643
|
+
* @import { Schema } from 'govuk-frontend/dist/govuk/common/configuration.mjs'
|
|
2644
|
+
*/
|
|
2645
|
+
RichTextEditor.moduleName = 'moj-rich-text-editor';
|
|
2646
|
+
/**
|
|
2647
|
+
* Rich text editor config
|
|
2648
|
+
*
|
|
2649
|
+
* @type {RichTextEditorConfig}
|
|
2650
|
+
*/
|
|
2651
|
+
RichTextEditor.defaults = Object.freeze({
|
|
2652
|
+
toolbar: {
|
|
2653
|
+
bold: false,
|
|
2654
|
+
italic: false,
|
|
2655
|
+
underline: false,
|
|
2656
|
+
bullets: true,
|
|
2657
|
+
numbers: true
|
|
2658
|
+
}
|
|
2659
|
+
});
|
|
2660
|
+
/**
|
|
2661
|
+
* Rich text editor config schema
|
|
2662
|
+
*
|
|
2663
|
+
* @satisfies {Schema<RichTextEditorConfig>}
|
|
2664
|
+
*/
|
|
2665
|
+
RichTextEditor.schema = Object.freeze(/** @type {const} */{
|
|
2666
|
+
properties: {
|
|
2667
|
+
toolbar: {
|
|
2668
|
+
type: 'object'
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
});
|
|
2672
|
+
|
|
2673
|
+
/**
|
|
2674
|
+
* @augments {ConfigurableComponent<SearchToggleConfig>}
|
|
2675
|
+
*/
|
|
2676
|
+
class SearchToggle extends govukFrontend.ConfigurableComponent {
|
|
2677
|
+
/**
|
|
2678
|
+
* @param {Element | null} $root - HTML element to use for search toggle
|
|
2679
|
+
* @param {SearchToggleConfig} [config] - Search toggle config
|
|
2680
|
+
*/
|
|
2681
|
+
constructor($root, config = {}) {
|
|
2682
|
+
var _this$config$searchCo, _this$config$toggleBu;
|
|
2683
|
+
super($root, config);
|
|
2684
|
+
const $searchContainer = (_this$config$searchCo = this.config.searchContainer.element) != null ? _this$config$searchCo : this.$root.querySelector(this.config.searchContainer.selector);
|
|
2685
|
+
const $toggleButtonContainer = (_this$config$toggleBu = this.config.toggleButtonContainer.element) != null ? _this$config$toggleBu : this.$root.querySelector(this.config.toggleButtonContainer.selector);
|
|
2686
|
+
if (!$searchContainer || !$toggleButtonContainer || !($searchContainer instanceof HTMLElement) || !($toggleButtonContainer instanceof HTMLElement)) {
|
|
2242
2687
|
return this;
|
|
2243
2688
|
}
|
|
2244
|
-
this
|
|
2689
|
+
this.$searchContainer = $searchContainer;
|
|
2690
|
+
this.$toggleButtonContainer = $toggleButtonContainer;
|
|
2245
2691
|
const svg = '<svg viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="moj-search-toggle__button__icon"><path d="M7.433,12.5790048 C6.06762625,12.5808611 4.75763941,12.0392925 3.79217348,11.0738265 C2.82670755,10.1083606 2.28513891,8.79837375 2.28699522,7.433 C2.28513891,6.06762625 2.82670755,4.75763941 3.79217348,3.79217348 C4.75763941,2.82670755 6.06762625,2.28513891 7.433,2.28699522 C8.79837375,2.28513891 10.1083606,2.82670755 11.0738265,3.79217348 C12.0392925,4.75763941 12.5808611,6.06762625 12.5790048,7.433 C12.5808611,8.79837375 12.0392925,10.1083606 11.0738265,11.0738265 C10.1083606,12.0392925 8.79837375,12.5808611 7.433,12.5790048 L7.433,12.5790048 Z M14.293,12.579 L13.391,12.579 L13.071,12.269 C14.2300759,10.9245158 14.8671539,9.20813198 14.866,7.433 C14.866,3.32786745 11.5381325,-1.65045755e-15 7.433,-1.65045755e-15 C3.32786745,-1.65045755e-15 -1.65045755e-15,3.32786745 -1.65045755e-15,7.433 C-1.65045755e-15,11.5381325 3.32786745,14.866 7.433,14.866 C9.208604,14.8671159 10.9253982,14.2296624 12.27,13.07 L12.579,13.39 L12.579,14.294 L18.296,20 L20,18.296 L14.294,12.579 L14.293,12.579 Z"></path></svg>';
|
|
2246
|
-
this
|
|
2247
|
-
this
|
|
2248
|
-
this
|
|
2249
|
-
this
|
|
2250
|
-
this
|
|
2251
|
-
this
|
|
2252
|
-
this
|
|
2253
|
-
this
|
|
2692
|
+
this.$toggleButton = document.createElement('button');
|
|
2693
|
+
this.$toggleButton.setAttribute('class', 'moj-search-toggle__button');
|
|
2694
|
+
this.$toggleButton.setAttribute('type', 'button');
|
|
2695
|
+
this.$toggleButton.setAttribute('aria-haspopup', 'true');
|
|
2696
|
+
this.$toggleButton.setAttribute('aria-expanded', 'false');
|
|
2697
|
+
this.$toggleButton.innerHTML = `${this.config.toggleButton.text} ${svg}`;
|
|
2698
|
+
this.$toggleButton.addEventListener('click', this.onToggleButtonClick.bind(this));
|
|
2699
|
+
this.$toggleButtonContainer.append(this.$toggleButton);
|
|
2254
2700
|
document.addEventListener('click', this.onDocumentClick.bind(this));
|
|
2255
2701
|
document.addEventListener('focusin', this.onDocumentClick.bind(this));
|
|
2256
2702
|
}
|
|
2257
2703
|
showMenu() {
|
|
2258
|
-
this
|
|
2259
|
-
this.
|
|
2260
|
-
this.
|
|
2704
|
+
this.$toggleButton.setAttribute('aria-expanded', 'true');
|
|
2705
|
+
this.$searchContainer.classList.remove('moj-js-hidden');
|
|
2706
|
+
this.$searchContainer.querySelector('input').focus();
|
|
2261
2707
|
}
|
|
2262
2708
|
hideMenu() {
|
|
2263
|
-
this.
|
|
2264
|
-
this
|
|
2709
|
+
this.$searchContainer.classList.add('moj-js-hidden');
|
|
2710
|
+
this.$toggleButton.setAttribute('aria-expanded', 'false');
|
|
2265
2711
|
}
|
|
2266
2712
|
onToggleButtonClick() {
|
|
2267
|
-
if (this
|
|
2713
|
+
if (this.$toggleButton.getAttribute('aria-expanded') === 'false') {
|
|
2268
2714
|
this.showMenu();
|
|
2269
2715
|
} else {
|
|
2270
2716
|
this.hideMenu();
|
|
2271
2717
|
}
|
|
2272
2718
|
}
|
|
2719
|
+
|
|
2720
|
+
/**
|
|
2721
|
+
* @param {MouseEvent | FocusEvent} event
|
|
2722
|
+
*/
|
|
2273
2723
|
onDocumentClick(event) {
|
|
2274
|
-
if (!this
|
|
2724
|
+
if (event.target instanceof Node && !this.$toggleButtonContainer.contains(event.target) && !this.$searchContainer.contains(event.target)) {
|
|
2275
2725
|
this.hideMenu();
|
|
2276
2726
|
}
|
|
2277
2727
|
}
|
|
2728
|
+
|
|
2729
|
+
/**
|
|
2730
|
+
* Name for the component used when initialising using data-module attributes.
|
|
2731
|
+
*/
|
|
2278
2732
|
}
|
|
2279
2733
|
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2734
|
+
/**
|
|
2735
|
+
* @typedef {object} SearchToggleConfig
|
|
2736
|
+
* @property {object} [searchContainer] - Search config
|
|
2737
|
+
* @property {string} [searchContainer.selector] - Selector for search container
|
|
2738
|
+
* @property {Element | null} [searchContainer.element] - HTML element for search container
|
|
2739
|
+
* @property {object} [toggleButton] - Toggle button config
|
|
2740
|
+
* @property {string} [toggleButton.text] - Text for toggle button
|
|
2741
|
+
* @property {object} [toggleButtonContainer] - Toggle button container config
|
|
2742
|
+
* @property {string} [toggleButtonContainer.selector] - Selector for toggle button container
|
|
2743
|
+
* @property {Element | null} [toggleButtonContainer.element] - HTML element for toggle button container
|
|
2744
|
+
*/
|
|
2745
|
+
|
|
2746
|
+
/**
|
|
2747
|
+
* @import { Schema } from 'govuk-frontend/dist/govuk/common/configuration.mjs'
|
|
2748
|
+
*/
|
|
2749
|
+
SearchToggle.moduleName = 'moj-search-toggle';
|
|
2750
|
+
/**
|
|
2751
|
+
* Search toggle config
|
|
2752
|
+
*
|
|
2753
|
+
* @type {SearchToggleConfig}
|
|
2754
|
+
*/
|
|
2755
|
+
SearchToggle.defaults = Object.freeze({
|
|
2756
|
+
searchContainer: {
|
|
2757
|
+
selector: '.moj-search'
|
|
2758
|
+
},
|
|
2759
|
+
toggleButton: {
|
|
2760
|
+
text: 'Search'
|
|
2761
|
+
},
|
|
2762
|
+
toggleButtonContainer: {
|
|
2763
|
+
selector: '.moj-search-toggle__toggle'
|
|
2764
|
+
}
|
|
2765
|
+
});
|
|
2766
|
+
/**
|
|
2767
|
+
* Search toggle config schema
|
|
2768
|
+
*
|
|
2769
|
+
* @satisfies {Schema<SearchToggleConfig>}
|
|
2770
|
+
*/
|
|
2771
|
+
SearchToggle.schema = Object.freeze(/** @type {const} */{
|
|
2772
|
+
properties: {
|
|
2773
|
+
searchContainer: {
|
|
2774
|
+
type: 'object'
|
|
2775
|
+
},
|
|
2776
|
+
toggleButton: {
|
|
2777
|
+
type: 'object'
|
|
2778
|
+
},
|
|
2779
|
+
toggleButtonContainer: {
|
|
2780
|
+
type: 'object'
|
|
2287
2781
|
}
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2782
|
+
}
|
|
2783
|
+
});
|
|
2784
|
+
|
|
2785
|
+
/**
|
|
2786
|
+
* @augments {ConfigurableComponent<SortableTableConfig>}
|
|
2787
|
+
*/
|
|
2788
|
+
class SortableTable extends govukFrontend.ConfigurableComponent {
|
|
2789
|
+
/**
|
|
2790
|
+
* @param {Element | null} $root - HTML element to use for sortable table
|
|
2791
|
+
* @param {SortableTableConfig} [config] - Sortable table config
|
|
2792
|
+
*/
|
|
2793
|
+
constructor($root, config = {}) {
|
|
2794
|
+
super($root, config);
|
|
2795
|
+
const $head = $root == null ? void 0 : $root.querySelector('thead');
|
|
2796
|
+
const $body = $root == null ? void 0 : $root.querySelector('tbody');
|
|
2797
|
+
if (!$head || !$body) {
|
|
2292
2798
|
return this;
|
|
2293
2799
|
}
|
|
2294
|
-
this
|
|
2295
|
-
this
|
|
2296
|
-
this.
|
|
2800
|
+
this.$head = $head;
|
|
2801
|
+
this.$body = $body;
|
|
2802
|
+
this.$caption = this.$root.querySelector('caption');
|
|
2803
|
+
this.$upArrow = `<svg width="22" height="22" focusable="false" aria-hidden="true" role="img" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2804
|
+
<path d="M6.5625 15.5L11 6.63125L15.4375 15.5H6.5625Z" fill="currentColor"/>
|
|
2805
|
+
</svg>`;
|
|
2806
|
+
this.$downArrow = `<svg width="22" height="22" focusable="false" aria-hidden="true" role="img" vviewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2807
|
+
<path d="M15.4375 7L11 15.8687L6.5625 7L15.4375 7Z" fill="currentColor"/>
|
|
2808
|
+
</svg>`;
|
|
2809
|
+
this.$upDownArrow = `<svg width="22" height="22" focusable="false" aria-hidden="true" role="img" vviewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2810
|
+
<path d="M8.1875 9.5L10.9609 3.95703L13.7344 9.5H8.1875Z" fill="currentColor"/>
|
|
2811
|
+
<path d="M13.7344 12.0781L10.9609 17.6211L8.1875 12.0781H13.7344Z" fill="currentColor"/>
|
|
2812
|
+
</svg>`;
|
|
2813
|
+
this.$headings = this.$head ? Array.from(this.$head.querySelectorAll('th')) : [];
|
|
2297
2814
|
this.createHeadingButtons();
|
|
2815
|
+
this.updateCaption();
|
|
2816
|
+
this.updateDirectionIndicators();
|
|
2298
2817
|
this.createStatusBox();
|
|
2299
2818
|
this.initialiseSortedColumn();
|
|
2300
|
-
this
|
|
2301
|
-
}
|
|
2302
|
-
setupOptions(params) {
|
|
2303
|
-
params = params || {};
|
|
2304
|
-
this.statusMessage = params.statusMessage || 'Sort by %heading% (%direction%)';
|
|
2305
|
-
this.ascendingText = params.ascendingText || 'ascending';
|
|
2306
|
-
this.descendingText = params.descendingText || 'descending';
|
|
2819
|
+
this.$head.addEventListener('click', this.onSortButtonClick.bind(this));
|
|
2307
2820
|
}
|
|
2308
2821
|
createHeadingButtons() {
|
|
2309
|
-
for (const heading of this
|
|
2310
|
-
if (heading.hasAttribute('aria-sort')) {
|
|
2311
|
-
this.createHeadingButton(heading);
|
|
2822
|
+
for (const $heading of this.$headings) {
|
|
2823
|
+
if ($heading.hasAttribute('aria-sort')) {
|
|
2824
|
+
this.createHeadingButton($heading);
|
|
2312
2825
|
}
|
|
2313
2826
|
}
|
|
2314
2827
|
}
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2828
|
+
|
|
2829
|
+
/**
|
|
2830
|
+
* @param {HTMLTableCellElement} $heading
|
|
2831
|
+
*/
|
|
2832
|
+
createHeadingButton($heading) {
|
|
2833
|
+
const index = this.$headings.indexOf($heading);
|
|
2834
|
+
const $button = document.createElement('button');
|
|
2835
|
+
$button.setAttribute('type', 'button');
|
|
2836
|
+
$button.setAttribute('data-index', `${index}`);
|
|
2837
|
+
$button.textContent = $heading.textContent;
|
|
2838
|
+
$heading.textContent = '';
|
|
2839
|
+
$heading.appendChild($button);
|
|
2323
2840
|
}
|
|
2324
2841
|
createStatusBox() {
|
|
2325
|
-
this
|
|
2326
|
-
this
|
|
2327
|
-
this
|
|
2328
|
-
this
|
|
2329
|
-
this
|
|
2330
|
-
this.
|
|
2842
|
+
this.$status = document.createElement('div');
|
|
2843
|
+
this.$status.setAttribute('aria-atomic', 'true');
|
|
2844
|
+
this.$status.setAttribute('aria-live', 'polite');
|
|
2845
|
+
this.$status.setAttribute('class', 'govuk-visually-hidden');
|
|
2846
|
+
this.$status.setAttribute('role', 'status');
|
|
2847
|
+
this.$root.insertAdjacentElement('afterend', this.$status);
|
|
2331
2848
|
}
|
|
2332
2849
|
initialiseSortedColumn() {
|
|
2333
|
-
var
|
|
2334
|
-
const rows = this.getTableRowsArray();
|
|
2335
|
-
const heading = this.
|
|
2336
|
-
const sortButton = heading == null ? void 0 : heading.querySelector('button');
|
|
2337
|
-
const sortDirection = heading == null ? void 0 : heading.getAttribute('aria-sort');
|
|
2338
|
-
const columnNumber = Number.parseInt((
|
|
2339
|
-
if (
|
|
2850
|
+
var _$sortButton$getAttri;
|
|
2851
|
+
const $rows = this.getTableRowsArray();
|
|
2852
|
+
const $heading = this.$root.querySelector('th[aria-sort="ascending"], th[aria-sort="descending"]');
|
|
2853
|
+
const $sortButton = $heading == null ? void 0 : $heading.querySelector('button');
|
|
2854
|
+
const sortDirection = $heading == null ? void 0 : $heading.getAttribute('aria-sort');
|
|
2855
|
+
const columnNumber = Number.parseInt((_$sortButton$getAttri = $sortButton == null ? void 0 : $sortButton.getAttribute('data-index')) != null ? _$sortButton$getAttri : '0', 10);
|
|
2856
|
+
if (!$heading || !$sortButton || !(sortDirection === 'ascending' || sortDirection === 'descending')) {
|
|
2340
2857
|
return;
|
|
2341
2858
|
}
|
|
2342
|
-
const sortedRows = this.sort(rows, columnNumber, sortDirection);
|
|
2343
|
-
this.addRows(sortedRows);
|
|
2859
|
+
const $sortedRows = this.sort($rows, columnNumber, sortDirection);
|
|
2860
|
+
this.addRows($sortedRows);
|
|
2344
2861
|
}
|
|
2862
|
+
|
|
2863
|
+
/**
|
|
2864
|
+
* @param {MouseEvent} event - Click event
|
|
2865
|
+
*/
|
|
2345
2866
|
onSortButtonClick(event) {
|
|
2346
|
-
var
|
|
2347
|
-
const
|
|
2348
|
-
|
|
2867
|
+
var _$button$getAttribute;
|
|
2868
|
+
const $target = /** @type {HTMLElement} */event.target;
|
|
2869
|
+
const $button = $target.closest('button');
|
|
2870
|
+
if (!$button || !($button instanceof HTMLButtonElement) || !$button.parentElement) {
|
|
2349
2871
|
return;
|
|
2350
2872
|
}
|
|
2351
|
-
const heading = button.parentElement;
|
|
2352
|
-
const sortDirection = heading.getAttribute('aria-sort');
|
|
2353
|
-
const columnNumber = Number.parseInt((
|
|
2873
|
+
const $heading = $button.parentElement;
|
|
2874
|
+
const sortDirection = $heading.getAttribute('aria-sort');
|
|
2875
|
+
const columnNumber = Number.parseInt((_$button$getAttribute = $button == null ? void 0 : $button.getAttribute('data-index')) != null ? _$button$getAttribute : '0', 10);
|
|
2354
2876
|
const newSortDirection = sortDirection === 'none' || sortDirection === 'descending' ? 'ascending' : 'descending';
|
|
2355
|
-
const rows = this.getTableRowsArray();
|
|
2356
|
-
const sortedRows = this.sort(rows, columnNumber, newSortDirection);
|
|
2357
|
-
this.addRows(sortedRows);
|
|
2877
|
+
const $rows = this.getTableRowsArray();
|
|
2878
|
+
const $sortedRows = this.sort($rows, columnNumber, newSortDirection);
|
|
2879
|
+
this.addRows($sortedRows);
|
|
2358
2880
|
this.removeButtonStates();
|
|
2359
|
-
this.updateButtonState(button, newSortDirection);
|
|
2881
|
+
this.updateButtonState($button, newSortDirection);
|
|
2882
|
+
this.updateDirectionIndicators();
|
|
2360
2883
|
}
|
|
2361
|
-
|
|
2884
|
+
updateCaption() {
|
|
2885
|
+
if (!this.$caption) {
|
|
2886
|
+
return;
|
|
2887
|
+
}
|
|
2888
|
+
let assistiveText = this.$caption.querySelector('.govuk-visually-hidden');
|
|
2889
|
+
if (assistiveText) {
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
assistiveText = document.createElement('span');
|
|
2893
|
+
assistiveText.classList.add('govuk-visually-hidden');
|
|
2894
|
+
assistiveText.textContent = ' (column headers with buttons are sortable).';
|
|
2895
|
+
this.$caption.appendChild(assistiveText);
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
/**
|
|
2899
|
+
* @param {HTMLButtonElement} $button
|
|
2900
|
+
* @param {string} direction
|
|
2901
|
+
*/
|
|
2902
|
+
updateButtonState($button, direction) {
|
|
2362
2903
|
if (!(direction === 'ascending' || direction === 'descending')) {
|
|
2363
2904
|
return;
|
|
2364
2905
|
}
|
|
2365
|
-
button.parentElement.setAttribute('aria-sort', direction);
|
|
2366
|
-
let message = this.statusMessage;
|
|
2367
|
-
message = message.replace(/%heading%/, button.textContent);
|
|
2368
|
-
message = message.replace(/%direction%/, this[`${direction}Text`]);
|
|
2369
|
-
this
|
|
2906
|
+
$button.parentElement.setAttribute('aria-sort', direction);
|
|
2907
|
+
let message = this.config.statusMessage;
|
|
2908
|
+
message = message.replace(/%heading%/, $button.textContent);
|
|
2909
|
+
message = message.replace(/%direction%/, this.config[`${direction}Text`]);
|
|
2910
|
+
this.$status.textContent = message;
|
|
2911
|
+
}
|
|
2912
|
+
updateDirectionIndicators() {
|
|
2913
|
+
for (const $heading of this.$headings) {
|
|
2914
|
+
const $button = /** @type {HTMLButtonElement} */
|
|
2915
|
+
$heading.querySelector('button');
|
|
2916
|
+
if ($heading.hasAttribute('aria-sort') && $button) {
|
|
2917
|
+
var _$button$querySelecto;
|
|
2918
|
+
const direction = $heading.getAttribute('aria-sort');
|
|
2919
|
+
(_$button$querySelecto = $button.querySelector('svg')) == null || _$button$querySelecto.remove();
|
|
2920
|
+
switch (direction) {
|
|
2921
|
+
case 'ascending':
|
|
2922
|
+
$button.insertAdjacentHTML('beforeend', this.$upArrow);
|
|
2923
|
+
break;
|
|
2924
|
+
case 'descending':
|
|
2925
|
+
$button.insertAdjacentHTML('beforeend', this.$downArrow);
|
|
2926
|
+
break;
|
|
2927
|
+
default:
|
|
2928
|
+
$button.insertAdjacentHTML('beforeend', this.$upDownArrow);
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2370
2932
|
}
|
|
2371
2933
|
removeButtonStates() {
|
|
2372
|
-
for (const heading of this
|
|
2373
|
-
heading.setAttribute('aria-sort', 'none');
|
|
2934
|
+
for (const $heading of this.$headings) {
|
|
2935
|
+
$heading.setAttribute('aria-sort', 'none');
|
|
2374
2936
|
}
|
|
2375
2937
|
}
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2938
|
+
|
|
2939
|
+
/**
|
|
2940
|
+
* @param {HTMLTableRowElement[]} $rows
|
|
2941
|
+
*/
|
|
2942
|
+
addRows($rows) {
|
|
2943
|
+
for (const $row of $rows) {
|
|
2944
|
+
this.$body.append($row);
|
|
2379
2945
|
}
|
|
2380
2946
|
}
|
|
2381
2947
|
getTableRowsArray() {
|
|
2382
|
-
return Array.from(this
|
|
2948
|
+
return Array.from(this.$body.querySelectorAll('tr'));
|
|
2383
2949
|
}
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2950
|
+
|
|
2951
|
+
/**
|
|
2952
|
+
* @param {HTMLTableRowElement[]} $rows
|
|
2953
|
+
* @param {number} columnNumber
|
|
2954
|
+
* @param {string} sortDirection
|
|
2955
|
+
*/
|
|
2956
|
+
sort($rows, columnNumber, sortDirection) {
|
|
2957
|
+
return $rows.sort(($rowA, $rowB) => {
|
|
2958
|
+
const $tdA = $rowA.querySelectorAll('td, th')[columnNumber];
|
|
2959
|
+
const $tdB = $rowB.querySelectorAll('td, th')[columnNumber];
|
|
2960
|
+
if (!$tdA || !$tdB || !($tdA instanceof HTMLElement) || !($tdB instanceof HTMLElement)) {
|
|
2389
2961
|
return 0;
|
|
2390
2962
|
}
|
|
2391
|
-
const valueA = sortDirection === 'ascending' ? this.getCellValue(tdA) : this.getCellValue(tdB);
|
|
2392
|
-
const valueB = sortDirection === 'ascending' ? this.getCellValue(tdB) : this.getCellValue(tdA);
|
|
2963
|
+
const valueA = sortDirection === 'ascending' ? this.getCellValue($tdA) : this.getCellValue($tdB);
|
|
2964
|
+
const valueB = sortDirection === 'ascending' ? this.getCellValue($tdB) : this.getCellValue($tdA);
|
|
2393
2965
|
return !(typeof valueA === 'number' && typeof valueB === 'number') ? valueA.toString().localeCompare(valueB.toString()) : valueA - valueB;
|
|
2394
2966
|
});
|
|
2395
2967
|
}
|
|
2396
|
-
|
|
2397
|
-
|
|
2968
|
+
|
|
2969
|
+
/**
|
|
2970
|
+
* @param {HTMLElement} $cell
|
|
2971
|
+
*/
|
|
2972
|
+
getCellValue($cell) {
|
|
2973
|
+
const val = $cell.getAttribute('data-sort-value') || $cell.innerHTML;
|
|
2398
2974
|
const valAsNumber = Number(val);
|
|
2399
2975
|
return Number.isFinite(valAsNumber) ? valAsNumber // Exclude invalid numbers, infinity etc
|
|
2400
2976
|
: val;
|
|
2401
2977
|
}
|
|
2402
|
-
}
|
|
2403
2978
|
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2979
|
+
/**
|
|
2980
|
+
* Name for the component used when initialising using data-module attributes.
|
|
2981
|
+
*/
|
|
2982
|
+
}
|
|
2407
2983
|
|
|
2984
|
+
/**
|
|
2985
|
+
* Sortable table config
|
|
2986
|
+
*
|
|
2987
|
+
* @typedef {object} SortableTableConfig
|
|
2988
|
+
* @property {string} [statusMessage] - Status message
|
|
2989
|
+
* @property {string} [ascendingText] - Ascending text
|
|
2990
|
+
* @property {string} [descendingText] - Descending text
|
|
2991
|
+
*/
|
|
2408
2992
|
|
|
2409
2993
|
/**
|
|
2410
|
-
* @
|
|
2994
|
+
* @import { Schema } from 'govuk-frontend/dist/govuk/common/configuration.mjs'
|
|
2411
2995
|
*/
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
});
|
|
2439
|
-
const $richTextEditors = scope.querySelectorAll('[data-module="moj-rich-text-editor"]');
|
|
2440
|
-
$richTextEditors.forEach($richTextEditor => {
|
|
2441
|
-
const options = {
|
|
2442
|
-
textarea: $richTextEditor
|
|
2443
|
-
};
|
|
2444
|
-
const toolbarAttr = $richTextEditor.getAttribute('data-moj-rich-text-editor-toolbar');
|
|
2445
|
-
if (toolbarAttr) {
|
|
2446
|
-
const toolbar = toolbarAttr.split(',');
|
|
2447
|
-
options.toolbar = {};
|
|
2448
|
-
for (const option of toolbar) {
|
|
2449
|
-
if (option === 'bold' || option === 'italic' || option === 'underline' || option === 'bullets' || option === 'numbers') {
|
|
2450
|
-
options.toolbar[option] = true;
|
|
2451
|
-
}
|
|
2452
|
-
}
|
|
2996
|
+
SortableTable.moduleName = 'moj-sortable-table';
|
|
2997
|
+
/**
|
|
2998
|
+
* Sortable table config
|
|
2999
|
+
*
|
|
3000
|
+
* @type {SortableTableConfig}
|
|
3001
|
+
*/
|
|
3002
|
+
SortableTable.defaults = Object.freeze({
|
|
3003
|
+
statusMessage: 'Sort by %heading% (%direction%)',
|
|
3004
|
+
ascendingText: 'ascending',
|
|
3005
|
+
descendingText: 'descending'
|
|
3006
|
+
});
|
|
3007
|
+
/**
|
|
3008
|
+
* Sortable table config schema
|
|
3009
|
+
*
|
|
3010
|
+
* @satisfies {Schema<SortableTableConfig>}
|
|
3011
|
+
*/
|
|
3012
|
+
SortableTable.schema = Object.freeze(/** @type {const} */{
|
|
3013
|
+
properties: {
|
|
3014
|
+
statusMessage: {
|
|
3015
|
+
type: 'string'
|
|
3016
|
+
},
|
|
3017
|
+
ascendingText: {
|
|
3018
|
+
type: 'string'
|
|
3019
|
+
},
|
|
3020
|
+
descendingText: {
|
|
3021
|
+
type: 'string'
|
|
2453
3022
|
}
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
const $searchToggles = scope.querySelectorAll('[data-module="moj-search-toggle"]');
|
|
2457
|
-
$searchToggles.forEach($searchToggle => {
|
|
2458
|
-
new SearchToggle({
|
|
2459
|
-
toggleButton: {
|
|
2460
|
-
container: $searchToggle.querySelector('.moj-search-toggle__toggle'),
|
|
2461
|
-
text: $searchToggle.getAttribute('data-moj-search-toggle-text')
|
|
2462
|
-
},
|
|
2463
|
-
search: {
|
|
2464
|
-
container: $searchToggle.querySelector('.moj-search')
|
|
2465
|
-
}
|
|
2466
|
-
});
|
|
2467
|
-
});
|
|
2468
|
-
const $sortableTables = scope.querySelectorAll('[data-module="moj-sortable-table"]');
|
|
2469
|
-
$sortableTables.forEach($table => {
|
|
2470
|
-
new SortableTable({
|
|
2471
|
-
table: $table
|
|
2472
|
-
});
|
|
2473
|
-
});
|
|
2474
|
-
const $datePickers = scope.querySelectorAll('[data-module="moj-date-picker"]');
|
|
2475
|
-
$datePickers.forEach($datePicker => {
|
|
2476
|
-
new DatePicker($datePicker);
|
|
2477
|
-
});
|
|
2478
|
-
const $buttonMenus = scope.querySelectorAll('[data-module="moj-button-menu"]');
|
|
2479
|
-
$buttonMenus.forEach($buttonmenu => {
|
|
2480
|
-
new ButtonMenu($buttonmenu);
|
|
2481
|
-
});
|
|
2482
|
-
const $alerts = scope.querySelectorAll('[data-module="moj-alert"]');
|
|
2483
|
-
$alerts.forEach($alert => {
|
|
2484
|
-
new Alert($alert);
|
|
2485
|
-
});
|
|
2486
|
-
}
|
|
3023
|
+
}
|
|
3024
|
+
});
|
|
2487
3025
|
|
|
2488
3026
|
/**
|
|
2489
|
-
* @
|
|
2490
|
-
* @property {Element} [scope=document] - Scope to query for components
|
|
3027
|
+
* @param {Config} [config]
|
|
2491
3028
|
*/
|
|
3029
|
+
function initAll(config) {
|
|
3030
|
+
for (const Component of [AddAnother, Alert, ButtonMenu, DatePicker, MultiSelect, PasswordReveal, RichTextEditor, SearchToggle, SortableTable]) {
|
|
3031
|
+
govukFrontend.createAll(Component, undefined, config);
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
2492
3034
|
|
|
2493
3035
|
/**
|
|
2494
|
-
*
|
|
2495
|
-
*
|
|
2496
|
-
* @typedef {object} Schema
|
|
2497
|
-
* @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
|
|
3036
|
+
* @typedef {Parameters<typeof GOVUKFrontend.initAll>[0]} Config
|
|
2498
3037
|
*/
|
|
2499
3038
|
|
|
2500
3039
|
/**
|
|
2501
|
-
*
|
|
2502
|
-
*
|
|
2503
|
-
* @typedef {object} SchemaProperty
|
|
2504
|
-
* @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
|
|
3040
|
+
* @import * as GOVUKFrontend from 'govuk-frontend'
|
|
2505
3041
|
*/
|
|
2506
3042
|
|
|
2507
3043
|
exports.AddAnother = AddAnother;
|