@ministryofjustice/frontend 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -10
- package/govuk-prototype-kit.config.json +5 -16
- package/moj/all.jquery.min.js +77 -3
- package/moj/all.js +2022 -1444
- package/moj/all.scss +2 -0
- package/moj/all.spec.js +15 -13
- package/moj/components/_all.scss +1 -0
- package/moj/components/action-bar/_action-bar.scss +4 -6
- package/moj/components/add-another/_add-another.scss +9 -7
- package/moj/components/add-another/add-another.js +90 -69
- package/moj/components/add-another/add-another.spec.js +165 -0
- package/moj/components/alert/README.md +0 -0
- package/moj/components/alert/_alert.scss +142 -0
- package/moj/components/alert/alert.js +247 -0
- package/moj/components/alert/alert.spec.helper.js +67 -0
- package/moj/components/alert/alert.spec.js +229 -0
- package/moj/components/alert/macro.njk +3 -0
- package/moj/components/alert/template.njk +83 -0
- package/moj/components/badge/_badge.scss +3 -4
- package/moj/components/banner/_banner.scss +5 -10
- package/moj/components/button-menu/_button-menu.scss +10 -9
- package/moj/components/button-menu/button-menu.js +139 -136
- package/moj/components/button-menu/button-menu.spec.js +295 -296
- package/moj/components/cookie-banner/_cookie-banner.scss +6 -5
- package/moj/components/currency-input/_currency-input.scss +4 -4
- package/moj/components/date-picker/README.md +14 -17
- package/moj/components/date-picker/_date-picker.scss +122 -106
- package/moj/components/date-picker/date-picker.js +473 -471
- package/moj/components/date-picker/date-picker.spec.js +962 -914
- package/moj/components/filter/README.md +1 -1
- package/moj/components/filter/_filter.scss +53 -75
- package/moj/components/filter-toggle-button/filter-toggle-button.js +71 -67
- package/moj/components/filter-toggle-button/filter-toggle-button.spec.js +203 -205
- package/moj/components/form-validator/form-validator.js +117 -109
- package/moj/components/header/_header.scss +17 -19
- package/moj/components/identity-bar/_identity-bar.scss +5 -5
- package/moj/components/interruption-card/_interruption-card.scss +9 -2
- package/moj/components/messages/_messages.scss +12 -19
- package/moj/components/multi-file-upload/README.md +1 -1
- package/moj/components/multi-file-upload/_multi-file-upload.scss +34 -30
- package/moj/components/multi-file-upload/multi-file-upload.js +188 -152
- package/moj/components/multi-file-upload/multi-file-upload.spec.js +510 -0
- package/moj/components/multi-select/_multi-select.scss +4 -3
- package/moj/components/multi-select/multi-select.js +55 -50
- package/moj/components/multi-select/multi-select.spec.js +128 -0
- package/moj/components/notification-badge/_notification-badge.scss +12 -12
- package/moj/components/organisation-switcher/_organisation-switcher.scss +1 -1
- package/moj/components/page-header-actions/_page-header-actions.scss +3 -2
- package/moj/components/pagination/_pagination.scss +26 -31
- package/moj/components/password-reveal/_password-reveal.scss +1 -2
- package/moj/components/password-reveal/password-reveal.js +22 -21
- package/moj/components/password-reveal/password-reveal.spec.js +39 -37
- package/moj/components/primary-navigation/_primary-navigation.scss +26 -29
- package/moj/components/progress-bar/_progress-bar.scss +21 -26
- package/moj/components/rich-text-editor/_rich-text-editor.scss +17 -16
- package/moj/components/rich-text-editor/rich-text-editor.js +117 -103
- package/moj/components/search/_search.scss +6 -4
- package/moj/components/search-toggle/search-toggle.js +29 -30
- package/moj/components/search-toggle/search-toggle.scss +21 -15
- package/moj/components/search-toggle/search-toggle.spec.js +129 -0
- package/moj/components/side-navigation/_side-navigation.scss +12 -21
- package/moj/components/sortable-table/_sortable-table.scss +25 -23
- package/moj/components/sortable-table/sortable-table.js +139 -117
- package/moj/components/sortable-table/sortable-table.spec.js +362 -0
- package/moj/components/sub-navigation/_sub-navigation.scss +24 -28
- package/moj/components/tag/_tag.scss +8 -9
- package/moj/components/task-list/_task-list.scss +8 -7
- package/moj/components/ticket-panel/_ticket-panel.scss +14 -6
- package/moj/components/timeline/_timeline.scss +18 -20
- package/moj/filters/all.js +28 -30
- package/moj/filters/prototype-kit-13-filters.js +2 -1
- package/moj/helpers/_all.scss +1 -0
- package/moj/helpers/_hidden.scss +1 -1
- package/moj/helpers/_links.scss +20 -0
- package/moj/helpers.js +160 -31
- package/moj/helpers.spec.js +235 -0
- package/moj/init.js +2 -2
- package/moj/moj-frontend.min.css +2 -2
- package/moj/moj-frontend.min.js +77 -3
- package/moj/namespace.js +2 -1
- package/moj/objects/_filter-layout.scss +11 -10
- package/moj/objects/_scrollable-pane.scss +11 -14
- package/moj/settings/_colours.scss +5 -0
- package/moj/settings/_measurements.scss +0 -2
- package/moj/utilities/_hidden.scss +3 -3
- package/moj/utilities/_width-container.scss +1 -1
- package/package.json +1 -1
package/moj/all.js
CHANGED
|
@@ -7,103 +7,250 @@
|
|
|
7
7
|
root.MOJFrontend = factory();
|
|
8
8
|
}
|
|
9
9
|
}(this, function() {
|
|
10
|
-
|
|
11
|
-
MOJFrontend
|
|
12
|
-
|
|
10
|
+
// eslint-disable-next-line no-unused-vars
|
|
11
|
+
const MOJFrontend = {}
|
|
12
|
+
|
|
13
|
+
MOJFrontend.removeAttributeValue = function (el, attr, value) {
|
|
14
|
+
let re, m
|
|
13
15
|
if (el.getAttribute(attr)) {
|
|
14
|
-
if (el.getAttribute(attr)
|
|
15
|
-
el.removeAttribute(attr)
|
|
16
|
+
if (el.getAttribute(attr) === value) {
|
|
17
|
+
el.removeAttribute(attr)
|
|
16
18
|
} else {
|
|
17
|
-
re = new RegExp(
|
|
18
|
-
m = el.getAttribute(attr).match(re)
|
|
19
|
-
if (m && m.length
|
|
20
|
-
el.setAttribute(
|
|
19
|
+
re = new RegExp(`(^|\\s)${value}(\\s|$)`)
|
|
20
|
+
m = el.getAttribute(attr).match(re)
|
|
21
|
+
if (m && m.length === 3) {
|
|
22
|
+
el.setAttribute(
|
|
23
|
+
attr,
|
|
24
|
+
el.getAttribute(attr).replace(re, m[1] && m[2] ? ' ' : '')
|
|
25
|
+
)
|
|
21
26
|
}
|
|
22
27
|
}
|
|
23
28
|
}
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
MOJFrontend.addAttributeValue = function(el, attr, value) {
|
|
27
|
-
|
|
31
|
+
MOJFrontend.addAttributeValue = function (el, attr, value) {
|
|
32
|
+
let re
|
|
28
33
|
if (!el.getAttribute(attr)) {
|
|
29
|
-
el.setAttribute(attr, value)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
re = new RegExp('(^|\\s)' + value + '(\\s|$)');
|
|
34
|
+
el.setAttribute(attr, value)
|
|
35
|
+
} else {
|
|
36
|
+
re = new RegExp(`(^|\\s)${value}(\\s|$)`)
|
|
33
37
|
if (!re.test(el.getAttribute(attr))) {
|
|
34
|
-
el.setAttribute(attr, el.getAttribute(attr)
|
|
38
|
+
el.setAttribute(attr, `${el.getAttribute(attr)} ${value}`)
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
|
-
}
|
|
41
|
+
}
|
|
38
42
|
|
|
39
|
-
MOJFrontend.dragAndDropSupported = function() {
|
|
40
|
-
|
|
41
|
-
return typeof div.ondrop
|
|
42
|
-
}
|
|
43
|
+
MOJFrontend.dragAndDropSupported = function () {
|
|
44
|
+
const div = document.createElement('div')
|
|
45
|
+
return typeof div.ondrop !== 'undefined'
|
|
46
|
+
}
|
|
43
47
|
|
|
44
|
-
MOJFrontend.formDataSupported = function() {
|
|
45
|
-
return typeof FormData
|
|
46
|
-
}
|
|
48
|
+
MOJFrontend.formDataSupported = function () {
|
|
49
|
+
return typeof FormData === 'function'
|
|
50
|
+
}
|
|
47
51
|
|
|
48
|
-
MOJFrontend.fileApiSupported = function() {
|
|
49
|
-
|
|
50
|
-
input.type = 'file'
|
|
51
|
-
return typeof input.files
|
|
52
|
-
}
|
|
52
|
+
MOJFrontend.fileApiSupported = function () {
|
|
53
|
+
const input = document.createElement('input')
|
|
54
|
+
input.type = 'file'
|
|
55
|
+
return typeof input.files !== 'undefined'
|
|
56
|
+
}
|
|
53
57
|
|
|
54
|
-
MOJFrontend.nodeListForEach = function(nodes, callback) {
|
|
58
|
+
MOJFrontend.nodeListForEach = function (nodes, callback) {
|
|
55
59
|
if (window.NodeList.prototype.forEach) {
|
|
56
60
|
return nodes.forEach(callback)
|
|
57
61
|
}
|
|
58
|
-
for (
|
|
62
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
59
63
|
callback.call(window, nodes[i], i, nodes)
|
|
60
64
|
}
|
|
61
|
-
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Find an elements next sibling
|
|
69
|
+
*
|
|
70
|
+
* Utility function to find an elements next sibling matching the provided
|
|
71
|
+
* selector.
|
|
72
|
+
*
|
|
73
|
+
* @param {HTMLElement} element - Element to find siblings for
|
|
74
|
+
* @param {string} selector - selector for required sibling
|
|
75
|
+
*/
|
|
76
|
+
MOJFrontend.getNextSibling = function ($element, selector) {
|
|
77
|
+
if (!$element) return
|
|
78
|
+
// Get the next sibling element
|
|
79
|
+
let $sibling = $element.nextElementSibling
|
|
80
|
+
|
|
81
|
+
// If there's no selector, return the first sibling
|
|
82
|
+
if (!selector) return $sibling
|
|
83
|
+
|
|
84
|
+
// If the sibling matches our selector, use it
|
|
85
|
+
// If not, jump to the next sibling and continue the loop
|
|
86
|
+
while ($sibling) {
|
|
87
|
+
if ($sibling.matches(selector)) return $sibling
|
|
88
|
+
$sibling = $sibling.nextElementSibling
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Find an elements preceding sibling
|
|
94
|
+
*
|
|
95
|
+
* Utility function to find an elements previous sibling matching the provided
|
|
96
|
+
* selector.
|
|
97
|
+
*
|
|
98
|
+
* @param {HTMLElement} element - Element to find siblings for
|
|
99
|
+
* @param {string} selector - selector for required sibling
|
|
100
|
+
*/
|
|
101
|
+
MOJFrontend.getPreviousSibling = function ($element, selector) {
|
|
102
|
+
if (!$element) return
|
|
103
|
+
// Get the previous sibling element
|
|
104
|
+
let $sibling = $element.previousElementSibling
|
|
105
|
+
|
|
106
|
+
// If there's no selector, return the first sibling
|
|
107
|
+
if (!selector) return $sibling
|
|
108
|
+
|
|
109
|
+
// If the sibling matches our selector, use it
|
|
110
|
+
// If not, jump to the next sibling and continue the loop
|
|
111
|
+
while ($sibling) {
|
|
112
|
+
if ($sibling.matches(selector)) return $sibling
|
|
113
|
+
$sibling = $sibling.previousElementSibling
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
MOJFrontend.findNearestMatchingElement = function ($element, selector) {
|
|
118
|
+
// If no element or selector is provided, return null
|
|
119
|
+
if (!$element) return
|
|
120
|
+
if (!selector) return
|
|
121
|
+
|
|
122
|
+
// Start with the current element
|
|
123
|
+
let $currentElement = $element
|
|
124
|
+
|
|
125
|
+
while ($currentElement) {
|
|
126
|
+
// First check the current element
|
|
127
|
+
if ($currentElement.matches(selector)) {
|
|
128
|
+
return $currentElement
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check all previous siblings
|
|
132
|
+
let $sibling = $currentElement.previousElementSibling
|
|
133
|
+
while ($sibling) {
|
|
134
|
+
// Check if the sibling itself is a heading
|
|
135
|
+
if ($sibling.matches(selector)) {
|
|
136
|
+
return $sibling
|
|
137
|
+
}
|
|
138
|
+
$sibling = $sibling.previousElementSibling
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// If no match found in siblings, move up to parent
|
|
142
|
+
$currentElement = $currentElement.parentElement
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Move focus to element
|
|
148
|
+
*
|
|
149
|
+
* Sets tabindex to -1 to make the element programmatically focusable,
|
|
150
|
+
* but removes it on blur as the element doesn't need to be focused again.
|
|
151
|
+
*
|
|
152
|
+
* @param {HTMLElement} $element - HTML element
|
|
153
|
+
* @param {object} [options] - Handler options
|
|
154
|
+
* @param {function(this: HTMLElement): void} [options.onBeforeFocus] - Callback before focus
|
|
155
|
+
* @param {function(this: HTMLElement): void} [options.onBlur] - Callback on blur
|
|
156
|
+
*/
|
|
157
|
+
MOJFrontend.setFocus = function ($element, options = {}) {
|
|
158
|
+
const isFocusable = $element.getAttribute('tabindex')
|
|
159
|
+
|
|
160
|
+
if (!isFocusable) {
|
|
161
|
+
$element.setAttribute('tabindex', '-1')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Handle element focus
|
|
166
|
+
*/
|
|
167
|
+
function onFocus() {
|
|
168
|
+
$element.addEventListener('blur', onBlur, { once: true })
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Handle element blur
|
|
173
|
+
*/
|
|
174
|
+
function onBlur() {
|
|
175
|
+
if (options.onBlur) {
|
|
176
|
+
options.onBlur.call($element)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!isFocusable) {
|
|
180
|
+
$element.removeAttribute('tabindex')
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Add listener to reset element on blur, after focus
|
|
185
|
+
$element.addEventListener('focus', onFocus, { once: true })
|
|
186
|
+
|
|
187
|
+
// Focus element
|
|
188
|
+
if (options.onBeforeFocus) {
|
|
189
|
+
options.onBeforeFocus.call($element)
|
|
190
|
+
}
|
|
191
|
+
$element.focus()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* eslint-disable no-new */
|
|
62
195
|
|
|
63
196
|
MOJFrontend.initAll = function (options) {
|
|
64
197
|
// Set the options to an empty object by default if no options are passed.
|
|
65
|
-
options = typeof options !== 'undefined' ? options : {}
|
|
198
|
+
options = typeof options !== 'undefined' ? options : {}
|
|
66
199
|
|
|
67
200
|
// Allow the user to initialise MOJ Frontend in only certain sections of the page
|
|
68
201
|
// Defaults to the entire document if nothing is set.
|
|
69
|
-
|
|
202
|
+
const scope = typeof options.scope !== 'undefined' ? options.scope : document
|
|
70
203
|
|
|
71
|
-
|
|
204
|
+
const $addAnothers = scope.querySelectorAll('[data-module="moj-add-another"]')
|
|
72
205
|
MOJFrontend.nodeListForEach($addAnothers, function ($addAnother) {
|
|
73
|
-
new MOJFrontend.AddAnother($addAnother)
|
|
74
|
-
})
|
|
206
|
+
new MOJFrontend.AddAnother($addAnother)
|
|
207
|
+
})
|
|
75
208
|
|
|
76
|
-
|
|
209
|
+
const $multiSelects = scope.querySelectorAll(
|
|
210
|
+
'[data-module="moj-multi-select"]'
|
|
211
|
+
)
|
|
77
212
|
MOJFrontend.nodeListForEach($multiSelects, function ($multiSelect) {
|
|
78
213
|
new MOJFrontend.MultiSelect({
|
|
79
|
-
container: $multiSelect.querySelector(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
214
|
+
container: $multiSelect.querySelector(
|
|
215
|
+
$multiSelect.getAttribute('data-multi-select-checkbox')
|
|
216
|
+
),
|
|
217
|
+
checkboxes: $multiSelect.querySelectorAll(
|
|
218
|
+
'tbody .govuk-checkboxes__input'
|
|
219
|
+
),
|
|
220
|
+
id_prefix: $multiSelect.getAttribute('data-multi-select-idprefix')
|
|
221
|
+
})
|
|
222
|
+
})
|
|
84
223
|
|
|
85
|
-
|
|
224
|
+
const $passwordReveals = scope.querySelectorAll(
|
|
225
|
+
'[data-module="moj-password-reveal"]'
|
|
226
|
+
)
|
|
86
227
|
MOJFrontend.nodeListForEach($passwordReveals, function ($passwordReveal) {
|
|
87
|
-
new MOJFrontend.PasswordReveal($passwordReveal)
|
|
88
|
-
})
|
|
228
|
+
new MOJFrontend.PasswordReveal($passwordReveal)
|
|
229
|
+
})
|
|
89
230
|
|
|
90
|
-
|
|
231
|
+
const $richTextEditors = scope.querySelectorAll(
|
|
232
|
+
'[data-module="moj-rich-text-editor"]'
|
|
233
|
+
)
|
|
91
234
|
MOJFrontend.nodeListForEach($richTextEditors, function ($richTextEditor) {
|
|
92
|
-
|
|
235
|
+
const options = {
|
|
93
236
|
textarea: $($richTextEditor)
|
|
94
|
-
}
|
|
237
|
+
}
|
|
95
238
|
|
|
96
|
-
|
|
239
|
+
const toolbarAttr = $richTextEditor.getAttribute(
|
|
240
|
+
'data-moj-rich-text-editor-toolbar'
|
|
241
|
+
)
|
|
97
242
|
if (toolbarAttr) {
|
|
98
|
-
|
|
99
|
-
options.toolbar = {}
|
|
100
|
-
for (
|
|
243
|
+
const toolbar = toolbarAttr.split(',')
|
|
244
|
+
options.toolbar = {}
|
|
245
|
+
for (const item in toolbar) options.toolbar[toolbar[item]] = true
|
|
101
246
|
}
|
|
102
247
|
|
|
103
|
-
new MOJFrontend.RichTextEditor(options)
|
|
104
|
-
})
|
|
248
|
+
new MOJFrontend.RichTextEditor(options)
|
|
249
|
+
})
|
|
105
250
|
|
|
106
|
-
|
|
251
|
+
const $searchToggles = scope.querySelectorAll(
|
|
252
|
+
'[data-module="moj-search-toggle"]'
|
|
253
|
+
)
|
|
107
254
|
MOJFrontend.nodeListForEach($searchToggles, function ($searchToggle) {
|
|
108
255
|
new MOJFrontend.SearchToggle({
|
|
109
256
|
toggleButton: {
|
|
@@ -113,120 +260,456 @@ MOJFrontend.initAll = function (options) {
|
|
|
113
260
|
search: {
|
|
114
261
|
container: $($searchToggle.querySelector('.moj-search'))
|
|
115
262
|
}
|
|
116
|
-
})
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
var $sortableTables = scope.querySelectorAll('[data-module="moj-sortable-table"]');
|
|
120
|
-
MOJFrontend.nodeListForEach($sortableTables, function ($table) {
|
|
121
|
-
new MOJFrontend.SortableTable({
|
|
122
|
-
table: $table
|
|
123
|
-
});
|
|
124
|
-
});
|
|
263
|
+
})
|
|
264
|
+
})
|
|
125
265
|
|
|
126
|
-
|
|
266
|
+
const $sortableTables = scope.querySelectorAll(
|
|
267
|
+
'[data-module="moj-sortable-table"]'
|
|
268
|
+
)
|
|
127
269
|
MOJFrontend.nodeListForEach($sortableTables, function ($table) {
|
|
128
270
|
new MOJFrontend.SortableTable({
|
|
129
271
|
table: $table
|
|
130
|
-
})
|
|
131
|
-
})
|
|
272
|
+
})
|
|
273
|
+
})
|
|
132
274
|
|
|
133
|
-
const $datepickers =
|
|
275
|
+
const $datepickers = scope.querySelectorAll('[data-module="moj-date-picker"]')
|
|
134
276
|
MOJFrontend.nodeListForEach($datepickers, function ($datepicker) {
|
|
135
|
-
new MOJFrontend.DatePicker($datepicker, {}).init()
|
|
277
|
+
new MOJFrontend.DatePicker($datepicker, {}).init()
|
|
136
278
|
})
|
|
137
279
|
|
|
138
280
|
const $buttonMenus = scope.querySelectorAll('[data-module="moj-button-menu"]')
|
|
139
281
|
MOJFrontend.nodeListForEach($buttonMenus, function ($buttonmenu) {
|
|
140
|
-
new MOJFrontend.ButtonMenu($buttonmenu, {}).init()
|
|
282
|
+
new MOJFrontend.ButtonMenu($buttonmenu, {}).init()
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
const $alerts = scope.querySelectorAll('[data-module="moj-alert"]')
|
|
286
|
+
MOJFrontend.nodeListForEach($alerts, function ($alert) {
|
|
287
|
+
new MOJFrontend.Alert($alert, {}).init()
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
MOJFrontend.version = '3.4.0'
|
|
292
|
+
|
|
293
|
+
MOJFrontend.AddAnother = function (container) {
|
|
294
|
+
this.container = $(container)
|
|
295
|
+
|
|
296
|
+
if (this.container.data('moj-add-another-initialised')) {
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.container.data('moj-add-another-initialised', true)
|
|
301
|
+
|
|
302
|
+
this.container.on(
|
|
303
|
+
'click',
|
|
304
|
+
'.moj-add-another__remove-button',
|
|
305
|
+
$.proxy(this, 'onRemoveButtonClick')
|
|
306
|
+
)
|
|
307
|
+
this.container.on(
|
|
308
|
+
'click',
|
|
309
|
+
'.moj-add-another__add-button',
|
|
310
|
+
$.proxy(this, 'onAddButtonClick')
|
|
311
|
+
)
|
|
312
|
+
this.container
|
|
313
|
+
.find('.moj-add-another__add-button, moj-add-another__remove-button')
|
|
314
|
+
.prop('type', 'button')
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
MOJFrontend.AddAnother.prototype.onAddButtonClick = function (e) {
|
|
318
|
+
const item = this.getNewItem()
|
|
319
|
+
this.updateAttributes(this.getItems().length, item)
|
|
320
|
+
this.resetItem(item)
|
|
321
|
+
const firstItem = this.getItems().first()
|
|
322
|
+
if (!this.hasRemoveButton(firstItem)) {
|
|
323
|
+
this.createRemoveButton(firstItem)
|
|
324
|
+
}
|
|
325
|
+
this.getItems().last().after(item)
|
|
326
|
+
item.find('input, textarea, select').first().focus()
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
MOJFrontend.AddAnother.prototype.hasRemoveButton = function (item) {
|
|
330
|
+
return item.find('.moj-add-another__remove-button').length
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
MOJFrontend.AddAnother.prototype.getItems = function () {
|
|
334
|
+
return this.container.find('.moj-add-another__item')
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
MOJFrontend.AddAnother.prototype.getNewItem = function () {
|
|
338
|
+
const item = this.getItems().first().clone()
|
|
339
|
+
if (!this.hasRemoveButton(item)) {
|
|
340
|
+
this.createRemoveButton(item)
|
|
341
|
+
}
|
|
342
|
+
return item
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
MOJFrontend.AddAnother.prototype.updateAttributes = function (index, item) {
|
|
346
|
+
item.find('[data-name]').each(function (i, el) {
|
|
347
|
+
const originalId = el.id
|
|
348
|
+
|
|
349
|
+
el.name = $(el)
|
|
350
|
+
.attr('data-name')
|
|
351
|
+
.replace(/%index%/, index)
|
|
352
|
+
el.id = $(el)
|
|
353
|
+
.attr('data-id')
|
|
354
|
+
.replace(/%index%/, index)
|
|
355
|
+
|
|
356
|
+
const label =
|
|
357
|
+
$(el).siblings('label')[0] ||
|
|
358
|
+
$(el).parents('label')[0] ||
|
|
359
|
+
item.find(`[for="${originalId}"]`)[0]
|
|
360
|
+
label.htmlFor = el.id
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
MOJFrontend.AddAnother.prototype.createRemoveButton = function (item) {
|
|
365
|
+
item.append(
|
|
366
|
+
'<button type="button" class="govuk-button govuk-button--secondary moj-add-another__remove-button">Remove</button>'
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
MOJFrontend.AddAnother.prototype.resetItem = function (item) {
|
|
371
|
+
item.find('[data-name], [data-id]').each(function (index, el) {
|
|
372
|
+
if (el.type === 'checkbox' || el.type === 'radio') {
|
|
373
|
+
el.checked = false
|
|
374
|
+
} else {
|
|
375
|
+
el.value = ''
|
|
376
|
+
}
|
|
141
377
|
})
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
MOJFrontend.AddAnother.prototype.onRemoveButtonClick = function (e) {
|
|
381
|
+
$(e.currentTarget).parents('.moj-add-another__item').remove()
|
|
382
|
+
const items = this.getItems()
|
|
383
|
+
if (items.length === 1) {
|
|
384
|
+
items.find('.moj-add-another__remove-button').remove()
|
|
385
|
+
}
|
|
386
|
+
items.each(
|
|
387
|
+
$.proxy(function (index, el) {
|
|
388
|
+
this.updateAttributes(index, $(el))
|
|
389
|
+
}, this)
|
|
390
|
+
)
|
|
391
|
+
this.focusHeading()
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
MOJFrontend.AddAnother.prototype.focusHeading = function () {
|
|
395
|
+
this.container.find('.moj-add-another__heading').get(0).focus()
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* @typedef {object} AlertConfig
|
|
400
|
+
* @property {boolean} [dismissible=false] - Can the alert be dismissed by the user
|
|
401
|
+
* @property {string} [dismissText=Dismiss] - the label text for the dismiss button
|
|
402
|
+
* @property {boolean} [disableAutoFocus=false] - whether the alert will be autofocused
|
|
403
|
+
* @property {string} [focusOnDismissSelector] - CSS Selector for element to be focused on dismiss
|
|
404
|
+
*/
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* @param {HTMLElement} $module - the Alert element
|
|
408
|
+
* @param {AlertConfig} config - configuration options
|
|
409
|
+
* @class
|
|
410
|
+
*/
|
|
411
|
+
MOJFrontend.Alert = function ($module, config = {}) {
|
|
412
|
+
if (!$module) {
|
|
413
|
+
return this
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const schema = Object.freeze({
|
|
417
|
+
properties: {
|
|
418
|
+
dismissible: { type: 'boolean' },
|
|
419
|
+
dismissText: { type: 'string' },
|
|
420
|
+
disableAutoFocus: { type: 'boolean' },
|
|
421
|
+
focusOnDismissSelector: { type: 'string' }
|
|
422
|
+
}
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
const defaults = {
|
|
426
|
+
dismissible: false,
|
|
427
|
+
dismissText: 'Dismiss',
|
|
428
|
+
disableAutoFocus: false
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// data attributes override JS config, which overrides defaults
|
|
432
|
+
this.config = this.mergeConfigs(
|
|
433
|
+
defaults,
|
|
434
|
+
config,
|
|
435
|
+
this.parseDataset(schema, $module.dataset)
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
this.$module = $module
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
MOJFrontend.Alert.prototype.init = function () {
|
|
442
|
+
/**
|
|
443
|
+
* Focus the alert
|
|
444
|
+
*
|
|
445
|
+
* If `role="alert"` is set, focus the element to help some assistive
|
|
446
|
+
* technologies prioritise announcing it.
|
|
447
|
+
*
|
|
448
|
+
* You can turn off the auto-focus functionality by setting
|
|
449
|
+
* `data-disable-auto-focus="true"` in the component HTML. You might wish to
|
|
450
|
+
* do this based on user research findings, or to avoid a clash with another
|
|
451
|
+
* element which should be focused when the page loads.
|
|
452
|
+
*/
|
|
453
|
+
if (
|
|
454
|
+
this.$module.getAttribute('role') === 'alert' &&
|
|
455
|
+
!this.config.disableAutoFocus
|
|
456
|
+
) {
|
|
457
|
+
MOJFrontend.setFocus(this.$module)
|
|
458
|
+
}
|
|
142
459
|
|
|
460
|
+
this.$dismissButton = this.$module.querySelector('.moj-alert__dismiss')
|
|
461
|
+
|
|
462
|
+
if (this.config.dismissible && this.$dismissButton) {
|
|
463
|
+
this.$dismissButton.innerHTML = this.config.dismissText
|
|
464
|
+
this.$dismissButton.removeAttribute('hidden')
|
|
465
|
+
|
|
466
|
+
this.$module.addEventListener('click', (event) => {
|
|
467
|
+
if (this.$dismissButton.contains(event.target)) {
|
|
468
|
+
this.dimiss()
|
|
469
|
+
}
|
|
470
|
+
})
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Handle dismissing the alert
|
|
476
|
+
*/
|
|
477
|
+
MOJFrontend.Alert.prototype.dimiss = function () {
|
|
478
|
+
let $elementToRecieveFocus
|
|
479
|
+
|
|
480
|
+
// If a selector has been provided, attempt to find that element
|
|
481
|
+
if (this.config.focusOnDismissSelector) {
|
|
482
|
+
$elementToRecieveFocus = document.querySelector(
|
|
483
|
+
this.config.focusOnDismissSelector
|
|
484
|
+
)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Is the next sibling another alert
|
|
488
|
+
if (!$elementToRecieveFocus) {
|
|
489
|
+
const $nextSibling = this.$module.nextElementSibling
|
|
490
|
+
if ($nextSibling && $nextSibling.matches('.moj-alert')) {
|
|
491
|
+
$elementToRecieveFocus = $nextSibling
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Else try to find any preceding sibling alert or heading
|
|
496
|
+
if (!$elementToRecieveFocus) {
|
|
497
|
+
$elementToRecieveFocus = MOJFrontend.getPreviousSibling(
|
|
498
|
+
this.$module,
|
|
499
|
+
'.moj-alert, h1, h2, h3, h4, h5, h6'
|
|
500
|
+
)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Else find the closest ancestor heading, or fallback to main, or last resort
|
|
504
|
+
// use the body element
|
|
505
|
+
if (!$elementToRecieveFocus) {
|
|
506
|
+
$elementToRecieveFocus = MOJFrontend.findNearestMatchingElement(
|
|
507
|
+
this.$module,
|
|
508
|
+
'h1, h2, h3, h4, h5, h6, main, body'
|
|
509
|
+
)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// If we have an element, place focus on it
|
|
513
|
+
if ($elementToRecieveFocus) {
|
|
514
|
+
MOJFrontend.setFocus($elementToRecieveFocus)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Remove the alert
|
|
518
|
+
this.$module.remove()
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Normalise string
|
|
523
|
+
*
|
|
524
|
+
* 'If it looks like a duck, and it quacks like a duck…' 🦆
|
|
525
|
+
*
|
|
526
|
+
* If the passed value looks like a boolean or a number, convert it to a boolean
|
|
527
|
+
* or number.
|
|
528
|
+
*
|
|
529
|
+
* Designed to be used to convert config passed via data attributes (which are
|
|
530
|
+
* always strings) into something sensible.
|
|
531
|
+
*
|
|
532
|
+
* @internal
|
|
533
|
+
* @param {DOMStringMap[string]} value - The value to normalise
|
|
534
|
+
* @param {SchemaProperty} [property] - Component schema property
|
|
535
|
+
* @returns {string | boolean | number | undefined} Normalised data
|
|
536
|
+
*/
|
|
537
|
+
MOJFrontend.Alert.prototype.normaliseString = function (value, property) {
|
|
538
|
+
const trimmedValue = value ? value.trim() : ''
|
|
539
|
+
|
|
540
|
+
let output
|
|
541
|
+
let outputType
|
|
542
|
+
if (property && property.type) {
|
|
543
|
+
outputType = property.type
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// No schema type set? Determine automatically
|
|
547
|
+
if (!outputType) {
|
|
548
|
+
if (['true', 'false'].includes(trimmedValue)) {
|
|
549
|
+
outputType = 'boolean'
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Empty / whitespace-only strings are considered finite so we need to check
|
|
553
|
+
// the length of the trimmed string as well
|
|
554
|
+
if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
|
|
555
|
+
outputType = 'number'
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
switch (outputType) {
|
|
560
|
+
case 'boolean':
|
|
561
|
+
output = trimmedValue === 'true'
|
|
562
|
+
break
|
|
563
|
+
|
|
564
|
+
case 'number':
|
|
565
|
+
output = Number(trimmedValue)
|
|
566
|
+
break
|
|
567
|
+
|
|
568
|
+
default:
|
|
569
|
+
output = value
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return output
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Parse dataset
|
|
577
|
+
*
|
|
578
|
+
* Loop over an object and normalise each value using {@link normaliseString},
|
|
579
|
+
* optionally expanding nested `i18n.field`
|
|
580
|
+
*
|
|
581
|
+
* @param {Schema} schema - component schema
|
|
582
|
+
* @param {DOMStringMap} dataset - HTML element dataset
|
|
583
|
+
* @returns {object} Normalised dataset
|
|
584
|
+
*/
|
|
585
|
+
MOJFrontend.Alert.prototype.parseDataset = function (schema, dataset) {
|
|
586
|
+
const parsed = {}
|
|
587
|
+
|
|
588
|
+
for (const [field, property] of Object.entries(schema.properties)) {
|
|
589
|
+
if (field in dataset) {
|
|
590
|
+
if (dataset[field]) {
|
|
591
|
+
parsed[field] = this.normaliseString(dataset[field], property)
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return parsed
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Config merging function
|
|
601
|
+
*
|
|
602
|
+
* Takes any number of objects and combines them together, with
|
|
603
|
+
* greatest priority on the LAST item passed in.
|
|
604
|
+
*
|
|
605
|
+
* @param {...{ [key: string]: unknown }} configObjects - Config objects to merge
|
|
606
|
+
* @returns {{ [key: string]: unknown }} A merged config object
|
|
607
|
+
*/
|
|
608
|
+
MOJFrontend.Alert.prototype.mergeConfigs = function (...configObjects) {
|
|
609
|
+
const formattedConfigObject = {}
|
|
610
|
+
|
|
611
|
+
// Loop through each of the passed objects
|
|
612
|
+
for (const configObject of configObjects) {
|
|
613
|
+
for (const key of Object.keys(configObject)) {
|
|
614
|
+
const option = formattedConfigObject[key]
|
|
615
|
+
const override = configObject[key]
|
|
616
|
+
|
|
617
|
+
// Push their keys one-by-one into formattedConfigObject. Any duplicate
|
|
618
|
+
// keys with object values will be merged, otherwise the new value will
|
|
619
|
+
// override the existing value.
|
|
620
|
+
if (typeof option === 'object' && typeof override === 'object') {
|
|
621
|
+
// @ts-expect-error Index signature for type 'string' is missing
|
|
622
|
+
formattedConfigObject[key] = this.mergeConfigs(option, override)
|
|
623
|
+
} else {
|
|
624
|
+
formattedConfigObject[key] = override
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return formattedConfigObject
|
|
143
630
|
}
|
|
144
631
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
MOJFrontend.AddAnother.prototype.focusHeading = function() {
|
|
228
|
-
this.container.find('.moj-add-another__heading').focus();
|
|
229
|
-
};
|
|
632
|
+
/**
|
|
633
|
+
* Schema for component config
|
|
634
|
+
*
|
|
635
|
+
* @typedef {object} Schema
|
|
636
|
+
* @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
|
|
637
|
+
*/
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Schema property for component config
|
|
641
|
+
*
|
|
642
|
+
* @typedef {object} SchemaProperty
|
|
643
|
+
* @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
|
|
644
|
+
*/
|
|
645
|
+
|
|
646
|
+
const pageTemplate = `
|
|
647
|
+
<main>
|
|
648
|
+
<div id="alert-1" role="region" class="moj-alert moj-alert--information moj-alert--with-heading" aria-label="information: This contains information" data-module="moj-alert" data-dismissible="true">
|
|
649
|
+
<div>
|
|
650
|
+
<svg class="moj-alert__icon" role="presentation" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" height="30" width="30"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.2165 3.45151C11.733 2.82332 13.3585 2.5 15 2.5C16.6415 2.5 18.267 2.82332 19.7835 3.45151C21.3001 4.07969 22.6781 5.00043 23.8388 6.16117C24.9996 7.3219 25.9203 8.69989 26.5485 10.2165C27.1767 11.733 27.5 13.3585 27.5 15C27.5 18.3152 26.183 21.4946 23.8388 23.8388C21.4946 26.183 18.3152 27.5 15 27.5C13.3585 27.5 11.733 27.1767 10.2165 26.5485C8.69989 25.9203 7.3219 24.9996 6.16117 23.8388C3.81696 21.4946 2.5 18.3152 2.5 15C2.5 11.6848 3.81696 8.50537 6.16117 6.16117C7.3219 5.00043 8.69989 4.07969 10.2165 3.45151ZM16.3574 22.4121H13.6621V12.95H16.3574V22.4121ZM13.3789 9.20898C13.3789 8.98763 13.4212 8.7793 13.5059 8.58398C13.5905 8.38216 13.7044 8.20964 13.8477 8.06641C13.9974 7.91667 14.1699 7.79948 14.3652 7.71484C14.5605 7.63021 14.7721 7.58789 15 7.58789C15.2214 7.58789 15.4297 7.63021 15.625 7.71484C15.8268 7.79948 15.9993 7.91667 16.1426 8.06641C16.2923 8.20964 16.4095 8.38216 16.4941 8.58398C16.5788 8.7793 16.6211 8.98763 16.6211 9.20898C16.6211 9.43685 16.5788 9.64844 16.4941 9.84375C16.4095 10.0391 16.2923 10.2116 16.1426 10.3613C15.9993 10.5046 15.8268 10.6185 15.625 10.7031C15.4297 10.7878 15.2214 10.8301 15 10.8301C14.7721 10.8301 14.5605 10.7878 14.3652 10.7031C14.1699 10.6185 13.9974 10.5046 13.8477 10.3613C13.7044 10.2116 13.5905 10.0391 13.5059 9.84375C13.4212 9.64844 13.3789 9.43685 13.3789 9.20898Z" fill="currentColor"/></svg>
|
|
651
|
+
</div>
|
|
652
|
+
<div class="moj-alert__content">
|
|
653
|
+
<h2 class="govuk-heading-m">This contains information</h2>
|
|
654
|
+
Content that informs you of a thing. It really is so very informative, that's why it needs so much content and is really, exceedingly verbose.
|
|
655
|
+
</div>
|
|
656
|
+
<div class="moj-alert__action">
|
|
657
|
+
<button class="moj-alert__dismiss" hidden>Dismiss</button>
|
|
658
|
+
</div>
|
|
659
|
+
</div>
|
|
660
|
+
|
|
661
|
+
<h1 id="h1">Heading 1</h1>
|
|
662
|
+
<section>
|
|
663
|
+
<h2 id="h2">heading 2</h2>
|
|
664
|
+
<div id="alert-2" role="region" class="moj-alert moj-alert--information" aria-label="information: You might like to know" data-module="moj-alert" data-dismissible="true">
|
|
665
|
+
<div>
|
|
666
|
+
<svg class="moj-alert__icon" role="presentation" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" height="30" width="30"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.2165 3.45151C11.733 2.82332 13.3585 2.5 15 2.5C16.6415 2.5 18.267 2.82332 19.7835 3.45151C21.3001 4.07969 22.6781 5.00043 23.8388 6.16117C24.9996 7.3219 25.9203 8.69989 26.5485 10.2165C27.1767 11.733 27.5 13.3585 27.5 15C27.5 18.3152 26.183 21.4946 23.8388 23.8388C21.4946 26.183 18.3152 27.5 15 27.5C13.3585 27.5 11.733 27.1767 10.2165 26.5485C8.69989 25.9203 7.3219 24.9996 6.16117 23.8388C3.81696 21.4946 2.5 18.3152 2.5 15C2.5 11.6848 3.81696 8.50537 6.16117 6.16117C7.3219 5.00043 8.69989 4.07969 10.2165 3.45151ZM16.3574 22.4121H13.6621V12.95H16.3574V22.4121ZM13.3789 9.20898C13.3789 8.98763 13.4212 8.7793 13.5059 8.58398C13.5905 8.38216 13.7044 8.20964 13.8477 8.06641C13.9974 7.91667 14.1699 7.79948 14.3652 7.71484C14.5605 7.63021 14.7721 7.58789 15 7.58789C15.2214 7.58789 15.4297 7.63021 15.625 7.71484C15.8268 7.79948 15.9993 7.91667 16.1426 8.06641C16.2923 8.20964 16.4095 8.38216 16.4941 8.58398C16.5788 8.7793 16.6211 8.98763 16.6211 9.20898C16.6211 9.43685 16.5788 9.64844 16.4941 9.84375C16.4095 10.0391 16.2923 10.2116 16.1426 10.3613C15.9993 10.5046 15.8268 10.6185 15.625 10.7031C15.4297 10.7878 15.2214 10.8301 15 10.8301C14.7721 10.8301 14.5605 10.7878 14.3652 10.7031C14.1699 10.6185 13.9974 10.5046 13.8477 10.3613C13.7044 10.2116 13.5905 10.0391 13.5059 9.84375C13.4212 9.64844 13.3789 9.43685 13.3789 9.20898Z" fill="currentColor"/></svg>
|
|
667
|
+
</div>
|
|
668
|
+
<div class="moj-alert__content">Content that informs you of a thing. <a href="#">More information</a></div>
|
|
669
|
+
<div class="moj-alert__action">
|
|
670
|
+
<button class="moj-alert__dismiss" hidden>Dismiss</button>
|
|
671
|
+
</div>
|
|
672
|
+
</div>
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
<div id="alert-3"role="region" class="moj-alert moj-alert--success" aria-label="success: Woohoo!" data-module="moj-alert" data-dismissible="true">
|
|
676
|
+
<div>
|
|
677
|
+
<svg class="moj-alert__icon" role="presentation" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" height="30" width="30"><path d="M11.2869 24.6726L2.00415 15.3899L4.62189 12.7722L11.2869 19.4186L25.3781 5.32739L27.9958 7.96369L11.2869 24.6726Z" fill="currentColor"/>
|
|
678
|
+
</svg>
|
|
679
|
+
</div>
|
|
680
|
+
<div class="moj-alert__content">That thing just successfully occurred. <a href="#">Celebrate here</a></div>
|
|
681
|
+
<div class="moj-alert__action">
|
|
682
|
+
<button class="moj-alert__dismiss" hidden>Dismiss</button>
|
|
683
|
+
</div>
|
|
684
|
+
</div>
|
|
685
|
+
|
|
686
|
+
<div id="alert-4" role="region" class="moj-alert moj-alert--warning" aria-label="warning: Something's not quite right" data-module="moj-alert" data-dismissible="true">
|
|
687
|
+
<div>
|
|
688
|
+
<svg class="moj-alert__icon" role="presentation" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" height="30" width="30"><path fill-rule="evenodd" clip-rule="evenodd" d="M15 2.44922L28.75 26.1992H1.25L15 2.44922ZM13.5107 9.49579H16.4697L16.2431 17.7678H13.7461L13.5107 9.49579ZM13.1299 21.82C13.1299 21.5661 13.1787 21.3285 13.2764 21.1071C13.374 20.8793 13.5075 20.6807 13.6768 20.5114C13.8525 20.3421 14.0544 20.2087 14.2822 20.111C14.5101 20.0134 14.7542 19.9645 15.0146 19.9645C15.2686 19.9645 15.5062 20.0134 15.7275 20.111C15.9554 20.2087 16.154 20.3421 16.3232 20.5114C16.4925 20.6807 16.626 20.8793 16.7236 21.1071C16.8213 21.3285 16.8701 21.5661 16.8701 21.82C16.8701 22.0804 16.8213 22.3246 16.7236 22.5524C16.626 22.7803 16.4925 22.9789 16.3232 23.1481C16.154 23.3174 15.9554 23.4509 15.7275 23.5485C15.5062 23.6462 15.2686 23.695 15.0146 23.695C14.7542 23.695 14.5101 23.6462 14.2822 23.5485C14.0544 23.4509 13.8525 23.3174 13.6768 23.1481C13.5075 22.9789 13.374 22.7803 13.2764 22.5524C13.1787 22.3246 13.1299 22.0804 13.1299 21.82Z" fill="currentColor"/></svg>
|
|
689
|
+
</div>
|
|
690
|
+
<div class="moj-alert__content">You should be aware of this thing. <a href="#">More information</a></div>
|
|
691
|
+
<div class="moj-alert__action">
|
|
692
|
+
<button class="moj-alert__dismiss" hidden>Dismiss</button>
|
|
693
|
+
</div>
|
|
694
|
+
</div>
|
|
695
|
+
|
|
696
|
+
<div id="alert-5" role="region" class="moj-alert moj-alert--error" aria-label="error: Woah, hold up!" data-module="moj-alert" data-dismissible="true" data-dismissible="true" data-focus-on-dismiss-selector="#focusOnMe">
|
|
697
|
+
<div>
|
|
698
|
+
<svg class="moj-alert__icon" role="presentation" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" height="30" width="30"><path fill-rule="evenodd" clip-rule="evenodd" d="M20.1777 2.5H9.82233L2.5 9.82233V20.1777L9.82233 27.5H20.1777L27.5 20.1777V9.82233L20.1777 2.5ZM10.9155 8.87769L15.0001 12.9623L19.0847 8.87771L21.1224 10.9154L17.0378 15L21.1224 19.0846L19.0847 21.1222L15.0001 17.0376L10.9155 21.1223L8.87782 19.0846L12.9624 15L8.87783 10.9153L10.9155 8.87769Z" fill="currentColor"/></svg>
|
|
699
|
+
</div>
|
|
700
|
+
<div class="moj-alert__content">Bad things happened. <a href="#">Contact us</a></div>
|
|
701
|
+
<div class="moj-alert__action">
|
|
702
|
+
<button class="moj-alert__dismiss" hidden>Dismiss</button>
|
|
703
|
+
</div>
|
|
704
|
+
</div>
|
|
705
|
+
|
|
706
|
+
<div id="focusOnMe">I will receive focus</div>
|
|
707
|
+
</section>
|
|
708
|
+
</main>
|
|
709
|
+
`
|
|
710
|
+
module.exports = {
|
|
711
|
+
pageTemplate
|
|
712
|
+
}
|
|
230
713
|
|
|
231
714
|
/**
|
|
232
715
|
* @typedef {object} ButtonMenuConfig
|
|
@@ -238,157 +721,157 @@ MOJFrontend.AddAnother.prototype.focusHeading = function() {
|
|
|
238
721
|
/**
|
|
239
722
|
* @param {HTMLElement} $module
|
|
240
723
|
* @param {ButtonMenuConfig} config
|
|
241
|
-
* @
|
|
724
|
+
* @class
|
|
242
725
|
*/
|
|
243
726
|
MOJFrontend.ButtonMenu = function ($module, config = {}) {
|
|
244
727
|
if (!$module) {
|
|
245
|
-
return this
|
|
728
|
+
return this
|
|
246
729
|
}
|
|
247
730
|
|
|
248
731
|
const schema = Object.freeze({
|
|
249
732
|
properties: {
|
|
250
|
-
buttonText: { type:
|
|
251
|
-
buttonClasses: { type:
|
|
252
|
-
alignMenu: { type:
|
|
253
|
-
}
|
|
254
|
-
})
|
|
733
|
+
buttonText: { type: 'string' },
|
|
734
|
+
buttonClasses: { type: 'string' },
|
|
735
|
+
alignMenu: { type: 'string' }
|
|
736
|
+
}
|
|
737
|
+
})
|
|
255
738
|
|
|
256
739
|
const defaults = {
|
|
257
|
-
buttonText:
|
|
258
|
-
alignMenu:
|
|
259
|
-
buttonClasses:
|
|
260
|
-
}
|
|
740
|
+
buttonText: 'Actions',
|
|
741
|
+
alignMenu: 'left',
|
|
742
|
+
buttonClasses: ''
|
|
743
|
+
}
|
|
261
744
|
// data attributes override JS config, which overrides defaults
|
|
262
745
|
this.config = this.mergeConfigs(
|
|
263
746
|
defaults,
|
|
264
747
|
config,
|
|
265
|
-
this.parseDataset(schema, $module.dataset)
|
|
266
|
-
)
|
|
748
|
+
this.parseDataset(schema, $module.dataset)
|
|
749
|
+
)
|
|
267
750
|
|
|
268
|
-
this.$module = $module
|
|
269
|
-
}
|
|
751
|
+
this.$module = $module
|
|
752
|
+
}
|
|
270
753
|
|
|
271
754
|
MOJFrontend.ButtonMenu.prototype.init = function () {
|
|
272
755
|
// If only one button is provided, don't initiate a menu and toggle button
|
|
273
756
|
// if classes have been provided for the toggleButton, apply them to the single item
|
|
274
|
-
if (this.$module.children.length
|
|
275
|
-
const button = this.$module.children[0]
|
|
757
|
+
if (this.$module.children.length === 1) {
|
|
758
|
+
const button = this.$module.children[0]
|
|
276
759
|
button.classList.forEach((className) => {
|
|
277
|
-
if (className.startsWith(
|
|
278
|
-
button.classList.remove(className)
|
|
760
|
+
if (className.startsWith('govuk-button-')) {
|
|
761
|
+
button.classList.remove(className)
|
|
279
762
|
}
|
|
280
|
-
button.classList.remove(
|
|
281
|
-
})
|
|
763
|
+
button.classList.remove('moj-button-menu__item')
|
|
764
|
+
})
|
|
282
765
|
if (this.config.buttonClasses) {
|
|
283
|
-
button.classList.add(...this.config.buttonClasses.split(
|
|
766
|
+
button.classList.add(...this.config.buttonClasses.split(' '))
|
|
284
767
|
}
|
|
285
768
|
}
|
|
286
769
|
// Otherwise intialise a button menu
|
|
287
770
|
if (this.$module.children.length > 1) {
|
|
288
|
-
this.initMenu()
|
|
771
|
+
this.initMenu()
|
|
289
772
|
}
|
|
290
|
-
}
|
|
773
|
+
}
|
|
291
774
|
|
|
292
775
|
MOJFrontend.ButtonMenu.prototype.initMenu = function () {
|
|
293
|
-
this.$menu = this.createMenu()
|
|
294
|
-
this.$module.insertAdjacentHTML(
|
|
295
|
-
this.setupMenuItems()
|
|
776
|
+
this.$menu = this.createMenu()
|
|
777
|
+
this.$module.insertAdjacentHTML('afterbegin', this.toggleTemplate())
|
|
778
|
+
this.setupMenuItems()
|
|
296
779
|
|
|
297
|
-
this.$menuToggle = this.$module.querySelector(
|
|
298
|
-
this.items = this.$menu.querySelectorAll(
|
|
780
|
+
this.$menuToggle = this.$module.querySelector(':scope > button')
|
|
781
|
+
this.items = this.$menu.querySelectorAll('a, button')
|
|
299
782
|
|
|
300
|
-
this.$menuToggle.addEventListener(
|
|
301
|
-
this.toggleMenu(event)
|
|
302
|
-
})
|
|
783
|
+
this.$menuToggle.addEventListener('click', (event) => {
|
|
784
|
+
this.toggleMenu(event)
|
|
785
|
+
})
|
|
303
786
|
|
|
304
|
-
this.$module.addEventListener(
|
|
305
|
-
this.handleKeyDown(event)
|
|
306
|
-
})
|
|
787
|
+
this.$module.addEventListener('keydown', (event) => {
|
|
788
|
+
this.handleKeyDown(event)
|
|
789
|
+
})
|
|
307
790
|
|
|
308
|
-
document.addEventListener(
|
|
791
|
+
document.addEventListener('click', (event) => {
|
|
309
792
|
if (!this.$module.contains(event.target)) {
|
|
310
|
-
this.closeMenu(false)
|
|
793
|
+
this.closeMenu(false)
|
|
311
794
|
}
|
|
312
|
-
})
|
|
313
|
-
}
|
|
795
|
+
})
|
|
796
|
+
}
|
|
314
797
|
|
|
315
798
|
MOJFrontend.ButtonMenu.prototype.createMenu = function () {
|
|
316
|
-
const $menu = document.createElement(
|
|
317
|
-
$menu.setAttribute(
|
|
318
|
-
$menu.hidden = true
|
|
319
|
-
$menu.classList.add(
|
|
320
|
-
if (this.config.alignMenu
|
|
321
|
-
$menu.classList.add(
|
|
799
|
+
const $menu = document.createElement('ul')
|
|
800
|
+
$menu.setAttribute('role', 'list')
|
|
801
|
+
$menu.hidden = true
|
|
802
|
+
$menu.classList.add('moj-button-menu__wrapper')
|
|
803
|
+
if (this.config.alignMenu === 'right') {
|
|
804
|
+
$menu.classList.add('moj-button-menu__wrapper--right')
|
|
322
805
|
}
|
|
323
806
|
|
|
324
|
-
this.$module.appendChild($menu)
|
|
807
|
+
this.$module.appendChild($menu)
|
|
325
808
|
while (this.$module.firstChild !== $menu) {
|
|
326
|
-
$menu.appendChild(this.$module.firstChild)
|
|
809
|
+
$menu.appendChild(this.$module.firstChild)
|
|
327
810
|
}
|
|
328
811
|
|
|
329
|
-
return $menu
|
|
330
|
-
}
|
|
812
|
+
return $menu
|
|
813
|
+
}
|
|
331
814
|
|
|
332
815
|
MOJFrontend.ButtonMenu.prototype.setupMenuItems = function () {
|
|
333
816
|
Array.from(this.$menu.children).forEach((item) => {
|
|
334
817
|
// wrap item in li tag
|
|
335
|
-
const listItem = document.createElement(
|
|
336
|
-
this.$menu.insertBefore(listItem, item)
|
|
337
|
-
listItem.appendChild(item)
|
|
818
|
+
const listItem = document.createElement('li')
|
|
819
|
+
this.$menu.insertBefore(listItem, item)
|
|
820
|
+
listItem.appendChild(item)
|
|
338
821
|
|
|
339
|
-
item.setAttribute(
|
|
822
|
+
item.setAttribute('tabindex', -1)
|
|
340
823
|
|
|
341
|
-
if (item.tagName
|
|
342
|
-
item.setAttribute(
|
|
824
|
+
if (item.tagName === 'BUTTON') {
|
|
825
|
+
item.setAttribute('type', 'button')
|
|
343
826
|
}
|
|
344
827
|
|
|
345
828
|
item.classList.forEach((className) => {
|
|
346
|
-
if (className.startsWith(
|
|
347
|
-
item.classList.remove(className)
|
|
829
|
+
if (className.startsWith('govuk-button')) {
|
|
830
|
+
item.classList.remove(className)
|
|
348
831
|
}
|
|
349
|
-
})
|
|
832
|
+
})
|
|
350
833
|
|
|
351
834
|
// add a slight delay after click before closing the menu, makes it *feel* better
|
|
352
|
-
item.addEventListener(
|
|
835
|
+
item.addEventListener('click', (event) => {
|
|
353
836
|
setTimeout(() => {
|
|
354
|
-
this.closeMenu(false)
|
|
355
|
-
}, 50)
|
|
356
|
-
})
|
|
357
|
-
})
|
|
358
|
-
}
|
|
837
|
+
this.closeMenu(false)
|
|
838
|
+
}, 50)
|
|
839
|
+
})
|
|
840
|
+
})
|
|
841
|
+
}
|
|
359
842
|
|
|
360
843
|
MOJFrontend.ButtonMenu.prototype.toggleTemplate = function () {
|
|
361
844
|
return `
|
|
362
|
-
<button type="button" class="govuk-button moj-button-menu__toggle-button ${this.config.buttonClasses ||
|
|
845
|
+
<button type="button" class="govuk-button moj-button-menu__toggle-button ${this.config.buttonClasses || ''}" aria-haspopup="true" aria-expanded="false">
|
|
363
846
|
<span>
|
|
364
847
|
${this.config.buttonText}
|
|
365
848
|
<svg width="11" height="5" viewBox="0 0 11 5" xmlns="http://www.w3.org/2000/svg">
|
|
366
849
|
<path d="M5.5 0L11 5L0 5L5.5 0Z" fill="currentColor"/>
|
|
367
850
|
</svg>
|
|
368
851
|
</span>
|
|
369
|
-
</button
|
|
370
|
-
}
|
|
852
|
+
</button>`
|
|
853
|
+
}
|
|
371
854
|
|
|
372
855
|
/**
|
|
373
856
|
* @returns {boolean}
|
|
374
857
|
*/
|
|
375
858
|
MOJFrontend.ButtonMenu.prototype.isOpen = function () {
|
|
376
|
-
return this.$menuToggle.getAttribute(
|
|
377
|
-
}
|
|
859
|
+
return this.$menuToggle.getAttribute('aria-expanded') === 'true'
|
|
860
|
+
}
|
|
378
861
|
|
|
379
862
|
MOJFrontend.ButtonMenu.prototype.toggleMenu = function (event) {
|
|
380
|
-
event.preventDefault()
|
|
863
|
+
event.preventDefault()
|
|
381
864
|
|
|
382
865
|
// If menu is triggered with mouse don't move focus to first item
|
|
383
|
-
const keyboardEvent = event.detail
|
|
384
|
-
const focusIndex = keyboardEvent ? 0 : -1
|
|
866
|
+
const keyboardEvent = event.detail === 0
|
|
867
|
+
const focusIndex = keyboardEvent ? 0 : -1
|
|
385
868
|
|
|
386
869
|
if (this.isOpen()) {
|
|
387
|
-
this.closeMenu()
|
|
870
|
+
this.closeMenu()
|
|
388
871
|
} else {
|
|
389
|
-
this.openMenu(focusIndex)
|
|
872
|
+
this.openMenu(focusIndex)
|
|
390
873
|
}
|
|
391
|
-
}
|
|
874
|
+
}
|
|
392
875
|
|
|
393
876
|
/**
|
|
394
877
|
* Opens the menu and optionally sets the focus to the item with given index
|
|
@@ -396,12 +879,12 @@ MOJFrontend.ButtonMenu.prototype.toggleMenu = function (event) {
|
|
|
396
879
|
* @param {number} focusIndex - The index of the item to focus
|
|
397
880
|
*/
|
|
398
881
|
MOJFrontend.ButtonMenu.prototype.openMenu = function (focusIndex = 0) {
|
|
399
|
-
this.$menu.hidden = false
|
|
400
|
-
this.$menuToggle.setAttribute(
|
|
882
|
+
this.$menu.hidden = false
|
|
883
|
+
this.$menuToggle.setAttribute('aria-expanded', 'true')
|
|
401
884
|
if (focusIndex !== -1) {
|
|
402
|
-
this.focusItem(focusIndex)
|
|
885
|
+
this.focusItem(focusIndex)
|
|
403
886
|
}
|
|
404
|
-
}
|
|
887
|
+
}
|
|
405
888
|
|
|
406
889
|
/**
|
|
407
890
|
* Closes the menu and optionally returns focus back to menuToggle
|
|
@@ -409,12 +892,12 @@ MOJFrontend.ButtonMenu.prototype.openMenu = function (focusIndex = 0) {
|
|
|
409
892
|
* @param {boolean} moveFocus - whether to return focus to the toggle button
|
|
410
893
|
*/
|
|
411
894
|
MOJFrontend.ButtonMenu.prototype.closeMenu = function (moveFocus = true) {
|
|
412
|
-
this.$menu.hidden = true
|
|
413
|
-
this.$menuToggle.setAttribute(
|
|
895
|
+
this.$menu.hidden = true
|
|
896
|
+
this.$menuToggle.setAttribute('aria-expanded', 'false')
|
|
414
897
|
if (moveFocus) {
|
|
415
|
-
this.$menuToggle.focus()
|
|
898
|
+
this.$menuToggle.focus()
|
|
416
899
|
}
|
|
417
|
-
}
|
|
900
|
+
}
|
|
418
901
|
|
|
419
902
|
/**
|
|
420
903
|
* Focuses the menu item at the specified index
|
|
@@ -422,65 +905,68 @@ MOJFrontend.ButtonMenu.prototype.closeMenu = function (moveFocus = true) {
|
|
|
422
905
|
* @param {number} index - the index of the item to focus
|
|
423
906
|
*/
|
|
424
907
|
MOJFrontend.ButtonMenu.prototype.focusItem = function (index) {
|
|
425
|
-
if (index >= this.items.length) index = 0
|
|
426
|
-
if (index < 0) index = this.items.length - 1
|
|
908
|
+
if (index >= this.items.length) index = 0
|
|
909
|
+
if (index < 0) index = this.items.length - 1
|
|
427
910
|
|
|
428
|
-
this.items.item(index)
|
|
429
|
-
|
|
911
|
+
const menuItem = this.items.item(index)
|
|
912
|
+
if (menuItem) {
|
|
913
|
+
menuItem.focus()
|
|
914
|
+
}
|
|
915
|
+
}
|
|
430
916
|
|
|
431
917
|
MOJFrontend.ButtonMenu.prototype.currentFocusIndex = function () {
|
|
432
|
-
const activeElement = document.activeElement
|
|
433
|
-
const menuItems = Array.from(this.items)
|
|
918
|
+
const activeElement = document.activeElement
|
|
919
|
+
const menuItems = Array.from(this.items)
|
|
434
920
|
|
|
435
|
-
return menuItems.indexOf(activeElement)
|
|
436
|
-
}
|
|
921
|
+
return menuItems.indexOf(activeElement)
|
|
922
|
+
}
|
|
437
923
|
|
|
438
924
|
MOJFrontend.ButtonMenu.prototype.handleKeyDown = function (event) {
|
|
439
|
-
if (event.target
|
|
925
|
+
if (event.target === this.$menuToggle) {
|
|
440
926
|
switch (event.key) {
|
|
441
|
-
case
|
|
442
|
-
event.preventDefault()
|
|
443
|
-
this.openMenu()
|
|
444
|
-
break
|
|
445
|
-
case
|
|
446
|
-
event.preventDefault()
|
|
447
|
-
this.openMenu(this.items.length - 1)
|
|
448
|
-
break
|
|
927
|
+
case 'ArrowDown':
|
|
928
|
+
event.preventDefault()
|
|
929
|
+
this.openMenu()
|
|
930
|
+
break
|
|
931
|
+
case 'ArrowUp':
|
|
932
|
+
event.preventDefault()
|
|
933
|
+
this.openMenu(this.items.length - 1)
|
|
934
|
+
break
|
|
449
935
|
}
|
|
450
936
|
}
|
|
451
937
|
|
|
452
938
|
if (this.$menu.contains(event.target) && this.isOpen()) {
|
|
453
939
|
switch (event.key) {
|
|
454
|
-
case
|
|
455
|
-
event.preventDefault()
|
|
940
|
+
case 'ArrowDown':
|
|
941
|
+
event.preventDefault()
|
|
456
942
|
if (this.currentFocusIndex() !== -1) {
|
|
457
|
-
this.focusItem(this.currentFocusIndex() + 1)
|
|
943
|
+
this.focusItem(this.currentFocusIndex() + 1)
|
|
458
944
|
}
|
|
459
|
-
break
|
|
460
|
-
case
|
|
461
|
-
event.preventDefault()
|
|
945
|
+
break
|
|
946
|
+
case 'ArrowUp':
|
|
947
|
+
event.preventDefault()
|
|
462
948
|
if (this.currentFocusIndex() !== -1) {
|
|
463
|
-
this.focusItem(this.currentFocusIndex() - 1)
|
|
949
|
+
this.focusItem(this.currentFocusIndex() - 1)
|
|
464
950
|
}
|
|
465
|
-
break
|
|
466
|
-
case
|
|
467
|
-
event.preventDefault()
|
|
468
|
-
this.focusItem(0)
|
|
469
|
-
break
|
|
470
|
-
case
|
|
471
|
-
event.preventDefault()
|
|
472
|
-
this.focusItem(this.items.length - 1)
|
|
473
|
-
break
|
|
951
|
+
break
|
|
952
|
+
case 'Home':
|
|
953
|
+
event.preventDefault()
|
|
954
|
+
this.focusItem(0)
|
|
955
|
+
break
|
|
956
|
+
case 'End':
|
|
957
|
+
event.preventDefault()
|
|
958
|
+
this.focusItem(this.items.length - 1)
|
|
959
|
+
break
|
|
474
960
|
}
|
|
475
961
|
}
|
|
476
962
|
|
|
477
|
-
if (event.key
|
|
478
|
-
this.closeMenu()
|
|
963
|
+
if (event.key === 'Escape' && this.isOpen()) {
|
|
964
|
+
this.closeMenu()
|
|
479
965
|
}
|
|
480
|
-
if (event.key
|
|
481
|
-
this.closeMenu(false)
|
|
966
|
+
if (event.key === 'Tab' && this.isOpen()) {
|
|
967
|
+
this.closeMenu(false)
|
|
482
968
|
}
|
|
483
|
-
}
|
|
969
|
+
}
|
|
484
970
|
|
|
485
971
|
/**
|
|
486
972
|
* Parse dataset
|
|
@@ -488,23 +974,23 @@ MOJFrontend.ButtonMenu.prototype.handleKeyDown = function (event) {
|
|
|
488
974
|
* Loop over an object and normalise each value using {@link normaliseString},
|
|
489
975
|
* optionally expanding nested `i18n.field`
|
|
490
976
|
*
|
|
491
|
-
* @param {
|
|
977
|
+
* @param {Schema} schema - component schema
|
|
492
978
|
* @param {DOMStringMap} dataset - HTML element dataset
|
|
493
|
-
* @returns {
|
|
979
|
+
* @returns {object} Normalised dataset
|
|
494
980
|
*/
|
|
495
981
|
MOJFrontend.ButtonMenu.prototype.parseDataset = function (schema, dataset) {
|
|
496
|
-
const parsed = {}
|
|
982
|
+
const parsed = {}
|
|
497
983
|
|
|
498
|
-
for (const [field,
|
|
984
|
+
for (const [field, ,] of Object.entries(schema.properties)) {
|
|
499
985
|
if (field in dataset) {
|
|
500
986
|
if (dataset[field]) {
|
|
501
|
-
parsed[field] = dataset[field]
|
|
987
|
+
parsed[field] = dataset[field]
|
|
502
988
|
}
|
|
503
989
|
}
|
|
504
990
|
}
|
|
505
991
|
|
|
506
|
-
return parsed
|
|
507
|
-
}
|
|
992
|
+
return parsed
|
|
993
|
+
}
|
|
508
994
|
|
|
509
995
|
/**
|
|
510
996
|
* Config merging function
|
|
@@ -516,28 +1002,28 @@ MOJFrontend.ButtonMenu.prototype.parseDataset = function (schema, dataset) {
|
|
|
516
1002
|
* @returns {{ [key: string]: unknown }} A merged config object
|
|
517
1003
|
*/
|
|
518
1004
|
MOJFrontend.ButtonMenu.prototype.mergeConfigs = function (...configObjects) {
|
|
519
|
-
const formattedConfigObject = {}
|
|
1005
|
+
const formattedConfigObject = {}
|
|
520
1006
|
|
|
521
1007
|
// Loop through each of the passed objects
|
|
522
1008
|
for (const configObject of configObjects) {
|
|
523
1009
|
for (const key of Object.keys(configObject)) {
|
|
524
|
-
const option = formattedConfigObject[key]
|
|
525
|
-
const override = configObject[key]
|
|
1010
|
+
const option = formattedConfigObject[key]
|
|
1011
|
+
const override = configObject[key]
|
|
526
1012
|
|
|
527
1013
|
// Push their keys one-by-one into formattedConfigObject. Any duplicate
|
|
528
1014
|
// keys with object values will be merged, otherwise the new value will
|
|
529
1015
|
// override the existing value.
|
|
530
|
-
if (typeof option ===
|
|
1016
|
+
if (typeof option === 'object' && typeof override === 'object') {
|
|
531
1017
|
// @ts-expect-error Index signature for type 'string' is missing
|
|
532
|
-
formattedConfigObject[key] = this.mergeConfigs(option, override)
|
|
1018
|
+
formattedConfigObject[key] = this.mergeConfigs(option, override)
|
|
533
1019
|
} else {
|
|
534
|
-
formattedConfigObject[key] = override
|
|
1020
|
+
formattedConfigObject[key] = override
|
|
535
1021
|
}
|
|
536
1022
|
}
|
|
537
1023
|
}
|
|
538
1024
|
|
|
539
|
-
return formattedConfigObject
|
|
540
|
-
}
|
|
1025
|
+
return formattedConfigObject
|
|
1026
|
+
}
|
|
541
1027
|
|
|
542
1028
|
/**
|
|
543
1029
|
* Schema for component config
|
|
@@ -557,234 +1043,232 @@ MOJFrontend.ButtonMenu.prototype.mergeConfigs = function (...configObjects) {
|
|
|
557
1043
|
* Datepicker config
|
|
558
1044
|
*
|
|
559
1045
|
* @typedef {object} DatepickerConfig
|
|
560
|
-
* @property {string}
|
|
561
|
-
* @property {string}
|
|
1046
|
+
* @property {string} [excludedDates] - Dates that cannot be selected
|
|
1047
|
+
* @property {string} [excludedDays] - Days that cannot be selected
|
|
562
1048
|
* @property {boolean} [leadingZeroes] - Whether to add leading zeroes when populating the field
|
|
563
|
-
* @property {string}
|
|
564
|
-
* @property {string}
|
|
565
|
-
* @property {string}
|
|
1049
|
+
* @property {string} [minDate] - The earliest available date
|
|
1050
|
+
* @property {string} [maxDate] - The latest available date
|
|
1051
|
+
* @property {string} [weekStartDay] - First day of the week in calendar view
|
|
566
1052
|
*/
|
|
567
1053
|
|
|
568
1054
|
/**
|
|
569
1055
|
* @param {HTMLElement} $module - HTML element
|
|
570
1056
|
* @param {DatepickerConfig} config - config object
|
|
571
|
-
* @
|
|
1057
|
+
* @class
|
|
572
1058
|
*/
|
|
573
1059
|
function Datepicker($module, config = {}) {
|
|
574
1060
|
if (!$module) {
|
|
575
|
-
return this
|
|
1061
|
+
return this
|
|
576
1062
|
}
|
|
577
1063
|
|
|
578
1064
|
const schema = Object.freeze({
|
|
579
1065
|
properties: {
|
|
580
|
-
excludedDates: { type:
|
|
581
|
-
excludedDays: { type:
|
|
582
|
-
leadingZeros: { type:
|
|
583
|
-
maxDate: { type:
|
|
584
|
-
minDate: { type:
|
|
585
|
-
weekStartDay: { type:
|
|
586
|
-
}
|
|
587
|
-
})
|
|
1066
|
+
excludedDates: { type: 'string' },
|
|
1067
|
+
excludedDays: { type: 'string' },
|
|
1068
|
+
leadingZeros: { type: 'string' },
|
|
1069
|
+
maxDate: { type: 'string' },
|
|
1070
|
+
minDate: { type: 'string' },
|
|
1071
|
+
weekStartDay: { type: 'string' }
|
|
1072
|
+
}
|
|
1073
|
+
})
|
|
588
1074
|
|
|
589
1075
|
const defaults = {
|
|
590
1076
|
leadingZeros: false,
|
|
591
|
-
weekStartDay:
|
|
592
|
-
}
|
|
1077
|
+
weekStartDay: 'monday'
|
|
1078
|
+
}
|
|
593
1079
|
|
|
594
1080
|
// data attributes override JS config, which overrides defaults
|
|
595
1081
|
this.config = this.mergeConfigs(
|
|
596
1082
|
defaults,
|
|
597
1083
|
config,
|
|
598
|
-
this.parseDataset(schema, $module.dataset)
|
|
599
|
-
)
|
|
1084
|
+
this.parseDataset(schema, $module.dataset)
|
|
1085
|
+
)
|
|
600
1086
|
|
|
601
1087
|
this.dayLabels = [
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
]
|
|
1088
|
+
'Monday',
|
|
1089
|
+
'Tuesday',
|
|
1090
|
+
'Wednesday',
|
|
1091
|
+
'Thursday',
|
|
1092
|
+
'Friday',
|
|
1093
|
+
'Saturday',
|
|
1094
|
+
'Sunday'
|
|
1095
|
+
]
|
|
610
1096
|
|
|
611
1097
|
this.monthLabels = [
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
]
|
|
625
|
-
|
|
626
|
-
this.currentDate = new Date()
|
|
627
|
-
this.currentDate.setHours(0, 0, 0, 0)
|
|
628
|
-
this.calendarDays = []
|
|
629
|
-
this.excludedDates = []
|
|
630
|
-
this.excludedDays = []
|
|
631
|
-
|
|
632
|
-
this.buttonClass =
|
|
633
|
-
this.selectedDayButtonClass =
|
|
634
|
-
this.currentDayButtonClass =
|
|
635
|
-
this.todayButtonClass =
|
|
636
|
-
|
|
637
|
-
this.$module = $module
|
|
638
|
-
this.$input = $module.querySelector(
|
|
1098
|
+
'January',
|
|
1099
|
+
'February',
|
|
1100
|
+
'March',
|
|
1101
|
+
'April',
|
|
1102
|
+
'May',
|
|
1103
|
+
'June',
|
|
1104
|
+
'July',
|
|
1105
|
+
'August',
|
|
1106
|
+
'September',
|
|
1107
|
+
'October',
|
|
1108
|
+
'November',
|
|
1109
|
+
'December'
|
|
1110
|
+
]
|
|
1111
|
+
|
|
1112
|
+
this.currentDate = new Date()
|
|
1113
|
+
this.currentDate.setHours(0, 0, 0, 0)
|
|
1114
|
+
this.calendarDays = []
|
|
1115
|
+
this.excludedDates = []
|
|
1116
|
+
this.excludedDays = []
|
|
1117
|
+
|
|
1118
|
+
this.buttonClass = 'moj-datepicker__button'
|
|
1119
|
+
this.selectedDayButtonClass = 'moj-datepicker__button--selected'
|
|
1120
|
+
this.currentDayButtonClass = 'moj-datepicker__button--current'
|
|
1121
|
+
this.todayButtonClass = 'moj-datepicker__button--today'
|
|
1122
|
+
|
|
1123
|
+
this.$module = $module
|
|
1124
|
+
this.$input = $module.querySelector('.moj-js-datepicker-input')
|
|
639
1125
|
}
|
|
640
1126
|
|
|
641
1127
|
Datepicker.prototype.init = function () {
|
|
642
1128
|
// Check that required elements are present
|
|
643
1129
|
if (!this.$input) {
|
|
644
|
-
return
|
|
1130
|
+
return
|
|
645
1131
|
}
|
|
646
1132
|
if (this.$module.dataset.initialized) {
|
|
647
|
-
return
|
|
1133
|
+
return
|
|
648
1134
|
}
|
|
649
1135
|
|
|
650
|
-
this.setOptions()
|
|
651
|
-
this.initControls()
|
|
652
|
-
this.$module.setAttribute(
|
|
653
|
-
}
|
|
1136
|
+
this.setOptions()
|
|
1137
|
+
this.initControls()
|
|
1138
|
+
this.$module.setAttribute('data-initialized', 'true')
|
|
1139
|
+
}
|
|
654
1140
|
|
|
655
1141
|
Datepicker.prototype.initControls = function () {
|
|
656
|
-
this.id = `datepicker-${this.$input.id}
|
|
1142
|
+
this.id = `datepicker-${this.$input.id}`
|
|
657
1143
|
|
|
658
|
-
this.$dialog = this.createDialog()
|
|
659
|
-
this.createCalendarHeaders()
|
|
1144
|
+
this.$dialog = this.createDialog()
|
|
1145
|
+
this.createCalendarHeaders()
|
|
660
1146
|
|
|
661
|
-
const $componentWrapper = document.createElement(
|
|
662
|
-
const $inputWrapper = document.createElement(
|
|
663
|
-
$componentWrapper.classList.add(
|
|
664
|
-
$inputWrapper.classList.add(
|
|
1147
|
+
const $componentWrapper = document.createElement('div')
|
|
1148
|
+
const $inputWrapper = document.createElement('div')
|
|
1149
|
+
$componentWrapper.classList.add('moj-datepicker__wrapper')
|
|
1150
|
+
$inputWrapper.classList.add('govuk-input__wrapper')
|
|
665
1151
|
|
|
666
|
-
this.$input.parentNode.insertBefore($componentWrapper, this.$input)
|
|
667
|
-
$componentWrapper.appendChild($inputWrapper)
|
|
668
|
-
$inputWrapper.appendChild(this.$input)
|
|
1152
|
+
this.$input.parentNode.insertBefore($componentWrapper, this.$input)
|
|
1153
|
+
$componentWrapper.appendChild($inputWrapper)
|
|
1154
|
+
$inputWrapper.appendChild(this.$input)
|
|
669
1155
|
|
|
670
|
-
$inputWrapper.insertAdjacentHTML(
|
|
671
|
-
$componentWrapper.insertAdjacentElement(
|
|
1156
|
+
$inputWrapper.insertAdjacentHTML('beforeend', this.toggleTemplate())
|
|
1157
|
+
$componentWrapper.insertAdjacentElement('beforeend', this.$dialog)
|
|
672
1158
|
|
|
673
|
-
this.$calendarButton = this.$module.querySelector(
|
|
674
|
-
".moj-js-datepicker-toggle",
|
|
675
|
-
);
|
|
1159
|
+
this.$calendarButton = this.$module.querySelector('.moj-js-datepicker-toggle')
|
|
676
1160
|
this.$dialogTitle = this.$dialog.querySelector(
|
|
677
|
-
|
|
678
|
-
)
|
|
1161
|
+
'.moj-js-datepicker-month-year'
|
|
1162
|
+
)
|
|
679
1163
|
|
|
680
|
-
this.createCalendar()
|
|
1164
|
+
this.createCalendar()
|
|
681
1165
|
|
|
682
1166
|
this.$prevMonthButton = this.$dialog.querySelector(
|
|
683
|
-
|
|
684
|
-
)
|
|
1167
|
+
'.moj-js-datepicker-prev-month'
|
|
1168
|
+
)
|
|
685
1169
|
this.$prevYearButton = this.$dialog.querySelector(
|
|
686
|
-
|
|
687
|
-
)
|
|
1170
|
+
'.moj-js-datepicker-prev-year'
|
|
1171
|
+
)
|
|
688
1172
|
this.$nextMonthButton = this.$dialog.querySelector(
|
|
689
|
-
|
|
690
|
-
)
|
|
1173
|
+
'.moj-js-datepicker-next-month'
|
|
1174
|
+
)
|
|
691
1175
|
this.$nextYearButton = this.$dialog.querySelector(
|
|
692
|
-
|
|
693
|
-
)
|
|
694
|
-
this.$cancelButton = this.$dialog.querySelector(
|
|
695
|
-
this.$okButton = this.$dialog.querySelector(
|
|
1176
|
+
'.moj-js-datepicker-next-year'
|
|
1177
|
+
)
|
|
1178
|
+
this.$cancelButton = this.$dialog.querySelector('.moj-js-datepicker-cancel')
|
|
1179
|
+
this.$okButton = this.$dialog.querySelector('.moj-js-datepicker-ok')
|
|
696
1180
|
|
|
697
1181
|
// add event listeners
|
|
698
|
-
this.$prevMonthButton.addEventListener(
|
|
699
|
-
this.focusPreviousMonth(event, false)
|
|
700
|
-
)
|
|
701
|
-
this.$prevYearButton.addEventListener(
|
|
702
|
-
this.focusPreviousYear(event, false)
|
|
703
|
-
)
|
|
704
|
-
this.$nextMonthButton.addEventListener(
|
|
705
|
-
this.focusNextMonth(event, false)
|
|
706
|
-
)
|
|
707
|
-
this.$nextYearButton.addEventListener(
|
|
708
|
-
this.focusNextYear(event, false)
|
|
709
|
-
)
|
|
710
|
-
this.$cancelButton.addEventListener(
|
|
711
|
-
event.preventDefault()
|
|
712
|
-
this.closeDialog(event)
|
|
713
|
-
})
|
|
714
|
-
this.$okButton.addEventListener(
|
|
715
|
-
this.selectDate(this.currentDate)
|
|
716
|
-
})
|
|
1182
|
+
this.$prevMonthButton.addEventListener('click', (event) =>
|
|
1183
|
+
this.focusPreviousMonth(event, false)
|
|
1184
|
+
)
|
|
1185
|
+
this.$prevYearButton.addEventListener('click', (event) =>
|
|
1186
|
+
this.focusPreviousYear(event, false)
|
|
1187
|
+
)
|
|
1188
|
+
this.$nextMonthButton.addEventListener('click', (event) =>
|
|
1189
|
+
this.focusNextMonth(event, false)
|
|
1190
|
+
)
|
|
1191
|
+
this.$nextYearButton.addEventListener('click', (event) =>
|
|
1192
|
+
this.focusNextYear(event, false)
|
|
1193
|
+
)
|
|
1194
|
+
this.$cancelButton.addEventListener('click', (event) => {
|
|
1195
|
+
event.preventDefault()
|
|
1196
|
+
this.closeDialog(event)
|
|
1197
|
+
})
|
|
1198
|
+
this.$okButton.addEventListener('click', () => {
|
|
1199
|
+
this.selectDate(this.currentDate)
|
|
1200
|
+
})
|
|
717
1201
|
|
|
718
1202
|
const dialogButtons = this.$dialog.querySelectorAll(
|
|
719
|
-
'button:not([disabled="true"])'
|
|
720
|
-
)
|
|
1203
|
+
'button:not([disabled="true"])'
|
|
1204
|
+
)
|
|
721
1205
|
// eslint-disable-next-line prefer-destructuring
|
|
722
|
-
this.$firstButtonInDialog = dialogButtons[0]
|
|
723
|
-
this.$lastButtonInDialog = dialogButtons[dialogButtons.length - 1]
|
|
724
|
-
this.$firstButtonInDialog.addEventListener(
|
|
725
|
-
this.firstButtonKeydown(event)
|
|
726
|
-
)
|
|
727
|
-
this.$lastButtonInDialog.addEventListener(
|
|
728
|
-
this.lastButtonKeydown(event)
|
|
729
|
-
)
|
|
730
|
-
|
|
731
|
-
this.$calendarButton.addEventListener(
|
|
732
|
-
this.toggleDialog(event)
|
|
733
|
-
)
|
|
734
|
-
|
|
735
|
-
this.$dialog.addEventListener(
|
|
736
|
-
if (event.key ===
|
|
737
|
-
this.closeDialog()
|
|
738
|
-
event.preventDefault()
|
|
739
|
-
event.stopPropagation()
|
|
1206
|
+
this.$firstButtonInDialog = dialogButtons[0]
|
|
1207
|
+
this.$lastButtonInDialog = dialogButtons[dialogButtons.length - 1]
|
|
1208
|
+
this.$firstButtonInDialog.addEventListener('keydown', (event) =>
|
|
1209
|
+
this.firstButtonKeydown(event)
|
|
1210
|
+
)
|
|
1211
|
+
this.$lastButtonInDialog.addEventListener('keydown', (event) =>
|
|
1212
|
+
this.lastButtonKeydown(event)
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1215
|
+
this.$calendarButton.addEventListener('click', (event) =>
|
|
1216
|
+
this.toggleDialog(event)
|
|
1217
|
+
)
|
|
1218
|
+
|
|
1219
|
+
this.$dialog.addEventListener('keydown', (event) => {
|
|
1220
|
+
if (event.key === 'Escape') {
|
|
1221
|
+
this.closeDialog()
|
|
1222
|
+
event.preventDefault()
|
|
1223
|
+
event.stopPropagation()
|
|
740
1224
|
}
|
|
741
|
-
})
|
|
1225
|
+
})
|
|
742
1226
|
|
|
743
|
-
document.body.addEventListener(
|
|
744
|
-
this.backgroundClick(event)
|
|
745
|
-
)
|
|
1227
|
+
document.body.addEventListener('mouseup', (event) =>
|
|
1228
|
+
this.backgroundClick(event)
|
|
1229
|
+
)
|
|
746
1230
|
|
|
747
1231
|
// populates calendar with initial dates, avoids Wave errors about null buttons
|
|
748
|
-
this.updateCalendar()
|
|
749
|
-
}
|
|
1232
|
+
this.updateCalendar()
|
|
1233
|
+
}
|
|
750
1234
|
|
|
751
1235
|
Datepicker.prototype.createDialog = function () {
|
|
752
|
-
const titleId = `datepicker-title-${this.$input.id}
|
|
753
|
-
const $dialog = document.createElement(
|
|
754
|
-
|
|
755
|
-
$dialog.id = this.id
|
|
756
|
-
$dialog.setAttribute(
|
|
757
|
-
$dialog.setAttribute(
|
|
758
|
-
$dialog.setAttribute(
|
|
759
|
-
$dialog.setAttribute(
|
|
760
|
-
$dialog.innerHTML = this.dialogTemplate(titleId)
|
|
761
|
-
$dialog.hidden = true
|
|
762
|
-
|
|
763
|
-
return $dialog
|
|
764
|
-
}
|
|
1236
|
+
const titleId = `datepicker-title-${this.$input.id}`
|
|
1237
|
+
const $dialog = document.createElement('div')
|
|
1238
|
+
|
|
1239
|
+
$dialog.id = this.id
|
|
1240
|
+
$dialog.setAttribute('class', 'moj-datepicker__dialog')
|
|
1241
|
+
$dialog.setAttribute('role', 'dialog')
|
|
1242
|
+
$dialog.setAttribute('aria-modal', 'true')
|
|
1243
|
+
$dialog.setAttribute('aria-labelledby', titleId)
|
|
1244
|
+
$dialog.innerHTML = this.dialogTemplate(titleId)
|
|
1245
|
+
$dialog.hidden = true
|
|
1246
|
+
|
|
1247
|
+
return $dialog
|
|
1248
|
+
}
|
|
765
1249
|
|
|
766
1250
|
Datepicker.prototype.createCalendar = function () {
|
|
767
|
-
const $tbody = this.$dialog.querySelector(
|
|
768
|
-
let dayCount = 0
|
|
1251
|
+
const $tbody = this.$dialog.querySelector('tbody')
|
|
1252
|
+
let dayCount = 0
|
|
769
1253
|
for (let i = 0; i < 6; i++) {
|
|
770
1254
|
// create row
|
|
771
|
-
const $row = $tbody.insertRow(i)
|
|
1255
|
+
const $row = $tbody.insertRow(i)
|
|
772
1256
|
|
|
773
1257
|
for (let j = 0; j < 7; j++) {
|
|
774
1258
|
// create cell (day)
|
|
775
|
-
const $cell = document.createElement(
|
|
776
|
-
const $dateButton = document.createElement(
|
|
1259
|
+
const $cell = document.createElement('td')
|
|
1260
|
+
const $dateButton = document.createElement('button')
|
|
777
1261
|
|
|
778
|
-
$cell.appendChild($dateButton)
|
|
779
|
-
$row.appendChild($cell)
|
|
1262
|
+
$cell.appendChild($dateButton)
|
|
1263
|
+
$row.appendChild($cell)
|
|
780
1264
|
|
|
781
|
-
const calendarDay = new DSCalendarDay($dateButton, dayCount, i, j, this)
|
|
782
|
-
calendarDay.init()
|
|
783
|
-
this.calendarDays.push(calendarDay)
|
|
784
|
-
dayCount
|
|
1265
|
+
const calendarDay = new DSCalendarDay($dateButton, dayCount, i, j, this)
|
|
1266
|
+
calendarDay.init()
|
|
1267
|
+
this.calendarDays.push(calendarDay)
|
|
1268
|
+
dayCount++
|
|
785
1269
|
}
|
|
786
1270
|
}
|
|
787
|
-
}
|
|
1271
|
+
}
|
|
788
1272
|
|
|
789
1273
|
Datepicker.prototype.toggleTemplate = function () {
|
|
790
1274
|
return `<button class="moj-datepicker__toggle moj-js-datepicker-toggle" type="button" aria-haspopup="dialog" aria-controls="${this.id}" aria-expanded="false">
|
|
@@ -799,14 +1283,14 @@ Datepicker.prototype.toggleTemplate = function () {
|
|
|
799
1283
|
<rect x="3.66669" width="1.46667" height="5.13333" rx="0.733333" fill="currentColor"></rect>
|
|
800
1284
|
<rect x="16.8667" width="1.46667" height="5.13333" rx="0.733333" fill="currentColor"></rect>
|
|
801
1285
|
</svg>
|
|
802
|
-
</button
|
|
803
|
-
}
|
|
1286
|
+
</button>`
|
|
1287
|
+
}
|
|
804
1288
|
|
|
805
1289
|
/**
|
|
806
1290
|
* HTML template for calendar dialog
|
|
807
1291
|
*
|
|
808
1292
|
* @param {string} [titleId] - Id attribute for dialog title
|
|
809
|
-
* @
|
|
1293
|
+
* @returns {string}
|
|
810
1294
|
*/
|
|
811
1295
|
Datepicker.prototype.dialogTemplate = function (titleId) {
|
|
812
1296
|
return `<div class="moj-datepicker__dialog-header">
|
|
@@ -858,213 +1342,220 @@ Datepicker.prototype.dialogTemplate = function (titleId) {
|
|
|
858
1342
|
<div class="govuk-button-group">
|
|
859
1343
|
<button type="button" class="govuk-button moj-js-datepicker-ok">Select</button>
|
|
860
1344
|
<button type="button" class="govuk-button govuk-button--secondary moj-js-datepicker-cancel">Close</button>
|
|
861
|
-
</div
|
|
862
|
-
}
|
|
1345
|
+
</div>`
|
|
1346
|
+
}
|
|
863
1347
|
|
|
864
1348
|
Datepicker.prototype.createCalendarHeaders = function () {
|
|
865
1349
|
this.dayLabels.forEach((day) => {
|
|
866
|
-
const html = `<th scope="col"><span aria-hidden="true">${day.substring(0, 3)}</span><span class="govuk-visually-hidden">${day}</span></th
|
|
867
|
-
const $headerRow = this.$dialog.querySelector(
|
|
868
|
-
$headerRow.insertAdjacentHTML(
|
|
869
|
-
})
|
|
870
|
-
}
|
|
1350
|
+
const html = `<th scope="col"><span aria-hidden="true">${day.substring(0, 3)}</span><span class="govuk-visually-hidden">${day}</span></th>`
|
|
1351
|
+
const $headerRow = this.$dialog.querySelector('thead > tr')
|
|
1352
|
+
$headerRow.insertAdjacentHTML('beforeend', html)
|
|
1353
|
+
})
|
|
1354
|
+
}
|
|
871
1355
|
|
|
872
1356
|
/**
|
|
873
1357
|
* Pads given number with leading zeros
|
|
874
1358
|
*
|
|
875
1359
|
* @param {number} value - The value to be padded
|
|
876
1360
|
* @param {number} length - The length in characters of the output
|
|
877
|
-
* @
|
|
1361
|
+
* @returns {string}
|
|
878
1362
|
*/
|
|
879
1363
|
Datepicker.prototype.leadingZeros = function (value, length = 2) {
|
|
880
|
-
let ret = value.toString()
|
|
1364
|
+
let ret = value.toString()
|
|
881
1365
|
|
|
882
1366
|
while (ret.length < length) {
|
|
883
|
-
ret = `0${ret}
|
|
1367
|
+
ret = `0${ret}`
|
|
884
1368
|
}
|
|
885
1369
|
|
|
886
|
-
return ret
|
|
887
|
-
}
|
|
1370
|
+
return ret
|
|
1371
|
+
}
|
|
888
1372
|
|
|
889
1373
|
Datepicker.prototype.setOptions = function () {
|
|
890
|
-
this.setMinAndMaxDatesOnCalendar()
|
|
891
|
-
this.setExcludedDates()
|
|
892
|
-
this.setExcludedDays()
|
|
893
|
-
this.setLeadingZeros()
|
|
894
|
-
this.setWeekStartDay()
|
|
895
|
-
}
|
|
1374
|
+
this.setMinAndMaxDatesOnCalendar()
|
|
1375
|
+
this.setExcludedDates()
|
|
1376
|
+
this.setExcludedDays()
|
|
1377
|
+
this.setLeadingZeros()
|
|
1378
|
+
this.setWeekStartDay()
|
|
1379
|
+
}
|
|
896
1380
|
|
|
897
1381
|
Datepicker.prototype.setMinAndMaxDatesOnCalendar = function () {
|
|
898
1382
|
if (this.config.minDate) {
|
|
899
|
-
this.minDate = this.formattedDateFromString(this.config.minDate, null)
|
|
1383
|
+
this.minDate = this.formattedDateFromString(this.config.minDate, null)
|
|
900
1384
|
if (this.minDate && this.currentDate < this.minDate) {
|
|
901
|
-
this.currentDate = this.minDate
|
|
1385
|
+
this.currentDate = this.minDate
|
|
902
1386
|
}
|
|
903
1387
|
}
|
|
904
1388
|
|
|
905
1389
|
if (this.config.maxDate) {
|
|
906
|
-
this.maxDate = this.formattedDateFromString(this.config.maxDate, null)
|
|
1390
|
+
this.maxDate = this.formattedDateFromString(this.config.maxDate, null)
|
|
907
1391
|
if (this.maxDate && this.currentDate > this.maxDate) {
|
|
908
|
-
this.currentDate = this.maxDate
|
|
1392
|
+
this.currentDate = this.maxDate
|
|
909
1393
|
}
|
|
910
1394
|
}
|
|
911
|
-
}
|
|
1395
|
+
}
|
|
912
1396
|
|
|
913
1397
|
Datepicker.prototype.setExcludedDates = function () {
|
|
914
1398
|
if (this.config.excludedDates) {
|
|
915
1399
|
this.excludedDates = this.config.excludedDates
|
|
916
|
-
.replace(/\s+/,
|
|
917
|
-
.split(
|
|
1400
|
+
.replace(/\s+/, ' ')
|
|
1401
|
+
.split(' ')
|
|
918
1402
|
.map((item) => {
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
.split("-")
|
|
923
|
-
.map((d) => this.formattedDateFromString(d, null));
|
|
924
|
-
if (startDate && endDate) {
|
|
925
|
-
const date = new Date(startDate.getTime());
|
|
926
|
-
const dates = [];
|
|
927
|
-
while (date <= endDate) {
|
|
928
|
-
dates.push(new Date(date));
|
|
929
|
-
date.setDate(date.getDate() + 1);
|
|
930
|
-
}
|
|
931
|
-
return dates;
|
|
932
|
-
}
|
|
933
|
-
} else {
|
|
934
|
-
return this.formattedDateFromString(item, null);
|
|
935
|
-
}
|
|
1403
|
+
return item.includes('-')
|
|
1404
|
+
? this.parseDateRangeString(item)
|
|
1405
|
+
: this.formattedDateFromString(item)
|
|
936
1406
|
})
|
|
937
1407
|
.flat()
|
|
938
|
-
.filter((item) => item)
|
|
1408
|
+
.filter((item) => item)
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/*
|
|
1413
|
+
* Parses a daterange string into an array of dates
|
|
1414
|
+
* @param {String} datestring - A daterange string in the format "dd/mm/yyyy-dd/mm/yyyy"
|
|
1415
|
+
* @returns {Date[]}
|
|
1416
|
+
*/
|
|
1417
|
+
Datepicker.prototype.parseDateRangeString = function (datestring) {
|
|
1418
|
+
const dates = []
|
|
1419
|
+
const [startDate, endDate] = datestring
|
|
1420
|
+
.split('-')
|
|
1421
|
+
.map((d) => this.formattedDateFromString(d, null))
|
|
1422
|
+
|
|
1423
|
+
if (startDate && endDate) {
|
|
1424
|
+
const date = new Date(startDate.getTime())
|
|
1425
|
+
/* eslint-disable no-unmodified-loop-condition */
|
|
1426
|
+
while (date <= endDate) {
|
|
1427
|
+
dates.push(new Date(date))
|
|
1428
|
+
date.setDate(date.getDate() + 1)
|
|
1429
|
+
}
|
|
1430
|
+
/* eslint-enable no-unmodified-loop-condition */
|
|
939
1431
|
}
|
|
940
|
-
|
|
1432
|
+
return dates
|
|
1433
|
+
}
|
|
941
1434
|
|
|
942
1435
|
Datepicker.prototype.setExcludedDays = function () {
|
|
943
1436
|
if (this.config.excludedDays) {
|
|
944
1437
|
// lowercase and arrange dayLabels to put indexOf sunday == 0 for comparison
|
|
945
1438
|
// with getDay() function
|
|
946
|
-
|
|
947
|
-
if (this.config.weekStartDay ===
|
|
948
|
-
weekDays.unshift(weekDays.pop())
|
|
1439
|
+
const weekDays = this.dayLabels.map((item) => item.toLowerCase())
|
|
1440
|
+
if (this.config.weekStartDay === 'monday') {
|
|
1441
|
+
weekDays.unshift(weekDays.pop())
|
|
949
1442
|
}
|
|
950
1443
|
|
|
951
1444
|
this.excludedDays = this.config.excludedDays
|
|
952
|
-
.replace(/\s+/,
|
|
1445
|
+
.replace(/\s+/, ' ')
|
|
953
1446
|
.toLowerCase()
|
|
954
|
-
.split(
|
|
1447
|
+
.split(' ')
|
|
955
1448
|
.map((item) => weekDays.indexOf(item))
|
|
956
|
-
.filter((item) => item !== -1)
|
|
1449
|
+
.filter((item) => item !== -1)
|
|
957
1450
|
}
|
|
958
|
-
}
|
|
1451
|
+
}
|
|
959
1452
|
|
|
960
1453
|
Datepicker.prototype.setLeadingZeros = function () {
|
|
961
|
-
if (typeof this.config.leadingZeros !==
|
|
962
|
-
if (this.config.leadingZeros.toLowerCase() ===
|
|
963
|
-
this.config.leadingZeros = true
|
|
964
|
-
return
|
|
1454
|
+
if (typeof this.config.leadingZeros !== 'boolean') {
|
|
1455
|
+
if (this.config.leadingZeros.toLowerCase() === 'true') {
|
|
1456
|
+
this.config.leadingZeros = true
|
|
1457
|
+
return
|
|
965
1458
|
}
|
|
966
|
-
if (this.config.leadingZeros.toLowerCase() ===
|
|
967
|
-
this.config.leadingZeros = false
|
|
968
|
-
return;
|
|
1459
|
+
if (this.config.leadingZeros.toLowerCase() === 'false') {
|
|
1460
|
+
this.config.leadingZeros = false
|
|
969
1461
|
}
|
|
970
1462
|
}
|
|
971
|
-
}
|
|
1463
|
+
}
|
|
972
1464
|
|
|
973
1465
|
Datepicker.prototype.setWeekStartDay = function () {
|
|
974
|
-
const weekStartDayParam = this.config.weekStartDay
|
|
975
|
-
if (weekStartDayParam
|
|
976
|
-
this.config.weekStartDay =
|
|
1466
|
+
const weekStartDayParam = this.config.weekStartDay
|
|
1467
|
+
if (weekStartDayParam && weekStartDayParam.toLowerCase() === 'sunday') {
|
|
1468
|
+
this.config.weekStartDay = 'sunday'
|
|
977
1469
|
// Rotate dayLabels array to put Sunday as the first item
|
|
978
|
-
this.dayLabels.unshift(this.dayLabels.pop())
|
|
1470
|
+
this.dayLabels.unshift(this.dayLabels.pop())
|
|
979
1471
|
} else {
|
|
980
|
-
this.config.weekStartDay =
|
|
1472
|
+
this.config.weekStartDay = 'monday'
|
|
981
1473
|
}
|
|
982
|
-
}
|
|
1474
|
+
}
|
|
983
1475
|
|
|
984
1476
|
/**
|
|
985
1477
|
* Determine if a date is selecteable
|
|
986
1478
|
*
|
|
987
1479
|
* @param {Date} date - the date to check
|
|
988
|
-
* @
|
|
989
|
-
*
|
|
1480
|
+
* @returns {boolean}
|
|
990
1481
|
*/
|
|
991
1482
|
Datepicker.prototype.isExcludedDate = function (date) {
|
|
992
1483
|
// This comparison does not work correctly - it will exclude the mindate itself
|
|
993
1484
|
// see: https://github.com/ministryofjustice/moj-frontend/issues/923
|
|
994
1485
|
if (this.minDate && this.minDate > date) {
|
|
995
|
-
return true
|
|
1486
|
+
return true
|
|
996
1487
|
}
|
|
997
1488
|
|
|
998
1489
|
// This comparison works as expected - the maxdate will not be excluded
|
|
999
1490
|
if (this.maxDate && this.maxDate < date) {
|
|
1000
|
-
return true
|
|
1491
|
+
return true
|
|
1001
1492
|
}
|
|
1002
1493
|
|
|
1003
1494
|
for (const excludedDate of this.excludedDates) {
|
|
1004
1495
|
if (date.toDateString() === excludedDate.toDateString()) {
|
|
1005
|
-
return true
|
|
1496
|
+
return true
|
|
1006
1497
|
}
|
|
1007
1498
|
}
|
|
1008
1499
|
|
|
1009
1500
|
if (this.excludedDays.includes(date.getDay())) {
|
|
1010
|
-
return true
|
|
1501
|
+
return true
|
|
1011
1502
|
}
|
|
1012
1503
|
|
|
1013
|
-
return false
|
|
1014
|
-
}
|
|
1504
|
+
return false
|
|
1505
|
+
}
|
|
1015
1506
|
|
|
1016
1507
|
/**
|
|
1017
1508
|
* Get a Date object from a string
|
|
1018
1509
|
*
|
|
1019
1510
|
* @param {string} dateString - string in the format d/m/yyyy dd/mm/yyyy
|
|
1020
1511
|
* @param {Date} fallback - date object to return if formatting fails
|
|
1021
|
-
* @
|
|
1512
|
+
* @returns {Date}
|
|
1022
1513
|
*/
|
|
1023
1514
|
Datepicker.prototype.formattedDateFromString = function (
|
|
1024
1515
|
dateString,
|
|
1025
|
-
fallback = new Date()
|
|
1516
|
+
fallback = new Date()
|
|
1026
1517
|
) {
|
|
1027
|
-
let formattedDate = null
|
|
1518
|
+
let formattedDate = null
|
|
1028
1519
|
// Accepts d/m/yyyy and dd/mm/yyyy
|
|
1029
|
-
const dateFormatPattern = /(\d{1,2})([-/,. ])(\d{1,2})\2(\d{4})
|
|
1520
|
+
const dateFormatPattern = /(\d{1,2})([-/,. ])(\d{1,2})\2(\d{4})/
|
|
1030
1521
|
|
|
1031
|
-
if (!dateFormatPattern.test(dateString)) return fallback
|
|
1522
|
+
if (!dateFormatPattern.test(dateString)) return fallback
|
|
1032
1523
|
|
|
1033
|
-
const match = dateString.match(dateFormatPattern)
|
|
1034
|
-
const day = match[1]
|
|
1035
|
-
const month = match[3]
|
|
1036
|
-
const year = match[4]
|
|
1524
|
+
const match = dateString.match(dateFormatPattern)
|
|
1525
|
+
const day = match[1]
|
|
1526
|
+
const month = match[3]
|
|
1527
|
+
const year = match[4]
|
|
1037
1528
|
|
|
1038
|
-
formattedDate = new Date(`${year}-${month}-${day}`)
|
|
1529
|
+
formattedDate = new Date(`${year}-${month}-${day}`)
|
|
1039
1530
|
if (formattedDate instanceof Date && !isNaN(formattedDate)) {
|
|
1040
|
-
return formattedDate
|
|
1531
|
+
return formattedDate
|
|
1041
1532
|
}
|
|
1042
|
-
return fallback
|
|
1043
|
-
}
|
|
1533
|
+
return fallback
|
|
1534
|
+
}
|
|
1044
1535
|
|
|
1045
1536
|
/**
|
|
1046
1537
|
* Get a formatted date string from a Date object
|
|
1047
1538
|
*
|
|
1048
1539
|
* @param {Date} date - date to format to a string
|
|
1049
|
-
* @
|
|
1540
|
+
* @returns {string}
|
|
1050
1541
|
*/
|
|
1051
1542
|
Datepicker.prototype.formattedDateFromDate = function (date) {
|
|
1052
1543
|
if (this.config.leadingZeros) {
|
|
1053
|
-
return `${this.leadingZeros(date.getDate())}/${this.leadingZeros(date.getMonth() + 1)}/${date.getFullYear()}
|
|
1054
|
-
} else {
|
|
1055
|
-
return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`;
|
|
1544
|
+
return `${this.leadingZeros(date.getDate())}/${this.leadingZeros(date.getMonth() + 1)}/${date.getFullYear()}`
|
|
1056
1545
|
}
|
|
1057
|
-
|
|
1546
|
+
|
|
1547
|
+
return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`
|
|
1548
|
+
}
|
|
1058
1549
|
|
|
1059
1550
|
/**
|
|
1060
1551
|
* Get a human readable date in the format Monday 2 March 2024
|
|
1061
1552
|
*
|
|
1062
|
-
* @param {Date} - date to format
|
|
1063
|
-
* @
|
|
1553
|
+
* @param {Date} date - date to format
|
|
1554
|
+
* @returns {string}
|
|
1064
1555
|
*/
|
|
1065
1556
|
Datepicker.prototype.formattedDateHuman = function (date) {
|
|
1066
|
-
return `${this.dayLabels[(date.getDay() + 6) % 7]} ${date.getDate()} ${this.monthLabels[date.getMonth()]} ${date.getFullYear()}
|
|
1067
|
-
}
|
|
1557
|
+
return `${this.dayLabels[(date.getDay() + 6) % 7]} ${date.getDate()} ${this.monthLabels[date.getMonth()]} ${date.getFullYear()}`
|
|
1558
|
+
}
|
|
1068
1559
|
|
|
1069
1560
|
Datepicker.prototype.backgroundClick = function (event) {
|
|
1070
1561
|
if (
|
|
@@ -1073,75 +1564,75 @@ Datepicker.prototype.backgroundClick = function (event) {
|
|
|
1073
1564
|
!this.$input.contains(event.target) &&
|
|
1074
1565
|
!this.$calendarButton.contains(event.target)
|
|
1075
1566
|
) {
|
|
1076
|
-
event.preventDefault()
|
|
1077
|
-
this.closeDialog()
|
|
1567
|
+
event.preventDefault()
|
|
1568
|
+
this.closeDialog()
|
|
1078
1569
|
}
|
|
1079
|
-
}
|
|
1570
|
+
}
|
|
1080
1571
|
|
|
1081
1572
|
Datepicker.prototype.firstButtonKeydown = function (event) {
|
|
1082
|
-
if (event.key ===
|
|
1083
|
-
this.$lastButtonInDialog.focus()
|
|
1084
|
-
event.preventDefault()
|
|
1573
|
+
if (event.key === 'Tab' && event.shiftKey) {
|
|
1574
|
+
this.$lastButtonInDialog.focus()
|
|
1575
|
+
event.preventDefault()
|
|
1085
1576
|
}
|
|
1086
|
-
}
|
|
1577
|
+
}
|
|
1087
1578
|
|
|
1088
1579
|
Datepicker.prototype.lastButtonKeydown = function (event) {
|
|
1089
|
-
if (event.key ===
|
|
1090
|
-
this.$firstButtonInDialog.focus()
|
|
1091
|
-
event.preventDefault()
|
|
1580
|
+
if (event.key === 'Tab' && !event.shiftKey) {
|
|
1581
|
+
this.$firstButtonInDialog.focus()
|
|
1582
|
+
event.preventDefault()
|
|
1092
1583
|
}
|
|
1093
|
-
}
|
|
1584
|
+
}
|
|
1094
1585
|
|
|
1095
1586
|
// render calendar
|
|
1096
1587
|
Datepicker.prototype.updateCalendar = function () {
|
|
1097
|
-
this.$dialogTitle.innerHTML = `${this.monthLabels[this.currentDate.getMonth()]} ${this.currentDate.getFullYear()}
|
|
1588
|
+
this.$dialogTitle.innerHTML = `${this.monthLabels[this.currentDate.getMonth()]} ${this.currentDate.getFullYear()}`
|
|
1098
1589
|
|
|
1099
|
-
const day = this.currentDate
|
|
1100
|
-
const firstOfMonth = new Date(day.getFullYear(), day.getMonth(), 1)
|
|
1101
|
-
let dayOfWeek
|
|
1590
|
+
const day = this.currentDate
|
|
1591
|
+
const firstOfMonth = new Date(day.getFullYear(), day.getMonth(), 1)
|
|
1592
|
+
let dayOfWeek
|
|
1102
1593
|
|
|
1103
|
-
if (this.config.weekStartDay ===
|
|
1104
|
-
dayOfWeek = firstOfMonth.getDay() === 0 ? 6 : firstOfMonth.getDay() - 1
|
|
1594
|
+
if (this.config.weekStartDay === 'monday') {
|
|
1595
|
+
dayOfWeek = firstOfMonth.getDay() === 0 ? 6 : firstOfMonth.getDay() - 1 // Change logic to make Monday first day of week, i.e. 0
|
|
1105
1596
|
} else {
|
|
1106
|
-
dayOfWeek = firstOfMonth.getDay()
|
|
1597
|
+
dayOfWeek = firstOfMonth.getDay()
|
|
1107
1598
|
}
|
|
1108
1599
|
|
|
1109
|
-
firstOfMonth.setDate(firstOfMonth.getDate() - dayOfWeek)
|
|
1600
|
+
firstOfMonth.setDate(firstOfMonth.getDate() - dayOfWeek)
|
|
1110
1601
|
|
|
1111
|
-
const thisDay = new Date(firstOfMonth)
|
|
1602
|
+
const thisDay = new Date(firstOfMonth)
|
|
1112
1603
|
|
|
1113
1604
|
// loop through our days
|
|
1114
1605
|
for (let i = 0; i < this.calendarDays.length; i++) {
|
|
1115
|
-
const hidden = thisDay.getMonth() !== day.getMonth()
|
|
1116
|
-
const disabled = this.isExcludedDate(thisDay)
|
|
1606
|
+
const hidden = thisDay.getMonth() !== day.getMonth()
|
|
1607
|
+
const disabled = this.isExcludedDate(thisDay)
|
|
1117
1608
|
|
|
1118
|
-
this.calendarDays[i].update(thisDay, hidden, disabled)
|
|
1609
|
+
this.calendarDays[i].update(thisDay, hidden, disabled)
|
|
1119
1610
|
|
|
1120
|
-
thisDay.setDate(thisDay.getDate() + 1)
|
|
1611
|
+
thisDay.setDate(thisDay.getDate() + 1)
|
|
1121
1612
|
}
|
|
1122
|
-
}
|
|
1613
|
+
}
|
|
1123
1614
|
|
|
1124
1615
|
Datepicker.prototype.setCurrentDate = function (focus = true) {
|
|
1125
|
-
const { currentDate } = this
|
|
1616
|
+
const { currentDate } = this
|
|
1126
1617
|
this.calendarDays.forEach((calendarDay) => {
|
|
1127
|
-
calendarDay.button.classList.add(
|
|
1128
|
-
calendarDay.button.classList.add(
|
|
1129
|
-
calendarDay.button.setAttribute(
|
|
1130
|
-
calendarDay.button.classList.remove(this.selectedDayButtonClass)
|
|
1131
|
-
const calendarDayDate = calendarDay.date
|
|
1132
|
-
calendarDayDate.setHours(0, 0, 0, 0)
|
|
1618
|
+
calendarDay.button.classList.add('moj-datepicker__button')
|
|
1619
|
+
calendarDay.button.classList.add('moj-datepicker__calendar-day')
|
|
1620
|
+
calendarDay.button.setAttribute('tabindex', -1)
|
|
1621
|
+
calendarDay.button.classList.remove(this.selectedDayButtonClass)
|
|
1622
|
+
const calendarDayDate = calendarDay.date
|
|
1623
|
+
calendarDayDate.setHours(0, 0, 0, 0)
|
|
1133
1624
|
|
|
1134
|
-
const today = new Date()
|
|
1135
|
-
today.setHours(0, 0, 0, 0)
|
|
1625
|
+
const today = new Date()
|
|
1626
|
+
today.setHours(0, 0, 0, 0)
|
|
1136
1627
|
|
|
1137
1628
|
if (
|
|
1138
1629
|
calendarDayDate.getTime() ===
|
|
1139
1630
|
currentDate.getTime() /* && !calendarDay.button.disabled */
|
|
1140
1631
|
) {
|
|
1141
1632
|
if (focus) {
|
|
1142
|
-
calendarDay.button.setAttribute(
|
|
1143
|
-
calendarDay.button.focus()
|
|
1144
|
-
calendarDay.button.classList.add(this.selectedDayButtonClass)
|
|
1633
|
+
calendarDay.button.setAttribute('tabindex', 0)
|
|
1634
|
+
calendarDay.button.focus()
|
|
1635
|
+
calendarDay.button.classList.add(this.selectedDayButtonClass)
|
|
1145
1636
|
}
|
|
1146
1637
|
}
|
|
1147
1638
|
|
|
@@ -1149,213 +1640,210 @@ Datepicker.prototype.setCurrentDate = function (focus = true) {
|
|
|
1149
1640
|
this.inputDate &&
|
|
1150
1641
|
calendarDayDate.getTime() === this.inputDate.getTime()
|
|
1151
1642
|
) {
|
|
1152
|
-
calendarDay.button.classList.add(this.currentDayButtonClass)
|
|
1153
|
-
calendarDay.button.setAttribute(
|
|
1643
|
+
calendarDay.button.classList.add(this.currentDayButtonClass)
|
|
1644
|
+
calendarDay.button.setAttribute('aria-current', 'date')
|
|
1154
1645
|
} else {
|
|
1155
|
-
calendarDay.button.classList.remove(this.currentDayButtonClass)
|
|
1156
|
-
calendarDay.button.removeAttribute(
|
|
1646
|
+
calendarDay.button.classList.remove(this.currentDayButtonClass)
|
|
1647
|
+
calendarDay.button.removeAttribute('aria-current')
|
|
1157
1648
|
}
|
|
1158
1649
|
|
|
1159
1650
|
if (calendarDayDate.getTime() === today.getTime()) {
|
|
1160
|
-
calendarDay.button.classList.add(this.todayButtonClass)
|
|
1651
|
+
calendarDay.button.classList.add(this.todayButtonClass)
|
|
1161
1652
|
} else {
|
|
1162
|
-
calendarDay.button.classList.remove(this.todayButtonClass)
|
|
1653
|
+
calendarDay.button.classList.remove(this.todayButtonClass)
|
|
1163
1654
|
}
|
|
1164
|
-
})
|
|
1655
|
+
})
|
|
1165
1656
|
|
|
1166
1657
|
// if no date is tab-able, make the first non-disabled date tab-able
|
|
1167
1658
|
if (!focus) {
|
|
1168
1659
|
const enabledDays = this.calendarDays.filter((calendarDay) => {
|
|
1169
1660
|
return (
|
|
1170
|
-
window.getComputedStyle(calendarDay.button).display ===
|
|
1661
|
+
window.getComputedStyle(calendarDay.button).display === 'block' &&
|
|
1171
1662
|
!calendarDay.button.disabled
|
|
1172
|
-
)
|
|
1173
|
-
})
|
|
1663
|
+
)
|
|
1664
|
+
})
|
|
1174
1665
|
|
|
1175
|
-
enabledDays[0].button.setAttribute(
|
|
1666
|
+
enabledDays[0].button.setAttribute('tabindex', 0)
|
|
1176
1667
|
|
|
1177
|
-
this.currentDate = enabledDays[0].date
|
|
1668
|
+
this.currentDate = enabledDays[0].date
|
|
1178
1669
|
}
|
|
1179
|
-
}
|
|
1670
|
+
}
|
|
1180
1671
|
|
|
1181
1672
|
Datepicker.prototype.selectDate = function (date) {
|
|
1182
1673
|
if (this.isExcludedDate(date)) {
|
|
1183
|
-
return
|
|
1674
|
+
return
|
|
1184
1675
|
}
|
|
1185
1676
|
|
|
1186
|
-
this.$calendarButton.querySelector(
|
|
1187
|
-
`Choose date. Selected date is ${this.formattedDateHuman(date)}
|
|
1188
|
-
this.$input.value = this.formattedDateFromDate(date)
|
|
1677
|
+
this.$calendarButton.querySelector('span').innerText =
|
|
1678
|
+
`Choose date. Selected date is ${this.formattedDateHuman(date)}`
|
|
1679
|
+
this.$input.value = this.formattedDateFromDate(date)
|
|
1189
1680
|
|
|
1190
|
-
const changeEvent = new Event(
|
|
1191
|
-
this.$input.dispatchEvent(changeEvent)
|
|
1681
|
+
const changeEvent = new Event('change', { bubbles: true, cancelable: true })
|
|
1682
|
+
this.$input.dispatchEvent(changeEvent)
|
|
1192
1683
|
|
|
1193
|
-
this.closeDialog()
|
|
1194
|
-
}
|
|
1684
|
+
this.closeDialog()
|
|
1685
|
+
}
|
|
1195
1686
|
|
|
1196
1687
|
Datepicker.prototype.isOpen = function () {
|
|
1197
|
-
return this.$dialog.classList.contains(
|
|
1198
|
-
}
|
|
1688
|
+
return this.$dialog.classList.contains('moj-datepicker__dialog--open')
|
|
1689
|
+
}
|
|
1199
1690
|
|
|
1200
1691
|
Datepicker.prototype.toggleDialog = function (event) {
|
|
1201
|
-
event.preventDefault()
|
|
1692
|
+
event.preventDefault()
|
|
1202
1693
|
if (this.isOpen()) {
|
|
1203
|
-
this.closeDialog()
|
|
1694
|
+
this.closeDialog()
|
|
1204
1695
|
} else {
|
|
1205
|
-
this.setMinAndMaxDatesOnCalendar()
|
|
1206
|
-
this.openDialog()
|
|
1696
|
+
this.setMinAndMaxDatesOnCalendar()
|
|
1697
|
+
this.openDialog()
|
|
1207
1698
|
}
|
|
1208
|
-
}
|
|
1699
|
+
}
|
|
1209
1700
|
|
|
1210
1701
|
Datepicker.prototype.openDialog = function () {
|
|
1211
|
-
this.$dialog.hidden = false
|
|
1212
|
-
this.$dialog.classList.add(
|
|
1213
|
-
this.$calendarButton.setAttribute(
|
|
1702
|
+
this.$dialog.hidden = false
|
|
1703
|
+
this.$dialog.classList.add('moj-datepicker__dialog--open')
|
|
1704
|
+
this.$calendarButton.setAttribute('aria-expanded', 'true')
|
|
1214
1705
|
|
|
1215
1706
|
// position the dialog
|
|
1216
1707
|
// if input is wider than dialog pin it to the right
|
|
1217
1708
|
if (this.$input.offsetWidth > this.$dialog.offsetWidth) {
|
|
1218
|
-
this.$dialog.style.right = `0px
|
|
1709
|
+
this.$dialog.style.right = `0px`
|
|
1219
1710
|
}
|
|
1220
|
-
this.$dialog.style.top = `${this.$input.offsetHeight + 3}px
|
|
1711
|
+
this.$dialog.style.top = `${this.$input.offsetHeight + 3}px`
|
|
1221
1712
|
|
|
1222
1713
|
// get the date from the input element
|
|
1223
|
-
this.inputDate = this.formattedDateFromString(this.$input.value)
|
|
1224
|
-
this.currentDate = this.inputDate
|
|
1225
|
-
this.currentDate.setHours(0, 0, 0, 0)
|
|
1714
|
+
this.inputDate = this.formattedDateFromString(this.$input.value)
|
|
1715
|
+
this.currentDate = this.inputDate
|
|
1716
|
+
this.currentDate.setHours(0, 0, 0, 0)
|
|
1226
1717
|
|
|
1227
|
-
this.updateCalendar()
|
|
1228
|
-
this.setCurrentDate()
|
|
1229
|
-
}
|
|
1718
|
+
this.updateCalendar()
|
|
1719
|
+
this.setCurrentDate()
|
|
1720
|
+
}
|
|
1230
1721
|
|
|
1231
1722
|
Datepicker.prototype.closeDialog = function () {
|
|
1232
|
-
this.$dialog.hidden = true
|
|
1233
|
-
this.$dialog.classList.remove(
|
|
1234
|
-
this.$calendarButton.setAttribute(
|
|
1235
|
-
this.$calendarButton.focus()
|
|
1236
|
-
}
|
|
1723
|
+
this.$dialog.hidden = true
|
|
1724
|
+
this.$dialog.classList.remove('moj-datepicker__dialog--open')
|
|
1725
|
+
this.$calendarButton.setAttribute('aria-expanded', 'false')
|
|
1726
|
+
this.$calendarButton.focus()
|
|
1727
|
+
}
|
|
1237
1728
|
|
|
1238
1729
|
Datepicker.prototype.goToDate = function (date, focus) {
|
|
1239
|
-
const current = this.currentDate
|
|
1240
|
-
this.currentDate = date
|
|
1730
|
+
const current = this.currentDate
|
|
1731
|
+
this.currentDate = date
|
|
1241
1732
|
|
|
1242
1733
|
if (
|
|
1243
1734
|
current.getMonth() !== this.currentDate.getMonth() ||
|
|
1244
1735
|
current.getFullYear() !== this.currentDate.getFullYear()
|
|
1245
1736
|
) {
|
|
1246
|
-
this.updateCalendar()
|
|
1737
|
+
this.updateCalendar()
|
|
1247
1738
|
}
|
|
1248
1739
|
|
|
1249
|
-
this.setCurrentDate(focus)
|
|
1250
|
-
}
|
|
1740
|
+
this.setCurrentDate(focus)
|
|
1741
|
+
}
|
|
1251
1742
|
|
|
1252
1743
|
// day navigation
|
|
1253
1744
|
Datepicker.prototype.focusNextDay = function () {
|
|
1254
|
-
const date = new Date(this.currentDate)
|
|
1255
|
-
date.setDate(date.getDate() + 1)
|
|
1256
|
-
this.goToDate(date)
|
|
1257
|
-
}
|
|
1745
|
+
const date = new Date(this.currentDate)
|
|
1746
|
+
date.setDate(date.getDate() + 1)
|
|
1747
|
+
this.goToDate(date)
|
|
1748
|
+
}
|
|
1258
1749
|
|
|
1259
1750
|
Datepicker.prototype.focusPreviousDay = function () {
|
|
1260
|
-
const date = new Date(this.currentDate)
|
|
1261
|
-
date.setDate(date.getDate() - 1)
|
|
1262
|
-
this.goToDate(date)
|
|
1263
|
-
}
|
|
1751
|
+
const date = new Date(this.currentDate)
|
|
1752
|
+
date.setDate(date.getDate() - 1)
|
|
1753
|
+
this.goToDate(date)
|
|
1754
|
+
}
|
|
1264
1755
|
|
|
1265
1756
|
// week navigation
|
|
1266
1757
|
Datepicker.prototype.focusNextWeek = function () {
|
|
1267
|
-
const date = new Date(this.currentDate)
|
|
1268
|
-
date.setDate(date.getDate() + 7)
|
|
1269
|
-
this.goToDate(date)
|
|
1270
|
-
}
|
|
1758
|
+
const date = new Date(this.currentDate)
|
|
1759
|
+
date.setDate(date.getDate() + 7)
|
|
1760
|
+
this.goToDate(date)
|
|
1761
|
+
}
|
|
1271
1762
|
|
|
1272
1763
|
Datepicker.prototype.focusPreviousWeek = function () {
|
|
1273
|
-
const date = new Date(this.currentDate)
|
|
1274
|
-
date.setDate(date.getDate() - 7)
|
|
1275
|
-
this.goToDate(date)
|
|
1276
|
-
}
|
|
1764
|
+
const date = new Date(this.currentDate)
|
|
1765
|
+
date.setDate(date.getDate() - 7)
|
|
1766
|
+
this.goToDate(date)
|
|
1767
|
+
}
|
|
1277
1768
|
|
|
1278
1769
|
Datepicker.prototype.focusFirstDayOfWeek = function () {
|
|
1279
|
-
const date = new Date(this.currentDate)
|
|
1280
|
-
const firstDayOfWeekIndex = this.config.weekStartDay
|
|
1281
|
-
const dayOfWeek = date.getDay()
|
|
1770
|
+
const date = new Date(this.currentDate)
|
|
1771
|
+
const firstDayOfWeekIndex = this.config.weekStartDay === 'sunday' ? 0 : 1
|
|
1772
|
+
const dayOfWeek = date.getDay()
|
|
1282
1773
|
const diff =
|
|
1283
1774
|
dayOfWeek >= firstDayOfWeekIndex
|
|
1284
1775
|
? dayOfWeek - firstDayOfWeekIndex
|
|
1285
|
-
: 6 - dayOfWeek
|
|
1776
|
+
: 6 - dayOfWeek
|
|
1286
1777
|
|
|
1287
|
-
date.setDate(date.getDate() - diff)
|
|
1288
|
-
date.setHours(0, 0, 0, 0)
|
|
1778
|
+
date.setDate(date.getDate() - diff)
|
|
1779
|
+
date.setHours(0, 0, 0, 0)
|
|
1289
1780
|
|
|
1290
|
-
this.goToDate(date)
|
|
1291
|
-
}
|
|
1781
|
+
this.goToDate(date)
|
|
1782
|
+
}
|
|
1292
1783
|
|
|
1293
1784
|
Datepicker.prototype.focusLastDayOfWeek = function () {
|
|
1294
|
-
const date = new Date(this.currentDate)
|
|
1295
|
-
const lastDayOfWeekIndex = this.config.weekStartDay
|
|
1296
|
-
const dayOfWeek = date.getDay()
|
|
1785
|
+
const date = new Date(this.currentDate)
|
|
1786
|
+
const lastDayOfWeekIndex = this.config.weekStartDay === 'sunday' ? 6 : 0
|
|
1787
|
+
const dayOfWeek = date.getDay()
|
|
1297
1788
|
const diff =
|
|
1298
1789
|
dayOfWeek <= lastDayOfWeekIndex
|
|
1299
1790
|
? lastDayOfWeekIndex - dayOfWeek
|
|
1300
|
-
: 7 - dayOfWeek
|
|
1791
|
+
: 7 - dayOfWeek
|
|
1301
1792
|
|
|
1302
|
-
date.setDate(date.getDate() + diff)
|
|
1303
|
-
date.setHours(0, 0, 0, 0)
|
|
1793
|
+
date.setDate(date.getDate() + diff)
|
|
1794
|
+
date.setHours(0, 0, 0, 0)
|
|
1304
1795
|
|
|
1305
|
-
this.goToDate(date)
|
|
1306
|
-
}
|
|
1796
|
+
this.goToDate(date)
|
|
1797
|
+
}
|
|
1307
1798
|
|
|
1308
1799
|
// month navigation
|
|
1309
1800
|
Datepicker.prototype.focusNextMonth = function (event, focus = true) {
|
|
1310
|
-
event.preventDefault()
|
|
1311
|
-
const date = new Date(this.currentDate)
|
|
1312
|
-
date.setMonth(date.getMonth() + 1, 1)
|
|
1313
|
-
this.goToDate(date, focus)
|
|
1314
|
-
}
|
|
1801
|
+
event.preventDefault()
|
|
1802
|
+
const date = new Date(this.currentDate)
|
|
1803
|
+
date.setMonth(date.getMonth() + 1, 1)
|
|
1804
|
+
this.goToDate(date, focus)
|
|
1805
|
+
}
|
|
1315
1806
|
|
|
1316
1807
|
Datepicker.prototype.focusPreviousMonth = function (event, focus = true) {
|
|
1317
|
-
event.preventDefault()
|
|
1318
|
-
const date = new Date(this.currentDate)
|
|
1319
|
-
date.setMonth(date.getMonth() - 1, 1)
|
|
1320
|
-
this.goToDate(date, focus)
|
|
1321
|
-
}
|
|
1808
|
+
event.preventDefault()
|
|
1809
|
+
const date = new Date(this.currentDate)
|
|
1810
|
+
date.setMonth(date.getMonth() - 1, 1)
|
|
1811
|
+
this.goToDate(date, focus)
|
|
1812
|
+
}
|
|
1322
1813
|
|
|
1323
1814
|
// year navigation
|
|
1324
1815
|
Datepicker.prototype.focusNextYear = function (event, focus = true) {
|
|
1325
|
-
event.preventDefault()
|
|
1326
|
-
const date = new Date(this.currentDate)
|
|
1327
|
-
date.setFullYear(date.getFullYear() + 1, date.getMonth(), 1)
|
|
1328
|
-
this.goToDate(date, focus)
|
|
1329
|
-
}
|
|
1816
|
+
event.preventDefault()
|
|
1817
|
+
const date = new Date(this.currentDate)
|
|
1818
|
+
date.setFullYear(date.getFullYear() + 1, date.getMonth(), 1)
|
|
1819
|
+
this.goToDate(date, focus)
|
|
1820
|
+
}
|
|
1330
1821
|
|
|
1331
1822
|
Datepicker.prototype.focusPreviousYear = function (event, focus = true) {
|
|
1332
|
-
event.preventDefault()
|
|
1333
|
-
const date = new Date(this.currentDate)
|
|
1334
|
-
date.setFullYear(date.getFullYear() - 1, date.getMonth(), 1)
|
|
1335
|
-
this.goToDate(date, focus)
|
|
1336
|
-
}
|
|
1823
|
+
event.preventDefault()
|
|
1824
|
+
const date = new Date(this.currentDate)
|
|
1825
|
+
date.setFullYear(date.getFullYear() - 1, date.getMonth(), 1)
|
|
1826
|
+
this.goToDate(date, focus)
|
|
1827
|
+
}
|
|
1337
1828
|
|
|
1338
1829
|
/**
|
|
1339
1830
|
* Parse dataset
|
|
1340
1831
|
*
|
|
1341
|
-
*
|
|
1342
|
-
* optionally expanding nested `i18n.field`
|
|
1343
|
-
*
|
|
1344
|
-
* @param {{ schema: Schema }} Component - Component class
|
|
1832
|
+
* @param {Schema} schema - Component class
|
|
1345
1833
|
* @param {DOMStringMap} dataset - HTML element dataset
|
|
1346
|
-
* @returns {
|
|
1834
|
+
* @returns {object} Normalised dataset
|
|
1347
1835
|
*/
|
|
1348
1836
|
Datepicker.prototype.parseDataset = function (schema, dataset) {
|
|
1349
|
-
const parsed = {}
|
|
1837
|
+
const parsed = {}
|
|
1350
1838
|
|
|
1351
|
-
for (const [field,
|
|
1839
|
+
for (const [field, ,] of Object.entries(schema.properties)) {
|
|
1352
1840
|
if (field in dataset) {
|
|
1353
|
-
parsed[field] = dataset[field]
|
|
1841
|
+
parsed[field] = dataset[field]
|
|
1354
1842
|
}
|
|
1355
1843
|
}
|
|
1356
1844
|
|
|
1357
|
-
return parsed
|
|
1358
|
-
}
|
|
1845
|
+
return parsed
|
|
1846
|
+
}
|
|
1359
1847
|
|
|
1360
1848
|
/**
|
|
1361
1849
|
* Config merging function
|
|
@@ -1367,28 +1855,28 @@ Datepicker.prototype.parseDataset = function (schema, dataset) {
|
|
|
1367
1855
|
* @returns {{ [key: string]: unknown }} A merged config object
|
|
1368
1856
|
*/
|
|
1369
1857
|
Datepicker.prototype.mergeConfigs = function (...configObjects) {
|
|
1370
|
-
const formattedConfigObject = {}
|
|
1858
|
+
const formattedConfigObject = {}
|
|
1371
1859
|
|
|
1372
1860
|
// Loop through each of the passed objects
|
|
1373
1861
|
for (const configObject of configObjects) {
|
|
1374
1862
|
for (const key of Object.keys(configObject)) {
|
|
1375
|
-
const option = formattedConfigObject[key]
|
|
1376
|
-
const override = configObject[key]
|
|
1863
|
+
const option = formattedConfigObject[key]
|
|
1864
|
+
const override = configObject[key]
|
|
1377
1865
|
|
|
1378
1866
|
// Push their keys one-by-one into formattedConfigObject. Any duplicate
|
|
1379
1867
|
// keys with object values will be merged, otherwise the new value will
|
|
1380
1868
|
// override the existing value.
|
|
1381
|
-
if (typeof option ===
|
|
1869
|
+
if (typeof option === 'object' && typeof override === 'object') {
|
|
1382
1870
|
// @ts-expect-error Index signature for type 'string' is missing
|
|
1383
|
-
formattedConfigObject[key] = this.mergeConfigs(option, override)
|
|
1871
|
+
formattedConfigObject[key] = this.mergeConfigs(option, override)
|
|
1384
1872
|
} else {
|
|
1385
|
-
formattedConfigObject[key] = override
|
|
1873
|
+
formattedConfigObject[key] = override
|
|
1386
1874
|
}
|
|
1387
1875
|
}
|
|
1388
1876
|
}
|
|
1389
1877
|
|
|
1390
|
-
return formattedConfigObject
|
|
1391
|
-
}
|
|
1878
|
+
return formattedConfigObject
|
|
1879
|
+
}
|
|
1392
1880
|
|
|
1393
1881
|
/**
|
|
1394
1882
|
*
|
|
@@ -1397,22 +1885,22 @@ Datepicker.prototype.mergeConfigs = function (...configObjects) {
|
|
|
1397
1885
|
* @param {number} row
|
|
1398
1886
|
* @param {number} column
|
|
1399
1887
|
* @param {Datepicker} picker
|
|
1400
|
-
* @
|
|
1888
|
+
* @class
|
|
1401
1889
|
*/
|
|
1402
1890
|
function DSCalendarDay(button, index, row, column, picker) {
|
|
1403
|
-
this.index = index
|
|
1404
|
-
this.row = row
|
|
1405
|
-
this.column = column
|
|
1406
|
-
this.button = button
|
|
1407
|
-
this.picker = picker
|
|
1891
|
+
this.index = index
|
|
1892
|
+
this.row = row
|
|
1893
|
+
this.column = column
|
|
1894
|
+
this.button = button
|
|
1895
|
+
this.picker = picker
|
|
1408
1896
|
|
|
1409
|
-
this.date = new Date()
|
|
1897
|
+
this.date = new Date()
|
|
1410
1898
|
}
|
|
1411
1899
|
|
|
1412
1900
|
DSCalendarDay.prototype.init = function () {
|
|
1413
|
-
this.button.addEventListener(
|
|
1414
|
-
this.button.addEventListener(
|
|
1415
|
-
}
|
|
1901
|
+
this.button.addEventListener('keydown', this.keyPress.bind(this))
|
|
1902
|
+
this.button.addEventListener('click', this.click.bind(this))
|
|
1903
|
+
}
|
|
1416
1904
|
|
|
1417
1905
|
/**
|
|
1418
1906
|
* @param {Date} day - the Date for the calendar day
|
|
@@ -1420,84 +1908,84 @@ DSCalendarDay.prototype.init = function () {
|
|
|
1420
1908
|
* @param {boolean} disabled - is the day selectable or excluded
|
|
1421
1909
|
*/
|
|
1422
1910
|
DSCalendarDay.prototype.update = function (day, hidden, disabled) {
|
|
1423
|
-
|
|
1424
|
-
let accessibleLabel = this.picker.formattedDateHuman(day)
|
|
1911
|
+
const label = day.getDate()
|
|
1912
|
+
let accessibleLabel = this.picker.formattedDateHuman(day)
|
|
1425
1913
|
|
|
1426
1914
|
if (disabled) {
|
|
1427
|
-
this.button.setAttribute(
|
|
1428
|
-
accessibleLabel =
|
|
1915
|
+
this.button.setAttribute('aria-disabled', true)
|
|
1916
|
+
accessibleLabel = `Excluded date, ${accessibleLabel}`
|
|
1429
1917
|
} else {
|
|
1430
|
-
this.button.removeAttribute(
|
|
1918
|
+
this.button.removeAttribute('aria-disabled')
|
|
1431
1919
|
}
|
|
1432
1920
|
|
|
1433
1921
|
if (hidden) {
|
|
1434
|
-
this.button.style.display =
|
|
1922
|
+
this.button.style.display = 'none'
|
|
1435
1923
|
} else {
|
|
1436
|
-
this.button.style.display =
|
|
1924
|
+
this.button.style.display = 'block'
|
|
1437
1925
|
}
|
|
1438
1926
|
this.button.setAttribute(
|
|
1439
|
-
|
|
1440
|
-
this.picker.formattedDateFromDate(day)
|
|
1441
|
-
)
|
|
1927
|
+
'data-testid',
|
|
1928
|
+
this.picker.formattedDateFromDate(day)
|
|
1929
|
+
)
|
|
1442
1930
|
|
|
1443
|
-
this.button.innerHTML = `<span class="govuk-visually-hidden">${accessibleLabel}</span><span aria-hidden="true">${label}</span
|
|
1444
|
-
this.date = new Date(day)
|
|
1445
|
-
}
|
|
1931
|
+
this.button.innerHTML = `<span class="govuk-visually-hidden">${accessibleLabel}</span><span aria-hidden="true">${label}</span>`
|
|
1932
|
+
this.date = new Date(day)
|
|
1933
|
+
}
|
|
1446
1934
|
|
|
1447
1935
|
DSCalendarDay.prototype.click = function (event) {
|
|
1448
|
-
this.picker.goToDate(this.date)
|
|
1449
|
-
this.picker.selectDate(this.date)
|
|
1936
|
+
this.picker.goToDate(this.date)
|
|
1937
|
+
this.picker.selectDate(this.date)
|
|
1450
1938
|
|
|
1451
|
-
event.stopPropagation()
|
|
1452
|
-
event.preventDefault()
|
|
1453
|
-
}
|
|
1939
|
+
event.stopPropagation()
|
|
1940
|
+
event.preventDefault()
|
|
1941
|
+
}
|
|
1454
1942
|
|
|
1455
1943
|
DSCalendarDay.prototype.keyPress = function (event) {
|
|
1456
|
-
let calendarNavKey = true
|
|
1944
|
+
let calendarNavKey = true
|
|
1457
1945
|
|
|
1458
1946
|
switch (event.key) {
|
|
1459
|
-
case
|
|
1460
|
-
this.picker.focusPreviousDay()
|
|
1461
|
-
break
|
|
1462
|
-
case
|
|
1463
|
-
this.picker.focusNextDay()
|
|
1464
|
-
break
|
|
1465
|
-
case
|
|
1466
|
-
this.picker.focusPreviousWeek()
|
|
1467
|
-
break
|
|
1468
|
-
case
|
|
1469
|
-
this.picker.focusNextWeek()
|
|
1470
|
-
break
|
|
1471
|
-
case
|
|
1472
|
-
this.picker.focusFirstDayOfWeek()
|
|
1473
|
-
break
|
|
1474
|
-
case
|
|
1475
|
-
this.picker.focusLastDayOfWeek()
|
|
1476
|
-
break
|
|
1477
|
-
case
|
|
1947
|
+
case 'ArrowLeft':
|
|
1948
|
+
this.picker.focusPreviousDay()
|
|
1949
|
+
break
|
|
1950
|
+
case 'ArrowRight':
|
|
1951
|
+
this.picker.focusNextDay()
|
|
1952
|
+
break
|
|
1953
|
+
case 'ArrowUp':
|
|
1954
|
+
this.picker.focusPreviousWeek()
|
|
1955
|
+
break
|
|
1956
|
+
case 'ArrowDown':
|
|
1957
|
+
this.picker.focusNextWeek()
|
|
1958
|
+
break
|
|
1959
|
+
case 'Home':
|
|
1960
|
+
this.picker.focusFirstDayOfWeek()
|
|
1961
|
+
break
|
|
1962
|
+
case 'End':
|
|
1963
|
+
this.picker.focusLastDayOfWeek()
|
|
1964
|
+
break
|
|
1965
|
+
case 'PageUp':
|
|
1478
1966
|
// eslint-disable-next-line no-unused-expressions
|
|
1479
1967
|
event.shiftKey
|
|
1480
1968
|
? this.picker.focusPreviousYear(event)
|
|
1481
|
-
: this.picker.focusPreviousMonth(event)
|
|
1482
|
-
break
|
|
1483
|
-
case
|
|
1969
|
+
: this.picker.focusPreviousMonth(event)
|
|
1970
|
+
break
|
|
1971
|
+
case 'PageDown':
|
|
1484
1972
|
// eslint-disable-next-line no-unused-expressions
|
|
1485
1973
|
event.shiftKey
|
|
1486
1974
|
? this.picker.focusNextYear(event)
|
|
1487
|
-
: this.picker.focusNextMonth(event)
|
|
1488
|
-
break
|
|
1975
|
+
: this.picker.focusNextMonth(event)
|
|
1976
|
+
break
|
|
1489
1977
|
default:
|
|
1490
|
-
calendarNavKey = false
|
|
1491
|
-
break
|
|
1978
|
+
calendarNavKey = false
|
|
1979
|
+
break
|
|
1492
1980
|
}
|
|
1493
1981
|
|
|
1494
1982
|
if (calendarNavKey) {
|
|
1495
|
-
event.preventDefault()
|
|
1496
|
-
event.stopPropagation()
|
|
1983
|
+
event.preventDefault()
|
|
1984
|
+
event.stopPropagation()
|
|
1497
1985
|
}
|
|
1498
|
-
}
|
|
1986
|
+
}
|
|
1499
1987
|
|
|
1500
|
-
MOJFrontend.DatePicker = Datepicker
|
|
1988
|
+
MOJFrontend.DatePicker = Datepicker
|
|
1501
1989
|
|
|
1502
1990
|
/**
|
|
1503
1991
|
* Schema for component config
|
|
@@ -1513,102 +2001,107 @@ MOJFrontend.DatePicker = Datepicker;
|
|
|
1513
2001
|
* @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
|
|
1514
2002
|
*/
|
|
1515
2003
|
|
|
1516
|
-
MOJFrontend.FilterToggleButton = function(options) {
|
|
1517
|
-
this.options = options
|
|
1518
|
-
this.container = $(this.options.toggleButton.container)
|
|
1519
|
-
this.filterContainer = $(this.options.filter.container)
|
|
1520
|
-
|
|
1521
|
-
this.createToggleButton()
|
|
1522
|
-
this.setupResponsiveChecks()
|
|
1523
|
-
this.filterContainer.attr('tabindex', '-1')
|
|
1524
|
-
if(this.options.startHidden) {
|
|
1525
|
-
this.hideMenu()
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
MOJFrontend.FilterToggleButton.prototype.setupResponsiveChecks = function() {
|
|
1530
|
-
this.mq = window.matchMedia(this.options.bigModeMediaQuery)
|
|
1531
|
-
this.mq.addListener($.proxy(this, 'checkMode'))
|
|
1532
|
-
this.checkMode(this.mq)
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
MOJFrontend.FilterToggleButton.prototype.createToggleButton = function() {
|
|
1536
|
-
this.menuButton = $(
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
2004
|
+
MOJFrontend.FilterToggleButton = function (options) {
|
|
2005
|
+
this.options = options
|
|
2006
|
+
this.container = $(this.options.toggleButton.container)
|
|
2007
|
+
this.filterContainer = $(this.options.filter.container)
|
|
2008
|
+
|
|
2009
|
+
this.createToggleButton()
|
|
2010
|
+
this.setupResponsiveChecks()
|
|
2011
|
+
this.filterContainer.attr('tabindex', '-1')
|
|
2012
|
+
if (this.options.startHidden) {
|
|
2013
|
+
this.hideMenu()
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
MOJFrontend.FilterToggleButton.prototype.setupResponsiveChecks = function () {
|
|
2018
|
+
this.mq = window.matchMedia(this.options.bigModeMediaQuery)
|
|
2019
|
+
this.mq.addListener($.proxy(this, 'checkMode'))
|
|
2020
|
+
this.checkMode(this.mq)
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
MOJFrontend.FilterToggleButton.prototype.createToggleButton = function () {
|
|
2024
|
+
this.menuButton = $(
|
|
2025
|
+
`<button class="govuk-button ${this.options.toggleButton.classes}" type="button" aria-haspopup="true" aria-expanded="false">${this.options.toggleButton.showText}</button>`
|
|
2026
|
+
)
|
|
2027
|
+
this.menuButton.on('click', $.proxy(this, 'onMenuButtonClick'))
|
|
2028
|
+
this.container.append(this.menuButton)
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
MOJFrontend.FilterToggleButton.prototype.checkMode = function (mq) {
|
|
2032
|
+
if (mq.matches) {
|
|
2033
|
+
this.enableBigMode()
|
|
1544
2034
|
} else {
|
|
1545
|
-
this.enableSmallMode()
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
MOJFrontend.FilterToggleButton.prototype.enableBigMode = function() {
|
|
1550
|
-
this.showMenu()
|
|
1551
|
-
this.removeCloseButton()
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
MOJFrontend.FilterToggleButton.prototype.enableSmallMode = function() {
|
|
1555
|
-
this.hideMenu()
|
|
1556
|
-
this.addCloseButton()
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
MOJFrontend.FilterToggleButton.prototype.addCloseButton = function() {
|
|
1560
|
-
if(this.options.closeButton) {
|
|
1561
|
-
this.closeButton = $(
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
this.menuButton.
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
this.menuButton.
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
2035
|
+
this.enableSmallMode()
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
MOJFrontend.FilterToggleButton.prototype.enableBigMode = function () {
|
|
2040
|
+
this.showMenu()
|
|
2041
|
+
this.removeCloseButton()
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
MOJFrontend.FilterToggleButton.prototype.enableSmallMode = function () {
|
|
2045
|
+
this.hideMenu()
|
|
2046
|
+
this.addCloseButton()
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
MOJFrontend.FilterToggleButton.prototype.addCloseButton = function () {
|
|
2050
|
+
if (this.options.closeButton) {
|
|
2051
|
+
this.closeButton = $(
|
|
2052
|
+
`<button class="moj-filter__close" type="button">${this.options.closeButton.text}</button>`
|
|
2053
|
+
)
|
|
2054
|
+
this.closeButton.on('click', $.proxy(this, 'onCloseClick'))
|
|
2055
|
+
$(this.options.closeButton.container).append(this.closeButton)
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
MOJFrontend.FilterToggleButton.prototype.onCloseClick = function () {
|
|
2060
|
+
this.hideMenu()
|
|
2061
|
+
this.menuButton.focus()
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
MOJFrontend.FilterToggleButton.prototype.removeCloseButton = function () {
|
|
2065
|
+
if (this.closeButton) {
|
|
2066
|
+
this.closeButton.remove()
|
|
2067
|
+
this.closeButton = null
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
MOJFrontend.FilterToggleButton.prototype.hideMenu = function () {
|
|
2072
|
+
this.menuButton.attr('aria-expanded', 'false')
|
|
2073
|
+
this.filterContainer.addClass('moj-js-hidden')
|
|
2074
|
+
this.menuButton.text(this.options.toggleButton.showText)
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
MOJFrontend.FilterToggleButton.prototype.showMenu = function () {
|
|
2078
|
+
this.menuButton.attr('aria-expanded', 'true')
|
|
2079
|
+
this.filterContainer.removeClass('moj-js-hidden')
|
|
2080
|
+
this.menuButton.text(this.options.toggleButton.hideText)
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
MOJFrontend.FilterToggleButton.prototype.onMenuButtonClick = function () {
|
|
2084
|
+
this.toggle()
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
MOJFrontend.FilterToggleButton.prototype.toggle = function () {
|
|
2088
|
+
if (this.menuButton.attr('aria-expanded') === 'false') {
|
|
2089
|
+
this.showMenu()
|
|
2090
|
+
this.filterContainer.get(0).focus()
|
|
1599
2091
|
} else {
|
|
1600
|
-
this.hideMenu()
|
|
2092
|
+
this.hideMenu()
|
|
1601
2093
|
}
|
|
1602
|
-
}
|
|
2094
|
+
}
|
|
1603
2095
|
|
|
1604
|
-
MOJFrontend.FormValidator = function(form, options) {
|
|
1605
|
-
this.form = form
|
|
1606
|
-
this.errors = []
|
|
1607
|
-
this.validators = []
|
|
1608
|
-
$(this.form).on('submit', $.proxy(this, 'onSubmit'))
|
|
1609
|
-
this.summary =
|
|
1610
|
-
|
|
1611
|
-
|
|
2096
|
+
MOJFrontend.FormValidator = function (form, options) {
|
|
2097
|
+
this.form = form
|
|
2098
|
+
this.errors = []
|
|
2099
|
+
this.validators = []
|
|
2100
|
+
$(this.form).on('submit', $.proxy(this, 'onSubmit'))
|
|
2101
|
+
this.summary =
|
|
2102
|
+
options && options.summary ? $(options.summary) : $('.govuk-error-summary')
|
|
2103
|
+
this.originalTitle = document.title
|
|
2104
|
+
}
|
|
1612
2105
|
|
|
1613
2106
|
MOJFrontend.FormValidator.entityMap = {
|
|
1614
2107
|
'&': '&',
|
|
@@ -1619,146 +2112,158 @@ MOJFrontend.FormValidator.entityMap = {
|
|
|
1619
2112
|
'/': '/',
|
|
1620
2113
|
'`': '`',
|
|
1621
2114
|
'=': '='
|
|
1622
|
-
}
|
|
2115
|
+
}
|
|
1623
2116
|
|
|
1624
|
-
MOJFrontend.FormValidator.prototype.escapeHtml = function(string) {
|
|
1625
|
-
return String(string).replace(/[&<>"'
|
|
1626
|
-
return MOJFrontend.FormValidator.entityMap[s]
|
|
1627
|
-
})
|
|
1628
|
-
}
|
|
2117
|
+
MOJFrontend.FormValidator.prototype.escapeHtml = function (string) {
|
|
2118
|
+
return String(string).replace(/[&<>"'`=/]/g, function fromEntityMap(s) {
|
|
2119
|
+
return MOJFrontend.FormValidator.entityMap[s]
|
|
2120
|
+
})
|
|
2121
|
+
}
|
|
1629
2122
|
|
|
1630
|
-
MOJFrontend.FormValidator.prototype.resetTitle = function() {
|
|
1631
|
-
document.title = this.originalTitle
|
|
1632
|
-
}
|
|
2123
|
+
MOJFrontend.FormValidator.prototype.resetTitle = function () {
|
|
2124
|
+
document.title = this.originalTitle
|
|
2125
|
+
}
|
|
1633
2126
|
|
|
1634
|
-
MOJFrontend.FormValidator.prototype.updateTitle = function() {
|
|
1635
|
-
document.title =
|
|
1636
|
-
}
|
|
2127
|
+
MOJFrontend.FormValidator.prototype.updateTitle = function () {
|
|
2128
|
+
document.title = `${this.errors.length} errors - ${document.title}`
|
|
2129
|
+
}
|
|
1637
2130
|
|
|
1638
2131
|
MOJFrontend.FormValidator.prototype.showSummary = function () {
|
|
1639
|
-
this.summary.html(this.getSummaryHtml())
|
|
1640
|
-
this.summary.removeClass('moj-hidden')
|
|
1641
|
-
this.summary.attr('aria-labelledby', 'errorSummary-heading')
|
|
1642
|
-
this.summary.focus()
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
MOJFrontend.FormValidator.prototype.getSummaryHtml = function() {
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
html += '<
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
html +=
|
|
1653
|
-
html +=
|
|
1654
|
-
html +=
|
|
1655
|
-
html += '</
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
html += '</
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
this.summary.
|
|
1665
|
-
|
|
2132
|
+
this.summary.html(this.getSummaryHtml())
|
|
2133
|
+
this.summary.removeClass('moj-hidden')
|
|
2134
|
+
this.summary.attr('aria-labelledby', 'errorSummary-heading')
|
|
2135
|
+
this.summary.focus()
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
MOJFrontend.FormValidator.prototype.getSummaryHtml = function () {
|
|
2139
|
+
let html =
|
|
2140
|
+
'<h2 id="error-summary-title" class="govuk-error-summary__title">There is a problem</h2>'
|
|
2141
|
+
html += '<div class="govuk-error-summary__body">'
|
|
2142
|
+
html += '<ul class="govuk-list govuk-error-summary__list">'
|
|
2143
|
+
for (let i = 0, j = this.errors.length; i < j; i++) {
|
|
2144
|
+
const error = this.errors[i]
|
|
2145
|
+
html += '<li>'
|
|
2146
|
+
html += `<a href="#${this.escapeHtml(error.fieldName)}">`
|
|
2147
|
+
html += this.escapeHtml(error.message)
|
|
2148
|
+
html += '</a>'
|
|
2149
|
+
html += '</li>'
|
|
2150
|
+
}
|
|
2151
|
+
html += '</ul>'
|
|
2152
|
+
html += '</div>'
|
|
2153
|
+
return html
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
MOJFrontend.FormValidator.prototype.hideSummary = function () {
|
|
2157
|
+
this.summary.addClass('moj-hidden')
|
|
2158
|
+
this.summary.removeAttr('aria-labelledby')
|
|
2159
|
+
}
|
|
1666
2160
|
|
|
1667
2161
|
MOJFrontend.FormValidator.prototype.onSubmit = function (e) {
|
|
1668
|
-
this.removeInlineErrors()
|
|
1669
|
-
this.hideSummary()
|
|
1670
|
-
this.resetTitle()
|
|
1671
|
-
if(!this.validate()) {
|
|
1672
|
-
e.preventDefault()
|
|
1673
|
-
this.updateTitle()
|
|
1674
|
-
this.showSummary()
|
|
1675
|
-
this.showInlineErrors()
|
|
2162
|
+
this.removeInlineErrors()
|
|
2163
|
+
this.hideSummary()
|
|
2164
|
+
this.resetTitle()
|
|
2165
|
+
if (!this.validate()) {
|
|
2166
|
+
e.preventDefault()
|
|
2167
|
+
this.updateTitle()
|
|
2168
|
+
this.showSummary()
|
|
2169
|
+
this.showInlineErrors()
|
|
1676
2170
|
}
|
|
1677
|
-
}
|
|
2171
|
+
}
|
|
1678
2172
|
|
|
1679
|
-
MOJFrontend.FormValidator.prototype.showInlineErrors = function() {
|
|
1680
|
-
for (
|
|
1681
|
-
this.showInlineError(this.errors[i])
|
|
2173
|
+
MOJFrontend.FormValidator.prototype.showInlineErrors = function () {
|
|
2174
|
+
for (let i = 0, j = this.errors.length; i < j; i++) {
|
|
2175
|
+
this.showInlineError(this.errors[i])
|
|
1682
2176
|
}
|
|
1683
|
-
}
|
|
2177
|
+
}
|
|
1684
2178
|
|
|
1685
2179
|
MOJFrontend.FormValidator.prototype.showInlineError = function (error) {
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
fieldContainer.
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
2180
|
+
const errorSpanId = `${error.fieldName}-error`
|
|
2181
|
+
const errorSpan = `<span class="govuk-error-message" id="${
|
|
2182
|
+
errorSpanId
|
|
2183
|
+
}">${this.escapeHtml(error.message)}</span>`
|
|
2184
|
+
const control = $(`#${error.fieldName}`)
|
|
2185
|
+
const fieldContainer = control.parents('.govuk-form-group')
|
|
2186
|
+
const label = fieldContainer.find('label')
|
|
2187
|
+
const legend = fieldContainer.find('legend')
|
|
2188
|
+
const fieldset = fieldContainer.find('fieldset')
|
|
2189
|
+
fieldContainer.addClass('govuk-form-group--error')
|
|
2190
|
+
if (legend.length) {
|
|
2191
|
+
legend.after(errorSpan)
|
|
2192
|
+
fieldContainer.attr('aria-invalid', 'true')
|
|
2193
|
+
MOJFrontend.addAttributeValue(fieldset[0], 'aria-describedby', errorSpanId)
|
|
1698
2194
|
} else {
|
|
1699
|
-
label.after(errorSpan)
|
|
1700
|
-
control.attr('aria-invalid', 'true')
|
|
1701
|
-
MOJFrontend.addAttributeValue(control[0], 'aria-describedby', errorSpanId)
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
MOJFrontend.FormValidator.prototype.removeInlineErrors = function() {
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
fieldContainer.find('
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
2195
|
+
label.after(errorSpan)
|
|
2196
|
+
control.attr('aria-invalid', 'true')
|
|
2197
|
+
MOJFrontend.addAttributeValue(control[0], 'aria-describedby', errorSpanId)
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
MOJFrontend.FormValidator.prototype.removeInlineErrors = function () {
|
|
2202
|
+
for (let i = 0; i < this.errors.length; i++) {
|
|
2203
|
+
this.removeInlineError(this.errors[i])
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
MOJFrontend.FormValidator.prototype.removeInlineError = function (error) {
|
|
2208
|
+
const control = $(`#${error.fieldName}`)
|
|
2209
|
+
const fieldContainer = control.parents('.govuk-form-group')
|
|
2210
|
+
fieldContainer.find('.govuk-error-message').remove()
|
|
2211
|
+
fieldContainer.removeClass('govuk-form-group--error')
|
|
2212
|
+
fieldContainer.find('[aria-invalid]').attr('aria-invalid', 'false')
|
|
2213
|
+
const errorSpanId = `${error.fieldName}-error`
|
|
2214
|
+
MOJFrontend.removeAttributeValue(
|
|
2215
|
+
fieldContainer.find('[aria-describedby]')[0],
|
|
2216
|
+
'aria-describedby',
|
|
2217
|
+
errorSpanId
|
|
2218
|
+
)
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
MOJFrontend.FormValidator.prototype.addValidator = function (fieldName, rules) {
|
|
1724
2222
|
this.validators.push({
|
|
1725
|
-
fieldName
|
|
1726
|
-
rules
|
|
2223
|
+
fieldName,
|
|
2224
|
+
rules,
|
|
1727
2225
|
field: this.form.elements[fieldName]
|
|
1728
|
-
})
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
MOJFrontend.FormValidator.prototype.validate = function() {
|
|
1732
|
-
this.errors = []
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
2226
|
+
})
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
MOJFrontend.FormValidator.prototype.validate = function () {
|
|
2230
|
+
this.errors = []
|
|
2231
|
+
let validator = null
|
|
2232
|
+
let validatorReturnValue = true
|
|
2233
|
+
let i
|
|
2234
|
+
let j
|
|
1737
2235
|
for (i = 0; i < this.validators.length; i++) {
|
|
1738
|
-
validator = this.validators[i]
|
|
2236
|
+
validator = this.validators[i]
|
|
1739
2237
|
for (j = 0; j < validator.rules.length; j++) {
|
|
1740
|
-
validatorReturnValue = validator.rules[j].method(
|
|
1741
|
-
validator.
|
|
2238
|
+
validatorReturnValue = validator.rules[j].method(
|
|
2239
|
+
validator.field,
|
|
2240
|
+
validator.rules[j].params
|
|
2241
|
+
)
|
|
1742
2242
|
|
|
1743
2243
|
if (typeof validatorReturnValue === 'boolean' && !validatorReturnValue) {
|
|
1744
2244
|
this.errors.push({
|
|
1745
2245
|
fieldName: validator.fieldName,
|
|
1746
2246
|
message: validator.rules[j].message
|
|
1747
|
-
})
|
|
1748
|
-
break
|
|
1749
|
-
} else if(typeof validatorReturnValue === 'string') {
|
|
2247
|
+
})
|
|
2248
|
+
break
|
|
2249
|
+
} else if (typeof validatorReturnValue === 'string') {
|
|
1750
2250
|
this.errors.push({
|
|
1751
2251
|
fieldName: validatorReturnValue,
|
|
1752
2252
|
message: validator.rules[j].message
|
|
1753
|
-
})
|
|
1754
|
-
break
|
|
2253
|
+
})
|
|
2254
|
+
break
|
|
1755
2255
|
}
|
|
1756
2256
|
}
|
|
1757
2257
|
}
|
|
1758
|
-
return this.errors.length === 0
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
|
|
2258
|
+
return this.errors.length === 0
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
if (
|
|
2262
|
+
MOJFrontend.dragAndDropSupported() &&
|
|
2263
|
+
MOJFrontend.formDataSupported() &&
|
|
2264
|
+
MOJFrontend.fileApiSupported()
|
|
2265
|
+
) {
|
|
2266
|
+
MOJFrontend.MultiFileUpload = function (params) {
|
|
1762
2267
|
this.defaultParams = {
|
|
1763
2268
|
uploadFileEntryHook: $.noop,
|
|
1764
2269
|
uploadFileExitHook: $.noop,
|
|
@@ -1767,120 +2272,131 @@ if(MOJFrontend.dragAndDropSupported() && MOJFrontend.formDataSupported() && MOJF
|
|
|
1767
2272
|
uploadStatusText: 'Uploading files, please wait',
|
|
1768
2273
|
dropzoneHintText: 'Drag and drop files here or',
|
|
1769
2274
|
dropzoneButtonText: 'Choose files'
|
|
1770
|
-
};
|
|
1771
|
-
|
|
1772
|
-
this.params = $.extend({}, this.defaultParams, params);
|
|
1773
|
-
this.container = $(this.params.container);
|
|
1774
|
-
|
|
1775
|
-
this.container.addClass('moj-multi-file-upload--enhanced');
|
|
1776
|
-
|
|
1777
|
-
this.feedbackContainer = this.container.find('.moj-multi-file__uploaded-files');
|
|
1778
|
-
this.setupFileInput();
|
|
1779
|
-
this.setupDropzone();
|
|
1780
|
-
this.setupLabel();
|
|
1781
|
-
this.setupStatusBox();
|
|
1782
|
-
this.container.on('click', '.moj-multi-file-upload__delete', $.proxy(this, 'onFileDeleteClick'));
|
|
1783
|
-
};
|
|
1784
|
-
|
|
1785
|
-
MOJFrontend.MultiFileUpload.prototype.setupDropzone = function() {
|
|
1786
|
-
this.fileInput.wrap('<div class="moj-multi-file-upload__dropzone" />');
|
|
1787
|
-
this.dropzone = this.container.find('.moj-multi-file-upload__dropzone');
|
|
1788
|
-
this.dropzone.on('dragover', $.proxy(this, 'onDragOver'));
|
|
1789
|
-
this.dropzone.on('dragleave', $.proxy(this, 'onDragLeave'));
|
|
1790
|
-
this.dropzone.on('drop', $.proxy(this, 'onDrop'));
|
|
1791
|
-
};
|
|
1792
|
-
|
|
1793
|
-
MOJFrontend.MultiFileUpload.prototype.setupLabel = function() {
|
|
1794
|
-
this.label = $('<label for="'+this.fileInput[0].id+'" class="govuk-button govuk-button--secondary">'+ this.params.dropzoneButtonText +'</label>');
|
|
1795
|
-
this.dropzone.append('<p class="govuk-body">' + this.params.dropzoneHintText + '</p>');
|
|
1796
|
-
this.dropzone.append(this.label);
|
|
1797
|
-
};
|
|
1798
|
-
|
|
1799
|
-
MOJFrontend.MultiFileUpload.prototype.setupFileInput = function() {
|
|
1800
|
-
this.fileInput = this.container.find('.moj-multi-file-upload__input');
|
|
1801
|
-
this.fileInput.on('change', $.proxy(this, 'onFileChange'));
|
|
1802
|
-
this.fileInput.on('focus', $.proxy(this, 'onFileFocus'));
|
|
1803
|
-
this.fileInput.on('blur', $.proxy(this, 'onFileBlur'));
|
|
1804
|
-
};
|
|
1805
|
-
|
|
1806
|
-
MOJFrontend.MultiFileUpload.prototype.setupStatusBox = function() {
|
|
1807
|
-
this.status = $('<div aria-live="polite" role="status" class="govuk-visually-hidden" />');
|
|
1808
|
-
this.dropzone.append(this.status);
|
|
1809
|
-
};
|
|
1810
|
-
|
|
1811
|
-
MOJFrontend.MultiFileUpload.prototype.onDragOver = function(e) {
|
|
1812
|
-
e.preventDefault();
|
|
1813
|
-
this.dropzone.addClass('moj-multi-file-upload--dragover');
|
|
1814
|
-
};
|
|
1815
|
-
|
|
1816
|
-
MOJFrontend.MultiFileUpload.prototype.onDragLeave = function() {
|
|
1817
|
-
this.dropzone.removeClass('moj-multi-file-upload--dragover');
|
|
1818
|
-
};
|
|
1819
|
-
|
|
1820
|
-
MOJFrontend.MultiFileUpload.prototype.onDrop = function(e) {
|
|
1821
|
-
e.preventDefault();
|
|
1822
|
-
this.dropzone.removeClass('moj-multi-file-upload--dragover');
|
|
1823
|
-
this.feedbackContainer.removeClass('moj-hidden');
|
|
1824
|
-
this.status.html(this.params.uploadStatusText);
|
|
1825
|
-
this.uploadFiles(e.originalEvent.dataTransfer.files);
|
|
1826
|
-
};
|
|
1827
|
-
|
|
1828
|
-
MOJFrontend.MultiFileUpload.prototype.uploadFiles = function(files) {
|
|
1829
|
-
for(var i = 0; i < files.length; i++) {
|
|
1830
|
-
this.uploadFile(files[i]);
|
|
1831
2275
|
}
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
this.
|
|
1837
|
-
|
|
1838
|
-
this.
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
this.
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
2276
|
+
|
|
2277
|
+
this.params = $.extend({}, this.defaultParams, params)
|
|
2278
|
+
this.container = $(this.params.container)
|
|
2279
|
+
|
|
2280
|
+
this.container.addClass('moj-multi-file-upload--enhanced')
|
|
2281
|
+
|
|
2282
|
+
this.feedbackContainer = this.container.find(
|
|
2283
|
+
'.moj-multi-file__uploaded-files'
|
|
2284
|
+
)
|
|
2285
|
+
this.setupFileInput()
|
|
2286
|
+
this.setupDropzone()
|
|
2287
|
+
this.setupLabel()
|
|
2288
|
+
this.setupStatusBox()
|
|
2289
|
+
this.container.on(
|
|
2290
|
+
'click',
|
|
2291
|
+
'.moj-multi-file-upload__delete',
|
|
2292
|
+
$.proxy(this, 'onFileDeleteClick')
|
|
2293
|
+
)
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
MOJFrontend.MultiFileUpload.prototype.setupDropzone = function () {
|
|
2297
|
+
this.fileInput.wrap('<div class="moj-multi-file-upload__dropzone" />')
|
|
2298
|
+
this.dropzone = this.container.find('.moj-multi-file-upload__dropzone')
|
|
2299
|
+
this.dropzone.on('dragover', $.proxy(this, 'onDragOver'))
|
|
2300
|
+
this.dropzone.on('dragleave', $.proxy(this, 'onDragLeave'))
|
|
2301
|
+
this.dropzone.on('drop', $.proxy(this, 'onDrop'))
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
MOJFrontend.MultiFileUpload.prototype.setupLabel = function () {
|
|
2305
|
+
this.label = $(
|
|
2306
|
+
`<label for="${this.fileInput[0].id}" class="govuk-button govuk-button--secondary">${this.params.dropzoneButtonText}</label>`
|
|
2307
|
+
)
|
|
2308
|
+
this.dropzone.append(
|
|
2309
|
+
`<p class="govuk-body">${this.params.dropzoneHintText}</p>`
|
|
2310
|
+
)
|
|
2311
|
+
this.dropzone.append(this.label)
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
MOJFrontend.MultiFileUpload.prototype.setupFileInput = function () {
|
|
2315
|
+
this.fileInput = this.container.find('.moj-multi-file-upload__input')
|
|
2316
|
+
this.fileInput.on('change', $.proxy(this, 'onFileChange'))
|
|
2317
|
+
this.fileInput.on('focus', $.proxy(this, 'onFileFocus'))
|
|
2318
|
+
this.fileInput.on('blur', $.proxy(this, 'onFileBlur'))
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
MOJFrontend.MultiFileUpload.prototype.setupStatusBox = function () {
|
|
2322
|
+
this.status = $(
|
|
2323
|
+
'<div aria-live="polite" role="status" class="govuk-visually-hidden" />'
|
|
2324
|
+
)
|
|
2325
|
+
this.dropzone.append(this.status)
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
MOJFrontend.MultiFileUpload.prototype.onDragOver = function (e) {
|
|
2329
|
+
e.preventDefault()
|
|
2330
|
+
this.dropzone.addClass('moj-multi-file-upload--dragover')
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
MOJFrontend.MultiFileUpload.prototype.onDragLeave = function () {
|
|
2334
|
+
this.dropzone.removeClass('moj-multi-file-upload--dragover')
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
MOJFrontend.MultiFileUpload.prototype.onDrop = function (e) {
|
|
2338
|
+
e.preventDefault()
|
|
2339
|
+
this.dropzone.removeClass('moj-multi-file-upload--dragover')
|
|
2340
|
+
this.feedbackContainer.removeClass('moj-hidden')
|
|
2341
|
+
this.status.html(this.params.uploadStatusText)
|
|
2342
|
+
this.uploadFiles(e.originalEvent.dataTransfer.files)
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
MOJFrontend.MultiFileUpload.prototype.uploadFiles = function (files) {
|
|
2346
|
+
for (let i = 0; i < files.length; i++) {
|
|
2347
|
+
this.uploadFile(files[i])
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
MOJFrontend.MultiFileUpload.prototype.onFileChange = function (e) {
|
|
2352
|
+
this.feedbackContainer.removeClass('moj-hidden')
|
|
2353
|
+
this.status.html(this.params.uploadStatusText)
|
|
2354
|
+
this.uploadFiles(e.currentTarget.files)
|
|
2355
|
+
this.fileInput.replaceWith($(e.currentTarget).val('').clone(true))
|
|
2356
|
+
this.setupFileInput()
|
|
2357
|
+
this.fileInput.get(0).focus()
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
MOJFrontend.MultiFileUpload.prototype.onFileFocus = function (e) {
|
|
2361
|
+
this.label.addClass('moj-multi-file-upload--focused')
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
MOJFrontend.MultiFileUpload.prototype.onFileBlur = function (e) {
|
|
2365
|
+
this.label.removeClass('moj-multi-file-upload--focused')
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
MOJFrontend.MultiFileUpload.prototype.getSuccessHtml = function (success) {
|
|
2369
|
+
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>`
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
MOJFrontend.MultiFileUpload.prototype.getErrorHtml = function (error) {
|
|
2373
|
+
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>`
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
MOJFrontend.MultiFileUpload.prototype.getFileRowHtml = function (file) {
|
|
2377
|
+
const html = `
|
|
2378
|
+
<div class="govuk-summary-list__row moj-multi-file-upload__row">;
|
|
2379
|
+
<div class="govuk-summary-list__value moj-multi-file-upload__message">;
|
|
2380
|
+
<span class="moj-multi-file-upload__filename">${file.name}</span>;
|
|
2381
|
+
<span class="moj-multi-file-upload__progress">0%</span>;
|
|
2382
|
+
</div>';
|
|
2383
|
+
<div class="govuk-summary-list__actions moj-multi-file-upload__actions"></div>;
|
|
2384
|
+
</div>`
|
|
2385
|
+
return html
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
MOJFrontend.MultiFileUpload.prototype.getDeleteButtonHtml = function (file) {
|
|
2389
|
+
return `<button class="moj-multi-file-upload__delete govuk-button govuk-button--secondary govuk-!-margin-bottom-0" type="button" name="delete" value="${file.filename}">
|
|
2390
|
+
Delete <span class="govuk-visually-hidden">${file.originalname}</span>
|
|
2391
|
+
</button>`
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
MOJFrontend.MultiFileUpload.prototype.uploadFile = function (file) {
|
|
2395
|
+
this.params.uploadFileEntryHook(this, file)
|
|
2396
|
+
const item = $(this.getFileRowHtml(file))
|
|
2397
|
+
const formData = new FormData()
|
|
2398
|
+
formData.append('documents', file)
|
|
2399
|
+
this.feedbackContainer.find('.moj-multi-file-upload__list').append(item)
|
|
1884
2400
|
|
|
1885
2401
|
$.ajax({
|
|
1886
2402
|
url: this.params.uploadUrl,
|
|
@@ -1888,481 +2404,543 @@ if(MOJFrontend.dragAndDropSupported() && MOJFrontend.formDataSupported() && MOJF
|
|
|
1888
2404
|
data: formData,
|
|
1889
2405
|
processData: false,
|
|
1890
2406
|
contentType: false,
|
|
1891
|
-
success: $.proxy(function(response){
|
|
1892
|
-
if(response.error) {
|
|
1893
|
-
item
|
|
1894
|
-
|
|
2407
|
+
success: $.proxy(function (response) {
|
|
2408
|
+
if (response.error) {
|
|
2409
|
+
item
|
|
2410
|
+
.find('.moj-multi-file-upload__message')
|
|
2411
|
+
.html(this.getErrorHtml(response.error))
|
|
2412
|
+
this.status.html(response.error.message)
|
|
1895
2413
|
} else {
|
|
1896
|
-
item
|
|
1897
|
-
|
|
2414
|
+
item
|
|
2415
|
+
.find('.moj-multi-file-upload__message')
|
|
2416
|
+
.html(this.getSuccessHtml(response.success))
|
|
2417
|
+
this.status.html(response.success.messageText)
|
|
1898
2418
|
}
|
|
1899
|
-
item
|
|
1900
|
-
|
|
2419
|
+
item
|
|
2420
|
+
.find('.moj-multi-file-upload__actions')
|
|
2421
|
+
.append(this.getDeleteButtonHtml(response.file))
|
|
2422
|
+
this.params.uploadFileExitHook(this, file, response)
|
|
1901
2423
|
}, this),
|
|
1902
|
-
error: $.proxy(function(jqXHR, textStatus, errorThrown) {
|
|
1903
|
-
this.params.uploadFileErrorHook(
|
|
2424
|
+
error: $.proxy(function (jqXHR, textStatus, errorThrown) {
|
|
2425
|
+
this.params.uploadFileErrorHook(
|
|
2426
|
+
this,
|
|
2427
|
+
file,
|
|
2428
|
+
jqXHR,
|
|
2429
|
+
textStatus,
|
|
2430
|
+
errorThrown
|
|
2431
|
+
)
|
|
1904
2432
|
}, this),
|
|
1905
|
-
xhr: function() {
|
|
1906
|
-
|
|
1907
|
-
xhr.upload.addEventListener(
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
2433
|
+
xhr: function () {
|
|
2434
|
+
const xhr = new XMLHttpRequest()
|
|
2435
|
+
xhr.upload.addEventListener(
|
|
2436
|
+
'progress',
|
|
2437
|
+
function (e) {
|
|
2438
|
+
if (e.lengthComputable) {
|
|
2439
|
+
let percentComplete = e.loaded / e.total
|
|
2440
|
+
percentComplete = parseInt(percentComplete * 100, 10)
|
|
2441
|
+
item
|
|
2442
|
+
.find('.moj-multi-file-upload__progress')
|
|
2443
|
+
.text(` ${percentComplete}%`)
|
|
2444
|
+
}
|
|
2445
|
+
},
|
|
2446
|
+
false
|
|
2447
|
+
)
|
|
2448
|
+
return xhr
|
|
1915
2449
|
}
|
|
1916
|
-
})
|
|
1917
|
-
}
|
|
1918
|
-
|
|
1919
|
-
MOJFrontend.MultiFileUpload.prototype.onFileDeleteClick = function(e) {
|
|
1920
|
-
e.preventDefault()
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
data[button[0].name] = button[0].value
|
|
2450
|
+
})
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
MOJFrontend.MultiFileUpload.prototype.onFileDeleteClick = function (e) {
|
|
2454
|
+
e.preventDefault() // if user refreshes page and then deletes
|
|
2455
|
+
const button = $(e.currentTarget)
|
|
2456
|
+
const data = {}
|
|
2457
|
+
data[button[0].name] = button[0].value
|
|
1924
2458
|
$.ajax({
|
|
1925
2459
|
url: this.params.deleteUrl,
|
|
1926
2460
|
type: 'post',
|
|
1927
2461
|
dataType: 'json',
|
|
1928
|
-
data
|
|
1929
|
-
success: $.proxy(function(response){
|
|
1930
|
-
if(response.error) {
|
|
2462
|
+
data,
|
|
2463
|
+
success: $.proxy(function (response) {
|
|
2464
|
+
if (response.error) {
|
|
1931
2465
|
// handle error
|
|
1932
2466
|
} else {
|
|
1933
|
-
button.parents('.moj-multi-file-upload__row').remove()
|
|
1934
|
-
if(
|
|
1935
|
-
this.feedbackContainer.
|
|
2467
|
+
button.parents('.moj-multi-file-upload__row').remove()
|
|
2468
|
+
if (
|
|
2469
|
+
this.feedbackContainer.find('.moj-multi-file-upload__row')
|
|
2470
|
+
.length === 0
|
|
2471
|
+
) {
|
|
2472
|
+
this.feedbackContainer.addClass('moj-hidden')
|
|
1936
2473
|
}
|
|
1937
2474
|
}
|
|
1938
|
-
this.params.fileDeleteHook(this, response)
|
|
2475
|
+
this.params.fileDeleteHook(this, response)
|
|
1939
2476
|
}, this)
|
|
1940
|
-
})
|
|
1941
|
-
}
|
|
2477
|
+
})
|
|
2478
|
+
}
|
|
1942
2479
|
}
|
|
1943
2480
|
|
|
1944
|
-
MOJFrontend.MultiSelect = function(options) {
|
|
1945
|
-
this.container = $(options.container)
|
|
2481
|
+
MOJFrontend.MultiSelect = function (options) {
|
|
2482
|
+
this.container = $(options.container)
|
|
1946
2483
|
|
|
1947
2484
|
if (this.container.data('moj-multi-select-initialised')) {
|
|
1948
2485
|
return
|
|
1949
2486
|
}
|
|
1950
2487
|
|
|
1951
|
-
this.container.data('moj-multi-select-initialised', true)
|
|
2488
|
+
this.container.data('moj-multi-select-initialised', true)
|
|
1952
2489
|
|
|
1953
|
-
const idPrefix = options.id_prefix
|
|
1954
|
-
let allId = 'checkboxes-all'
|
|
2490
|
+
const idPrefix = options.id_prefix
|
|
2491
|
+
let allId = 'checkboxes-all'
|
|
1955
2492
|
if (typeof idPrefix !== 'undefined') {
|
|
1956
|
-
allId = idPrefix
|
|
2493
|
+
allId = `${idPrefix}checkboxes-all`
|
|
1957
2494
|
}
|
|
1958
2495
|
|
|
1959
|
-
this.toggle = $(this.getToggleHtml(allId))
|
|
1960
|
-
this.toggleButton = this.toggle.find('input')
|
|
1961
|
-
this.toggleButton.on('click', $.proxy(this, 'onButtonClick'))
|
|
1962
|
-
this.container.append(this.toggle)
|
|
1963
|
-
this.checkboxes = $(options.checkboxes)
|
|
1964
|
-
this.checkboxes.on('click', $.proxy(this, 'onCheckboxClick'))
|
|
1965
|
-
this.checked = options.checked || false
|
|
1966
|
-
}
|
|
2496
|
+
this.toggle = $(this.getToggleHtml(allId))
|
|
2497
|
+
this.toggleButton = this.toggle.find('input')
|
|
2498
|
+
this.toggleButton.on('click', $.proxy(this, 'onButtonClick'))
|
|
2499
|
+
this.container.append(this.toggle)
|
|
2500
|
+
this.checkboxes = $(options.checkboxes)
|
|
2501
|
+
this.checkboxes.on('click', $.proxy(this, 'onCheckboxClick'))
|
|
2502
|
+
this.checked = options.checked || false
|
|
2503
|
+
}
|
|
1967
2504
|
|
|
1968
2505
|
MOJFrontend.MultiSelect.prototype.getToggleHtml = function (allId) {
|
|
1969
|
-
let html = ''
|
|
1970
|
-
html +=
|
|
1971
|
-
|
|
1972
|
-
html += ` <
|
|
1973
|
-
html +=
|
|
1974
|
-
html += '
|
|
1975
|
-
html += '</
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
this.
|
|
2506
|
+
let html = ''
|
|
2507
|
+
html +=
|
|
2508
|
+
'<div class="govuk-checkboxes__item govuk-checkboxes--small moj-multi-select__checkbox">'
|
|
2509
|
+
html += ` <input type="checkbox" class="govuk-checkboxes__input" id="${allId}">`
|
|
2510
|
+
html += ` <label class="govuk-label govuk-checkboxes__label moj-multi-select__toggle-label" for="${allId}">`
|
|
2511
|
+
html += ' <span class="govuk-visually-hidden">Select all</span>'
|
|
2512
|
+
html += ' </label>'
|
|
2513
|
+
html += '</div>'
|
|
2514
|
+
return html
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
MOJFrontend.MultiSelect.prototype.onButtonClick = function (e) {
|
|
2518
|
+
if (this.checked) {
|
|
2519
|
+
this.uncheckAll()
|
|
2520
|
+
this.toggleButton[0].checked = false
|
|
1983
2521
|
} else {
|
|
1984
|
-
this.checkAll()
|
|
1985
|
-
this.toggleButton[0].checked = true
|
|
1986
|
-
}
|
|
1987
|
-
}
|
|
1988
|
-
|
|
1989
|
-
MOJFrontend.MultiSelect.prototype.checkAll = function() {
|
|
1990
|
-
this.checkboxes.each(
|
|
1991
|
-
el
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2522
|
+
this.checkAll()
|
|
2523
|
+
this.toggleButton[0].checked = true
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
MOJFrontend.MultiSelect.prototype.checkAll = function () {
|
|
2528
|
+
this.checkboxes.each(
|
|
2529
|
+
$.proxy(function (index, el) {
|
|
2530
|
+
el.checked = true
|
|
2531
|
+
}, this)
|
|
2532
|
+
)
|
|
2533
|
+
this.checked = true
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
MOJFrontend.MultiSelect.prototype.uncheckAll = function () {
|
|
2537
|
+
this.checkboxes.each(
|
|
2538
|
+
$.proxy(function (index, el) {
|
|
2539
|
+
el.checked = false
|
|
2540
|
+
}, this)
|
|
2541
|
+
)
|
|
2542
|
+
this.checked = false
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
MOJFrontend.MultiSelect.prototype.onCheckboxClick = function (e) {
|
|
2546
|
+
if (!e.target.checked) {
|
|
2547
|
+
this.toggleButton[0].checked = false
|
|
2548
|
+
this.checked = false
|
|
2007
2549
|
} else {
|
|
2008
|
-
if(this.checkboxes.filter(':checked').length === this.checkboxes.length) {
|
|
2009
|
-
this.toggleButton[0].checked = true
|
|
2010
|
-
this.checked = true
|
|
2550
|
+
if (this.checkboxes.filter(':checked').length === this.checkboxes.length) {
|
|
2551
|
+
this.toggleButton[0].checked = true
|
|
2552
|
+
this.checked = true
|
|
2011
2553
|
}
|
|
2012
2554
|
}
|
|
2013
|
-
}
|
|
2555
|
+
}
|
|
2014
2556
|
|
|
2015
|
-
MOJFrontend.PasswordReveal = function(element) {
|
|
2016
|
-
this.el = element
|
|
2017
|
-
|
|
2557
|
+
MOJFrontend.PasswordReveal = function (element) {
|
|
2558
|
+
this.el = element
|
|
2559
|
+
const $el = $(this.el)
|
|
2018
2560
|
|
|
2019
2561
|
if ($el.data('moj-password-reveal-initialised')) {
|
|
2020
2562
|
return
|
|
2021
2563
|
}
|
|
2022
2564
|
|
|
2023
|
-
$el.data('moj-password-reveal-initialised', true)
|
|
2024
|
-
$el.attr('spellcheck', 'false')
|
|
2565
|
+
$el.data('moj-password-reveal-initialised', true)
|
|
2566
|
+
$el.attr('spellcheck', 'false')
|
|
2025
2567
|
|
|
2026
|
-
$el.wrap('<div class="moj-password-reveal"></div>')
|
|
2027
|
-
this.container = $(this.el).parent()
|
|
2028
|
-
this.createButton()
|
|
2029
|
-
}
|
|
2568
|
+
$el.wrap('<div class="moj-password-reveal"></div>')
|
|
2569
|
+
this.container = $(this.el).parent()
|
|
2570
|
+
this.createButton()
|
|
2571
|
+
}
|
|
2030
2572
|
|
|
2031
|
-
MOJFrontend.PasswordReveal.prototype.createButton = function() {
|
|
2032
|
-
this.button = $(
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2573
|
+
MOJFrontend.PasswordReveal.prototype.createButton = function () {
|
|
2574
|
+
this.button = $(
|
|
2575
|
+
'<button type="button" class="govuk-button govuk-button--secondary moj-password-reveal__button">Show <span class="govuk-visually-hidden">password</span></button>'
|
|
2576
|
+
)
|
|
2577
|
+
this.container.append(this.button)
|
|
2578
|
+
this.button.on('click', $.proxy(this, 'onButtonClick'))
|
|
2579
|
+
}
|
|
2036
2580
|
|
|
2037
|
-
MOJFrontend.PasswordReveal.prototype.onButtonClick = function() {
|
|
2581
|
+
MOJFrontend.PasswordReveal.prototype.onButtonClick = function () {
|
|
2038
2582
|
if (this.el.type === 'password') {
|
|
2039
|
-
this.el.type = 'text'
|
|
2040
|
-
this.button.html('Hide <span class="govuk-visually-hidden">password</span>')
|
|
2583
|
+
this.el.type = 'text'
|
|
2584
|
+
this.button.html('Hide <span class="govuk-visually-hidden">password</span>')
|
|
2041
2585
|
} else {
|
|
2042
|
-
this.el.type = 'password'
|
|
2043
|
-
this.button.html('Show <span class="govuk-visually-hidden">password</span>')
|
|
2586
|
+
this.el.type = 'password'
|
|
2587
|
+
this.button.html('Show <span class="govuk-visually-hidden">password</span>')
|
|
2044
2588
|
}
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2589
|
+
}
|
|
2047
2590
|
|
|
2048
|
-
if('contentEditable' in document.documentElement) {
|
|
2049
|
-
MOJFrontend.RichTextEditor = function(options) {
|
|
2050
|
-
this.options = options
|
|
2591
|
+
if ('contentEditable' in document.documentElement) {
|
|
2592
|
+
MOJFrontend.RichTextEditor = function (options) {
|
|
2593
|
+
this.options = options
|
|
2051
2594
|
this.options.toolbar = this.options.toolbar || {
|
|
2052
2595
|
bold: false,
|
|
2053
2596
|
italic: false,
|
|
2054
2597
|
underline: false,
|
|
2055
2598
|
bullets: true,
|
|
2056
2599
|
numbers: true
|
|
2057
|
-
}
|
|
2058
|
-
this.textarea = this.options.textarea
|
|
2059
|
-
this.container = $(this.textarea).parent()
|
|
2600
|
+
}
|
|
2601
|
+
this.textarea = this.options.textarea
|
|
2602
|
+
this.container = $(this.textarea).parent()
|
|
2060
2603
|
|
|
2061
2604
|
if (this.container.data('moj-rich-text-editor-initialised')) {
|
|
2062
2605
|
return
|
|
2063
2606
|
}
|
|
2064
2607
|
|
|
2065
|
-
this.container.data('moj-rich-text-editor-initialised', true)
|
|
2608
|
+
this.container.data('moj-rich-text-editor-initialised', true)
|
|
2066
2609
|
|
|
2067
|
-
this.createToolbar()
|
|
2068
|
-
this.hideDefault()
|
|
2069
|
-
this.configureToolbar()
|
|
2610
|
+
this.createToolbar()
|
|
2611
|
+
this.hideDefault()
|
|
2612
|
+
this.configureToolbar()
|
|
2070
2613
|
this.keys = {
|
|
2071
2614
|
left: 37,
|
|
2072
2615
|
right: 39,
|
|
2073
2616
|
up: 38,
|
|
2074
2617
|
down: 40
|
|
2075
|
-
}
|
|
2076
|
-
this.container.on(
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2618
|
+
}
|
|
2619
|
+
this.container.on(
|
|
2620
|
+
'click',
|
|
2621
|
+
'.moj-rich-text-editor__toolbar-button',
|
|
2622
|
+
$.proxy(this, 'onButtonClick')
|
|
2623
|
+
)
|
|
2624
|
+
this.container
|
|
2625
|
+
.find('.moj-rich-text-editor__content')
|
|
2626
|
+
.on('input', $.proxy(this, 'onEditorInput'))
|
|
2627
|
+
this.container.find('label').on('click', $.proxy(this, 'onLabelClick'))
|
|
2628
|
+
this.toolbar.on('keydown', $.proxy(this, 'onToolbarKeydown'))
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
MOJFrontend.RichTextEditor.prototype.onToolbarKeydown = function (e) {
|
|
2632
|
+
let focusableButton
|
|
2633
|
+
switch (e.keyCode) {
|
|
2085
2634
|
case this.keys.right:
|
|
2086
|
-
case this.keys.down:
|
|
2087
|
-
focusableButton = this.toolbar.find('button[tabindex=0]')
|
|
2088
|
-
|
|
2089
|
-
if(nextButton[0]) {
|
|
2090
|
-
nextButton.focus()
|
|
2091
|
-
focusableButton.attr('tabindex', '-1')
|
|
2092
|
-
nextButton.attr('tabindex', '0')
|
|
2635
|
+
case this.keys.down: {
|
|
2636
|
+
focusableButton = this.toolbar.find('button[tabindex=0]')
|
|
2637
|
+
const nextButton = focusableButton.next('button')
|
|
2638
|
+
if (nextButton[0]) {
|
|
2639
|
+
nextButton.focus()
|
|
2640
|
+
focusableButton.attr('tabindex', '-1')
|
|
2641
|
+
nextButton.attr('tabindex', '0')
|
|
2093
2642
|
}
|
|
2094
|
-
break
|
|
2643
|
+
break
|
|
2644
|
+
}
|
|
2095
2645
|
case this.keys.left:
|
|
2096
|
-
case this.keys.up:
|
|
2097
|
-
focusableButton = this.toolbar.find('button[tabindex=0]')
|
|
2098
|
-
|
|
2099
|
-
if(previousButton[0]) {
|
|
2100
|
-
previousButton.focus()
|
|
2101
|
-
focusableButton.attr('tabindex', '-1')
|
|
2102
|
-
previousButton.attr('tabindex', '0')
|
|
2646
|
+
case this.keys.up: {
|
|
2647
|
+
focusableButton = this.toolbar.find('button[tabindex=0]')
|
|
2648
|
+
const previousButton = focusableButton.prev('button')
|
|
2649
|
+
if (previousButton[0]) {
|
|
2650
|
+
previousButton.focus()
|
|
2651
|
+
focusableButton.attr('tabindex', '-1')
|
|
2652
|
+
previousButton.attr('tabindex', '0')
|
|
2103
2653
|
}
|
|
2104
|
-
break
|
|
2654
|
+
break
|
|
2655
|
+
}
|
|
2105
2656
|
}
|
|
2106
|
-
}
|
|
2657
|
+
}
|
|
2107
2658
|
|
|
2108
|
-
MOJFrontend.RichTextEditor.prototype.getToolbarHtml = function() {
|
|
2109
|
-
|
|
2659
|
+
MOJFrontend.RichTextEditor.prototype.getToolbarHtml = function () {
|
|
2660
|
+
let html = ''
|
|
2110
2661
|
|
|
2111
|
-
html += '<div class="moj-rich-text-editor__toolbar" role="toolbar">'
|
|
2662
|
+
html += '<div class="moj-rich-text-editor__toolbar" role="toolbar">'
|
|
2112
2663
|
|
|
2113
|
-
if(this.options.toolbar.bold) {
|
|
2114
|
-
html +=
|
|
2664
|
+
if (this.options.toolbar.bold) {
|
|
2665
|
+
html +=
|
|
2666
|
+
'<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>'
|
|
2115
2667
|
}
|
|
2116
2668
|
|
|
2117
|
-
if(this.options.toolbar.italic) {
|
|
2118
|
-
html +=
|
|
2669
|
+
if (this.options.toolbar.italic) {
|
|
2670
|
+
html +=
|
|
2671
|
+
'<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>'
|
|
2119
2672
|
}
|
|
2120
2673
|
|
|
2121
|
-
if(this.options.toolbar.underline) {
|
|
2122
|
-
html +=
|
|
2674
|
+
if (this.options.toolbar.underline) {
|
|
2675
|
+
html +=
|
|
2676
|
+
'<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>'
|
|
2123
2677
|
}
|
|
2124
2678
|
|
|
2125
|
-
if(this.options.toolbar.bullets) {
|
|
2126
|
-
html +=
|
|
2679
|
+
if (this.options.toolbar.bullets) {
|
|
2680
|
+
html +=
|
|
2681
|
+
'<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>'
|
|
2127
2682
|
}
|
|
2128
2683
|
|
|
2129
|
-
if(this.options.toolbar.numbers) {
|
|
2130
|
-
html +=
|
|
2684
|
+
if (this.options.toolbar.numbers) {
|
|
2685
|
+
html +=
|
|
2686
|
+
'<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>'
|
|
2131
2687
|
}
|
|
2132
2688
|
|
|
2133
|
-
html += '</div>'
|
|
2134
|
-
return html
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
MOJFrontend.RichTextEditor.prototype.getEnhancedHtml = function(val) {
|
|
2138
|
-
return this.getToolbarHtml()
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
MOJFrontend.RichTextEditor.prototype.hideDefault = function() {
|
|
2142
|
-
this.textarea = this.container.find('textarea')
|
|
2143
|
-
this.textarea.addClass('govuk-visually-hidden')
|
|
2144
|
-
this.textarea.attr('aria-hidden', true)
|
|
2145
|
-
this.textarea.attr('tabindex', '-1')
|
|
2146
|
-
}
|
|
2147
|
-
|
|
2148
|
-
MOJFrontend.RichTextEditor.prototype.createToolbar = function() {
|
|
2149
|
-
this.toolbar = document.createElement('div')
|
|
2150
|
-
this.toolbar.className = 'moj-rich-text-editor'
|
|
2151
|
-
this.toolbar.innerHTML = this.getEnhancedHtml()
|
|
2152
|
-
this.container.append(this.toolbar)
|
|
2153
|
-
this.toolbar = this.container.find('.moj-rich-text-editor__toolbar')
|
|
2154
|
-
this.container
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
e.preventDefault();
|
|
2183
|
-
this.container.find('.moj-rich-text-editor__content').focus();
|
|
2184
|
-
};
|
|
2689
|
+
html += '</div>'
|
|
2690
|
+
return html
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
MOJFrontend.RichTextEditor.prototype.getEnhancedHtml = function (val) {
|
|
2694
|
+
return `${this.getToolbarHtml()}<div class="govuk-textarea moj-rich-text-editor__content" contenteditable="true" spellcheck="false"></div>`
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
MOJFrontend.RichTextEditor.prototype.hideDefault = function () {
|
|
2698
|
+
this.textarea = this.container.find('textarea')
|
|
2699
|
+
this.textarea.addClass('govuk-visually-hidden')
|
|
2700
|
+
this.textarea.attr('aria-hidden', true)
|
|
2701
|
+
this.textarea.attr('tabindex', '-1')
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
MOJFrontend.RichTextEditor.prototype.createToolbar = function () {
|
|
2705
|
+
this.toolbar = document.createElement('div')
|
|
2706
|
+
this.toolbar.className = 'moj-rich-text-editor'
|
|
2707
|
+
this.toolbar.innerHTML = this.getEnhancedHtml()
|
|
2708
|
+
this.container.append(this.toolbar)
|
|
2709
|
+
this.toolbar = this.container.find('.moj-rich-text-editor__toolbar')
|
|
2710
|
+
this.container
|
|
2711
|
+
.find('.moj-rich-text-editor__content')
|
|
2712
|
+
.html(this.textarea.val())
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
MOJFrontend.RichTextEditor.prototype.configureToolbar = function () {
|
|
2716
|
+
this.buttons = this.container.find('.moj-rich-text-editor__toolbar-button')
|
|
2717
|
+
this.buttons.prop('tabindex', '-1')
|
|
2718
|
+
const firstTab = this.buttons.first()
|
|
2719
|
+
firstTab.prop('tabindex', '0')
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
MOJFrontend.RichTextEditor.prototype.onButtonClick = function (e) {
|
|
2723
|
+
document.execCommand($(e.currentTarget).data('command'), false, null)
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
MOJFrontend.RichTextEditor.prototype.getContent = function () {
|
|
2727
|
+
return this.container.find('.moj-rich-text-editor__content').html()
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
MOJFrontend.RichTextEditor.prototype.onEditorInput = function (e) {
|
|
2731
|
+
this.updateTextarea()
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
MOJFrontend.RichTextEditor.prototype.updateTextarea = function () {
|
|
2735
|
+
document.execCommand('defaultParagraphSeparator', false, 'p')
|
|
2736
|
+
this.textarea.val(this.getContent())
|
|
2737
|
+
}
|
|
2185
2738
|
|
|
2739
|
+
MOJFrontend.RichTextEditor.prototype.onLabelClick = function (e) {
|
|
2740
|
+
e.preventDefault()
|
|
2741
|
+
this.container.find('.moj-rich-text-editor__content').focus()
|
|
2742
|
+
}
|
|
2186
2743
|
}
|
|
2187
2744
|
|
|
2188
2745
|
MOJFrontend.SearchToggle = function (options) {
|
|
2189
|
-
this.options = options
|
|
2190
|
-
this.container = $(this.options.search.container)
|
|
2191
|
-
this.toggleButtonContainer = $(this.options.toggleButton.container)
|
|
2746
|
+
this.options = options
|
|
2747
|
+
this.container = $(this.options.search.container)
|
|
2748
|
+
this.toggleButtonContainer = $(this.options.toggleButton.container)
|
|
2192
2749
|
|
|
2193
|
-
if (this.container.data(
|
|
2194
|
-
return
|
|
2750
|
+
if (this.container.data('moj-search-toggle-initialised')) {
|
|
2751
|
+
return
|
|
2195
2752
|
}
|
|
2196
2753
|
|
|
2197
|
-
this.container.data(
|
|
2754
|
+
this.container.data('moj-search-toggle-initialised', true)
|
|
2198
2755
|
|
|
2199
2756
|
const svg =
|
|
2200
|
-
'<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>'
|
|
2757
|
+
'<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>'
|
|
2201
2758
|
|
|
2202
2759
|
this.toggleButton = $(
|
|
2203
|
-
|
|
2204
|
-
this.options.toggleButton.text
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
)
|
|
2208
|
-
this.
|
|
2209
|
-
this.
|
|
2210
|
-
$(document).on(
|
|
2211
|
-
|
|
2212
|
-
};
|
|
2760
|
+
`<button class="moj-search-toggle__button" type="button" aria-haspopup="true" aria-expanded="false">
|
|
2761
|
+
${this.options.toggleButton.text} ${svg}
|
|
2762
|
+
</button>`
|
|
2763
|
+
)
|
|
2764
|
+
this.toggleButton.on('click', $.proxy(this, 'onToggleButtonClick'))
|
|
2765
|
+
this.toggleButtonContainer.append(this.toggleButton)
|
|
2766
|
+
$(document).on('click', this.onDocumentClick.bind(this))
|
|
2767
|
+
$(document).on('focusin', this.onDocumentClick.bind(this))
|
|
2768
|
+
}
|
|
2213
2769
|
|
|
2214
2770
|
MOJFrontend.SearchToggle.prototype.showMenu = function () {
|
|
2215
|
-
this.toggleButton.attr(
|
|
2216
|
-
this.container.removeClass(
|
|
2217
|
-
this.container.find(
|
|
2218
|
-
}
|
|
2771
|
+
this.toggleButton.attr('aria-expanded', 'true')
|
|
2772
|
+
this.container.removeClass('moj-js-hidden')
|
|
2773
|
+
this.container.find('input').first().get(0).focus()
|
|
2774
|
+
}
|
|
2219
2775
|
|
|
2220
2776
|
MOJFrontend.SearchToggle.prototype.hideMenu = function () {
|
|
2221
|
-
this.container.addClass(
|
|
2222
|
-
this.toggleButton.attr(
|
|
2223
|
-
}
|
|
2777
|
+
this.container.addClass('moj-js-hidden')
|
|
2778
|
+
this.toggleButton.attr('aria-expanded', 'false')
|
|
2779
|
+
}
|
|
2224
2780
|
|
|
2225
2781
|
MOJFrontend.SearchToggle.prototype.onToggleButtonClick = function () {
|
|
2226
|
-
if (this.toggleButton.attr(
|
|
2227
|
-
this.showMenu()
|
|
2782
|
+
if (this.toggleButton.attr('aria-expanded') === 'false') {
|
|
2783
|
+
this.showMenu()
|
|
2228
2784
|
} else {
|
|
2229
|
-
this.hideMenu()
|
|
2785
|
+
this.hideMenu()
|
|
2230
2786
|
}
|
|
2231
|
-
}
|
|
2787
|
+
}
|
|
2232
2788
|
|
|
2233
2789
|
MOJFrontend.SearchToggle.prototype.onDocumentClick = function (e) {
|
|
2234
2790
|
if (
|
|
2235
2791
|
!$.contains(this.toggleButtonContainer[0], e.target) &&
|
|
2236
2792
|
!$.contains(this.container[0], e.target)
|
|
2237
2793
|
) {
|
|
2238
|
-
this.hideMenu()
|
|
2239
|
-
}
|
|
2240
|
-
}
|
|
2241
|
-
|
|
2242
|
-
MOJFrontend.SortableTable = function(params) {
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
this.initialiseSortedColumn()
|
|
2256
|
-
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
|
-
MOJFrontend.SortableTable.prototype.setupOptions = function(params) {
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
}
|
|
2265
|
-
|
|
2266
|
-
MOJFrontend.SortableTable.prototype.createHeadingButtons = function() {
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2277
|
-
MOJFrontend.SortableTable.prototype.createHeadingButton = function(
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
}
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2794
|
+
this.hideMenu()
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
MOJFrontend.SortableTable = function (params) {
|
|
2799
|
+
this.table = $(params.table)
|
|
2800
|
+
|
|
2801
|
+
if (this.table.data('moj-search-toggle-initialised')) {
|
|
2802
|
+
return
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
this.table.data('moj-search-toggle-initialised', true)
|
|
2806
|
+
|
|
2807
|
+
this.setupOptions(params)
|
|
2808
|
+
this.body = this.table.find('tbody')
|
|
2809
|
+
this.createHeadingButtons()
|
|
2810
|
+
this.createStatusBox()
|
|
2811
|
+
this.initialiseSortedColumn()
|
|
2812
|
+
this.table.on('click', 'th button', $.proxy(this, 'onSortButtonClick'))
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
MOJFrontend.SortableTable.prototype.setupOptions = function (params) {
|
|
2816
|
+
params = params || {}
|
|
2817
|
+
this.statusMessage = params.statusMessage || 'Sort by %heading% (%direction%)'
|
|
2818
|
+
this.ascendingText = params.ascendingText || 'ascending'
|
|
2819
|
+
this.descendingText = params.descendingText || 'descending'
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
MOJFrontend.SortableTable.prototype.createHeadingButtons = function () {
|
|
2823
|
+
const headings = this.table.find('thead th')
|
|
2824
|
+
let heading
|
|
2825
|
+
for (let i = 0; i < headings.length; i++) {
|
|
2826
|
+
heading = $(headings[i])
|
|
2827
|
+
if (heading.attr('aria-sort')) {
|
|
2828
|
+
this.createHeadingButton(heading, i)
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
MOJFrontend.SortableTable.prototype.createHeadingButton = function (
|
|
2834
|
+
heading,
|
|
2835
|
+
i
|
|
2836
|
+
) {
|
|
2837
|
+
const text = heading.text()
|
|
2838
|
+
const button = $(`<button type="button" data-index="${i}">${text}</button>`)
|
|
2839
|
+
heading.text('')
|
|
2840
|
+
heading.append(button)
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
MOJFrontend.SortableTable.prototype.createStatusBox = function () {
|
|
2844
|
+
this.status = $(
|
|
2845
|
+
'<div aria-live="polite" role="status" aria-atomic="true" class="govuk-visually-hidden" />'
|
|
2846
|
+
)
|
|
2847
|
+
this.table.parent().append(this.status)
|
|
2848
|
+
}
|
|
2288
2849
|
|
|
2289
2850
|
MOJFrontend.SortableTable.prototype.initialiseSortedColumn = function () {
|
|
2290
|
-
|
|
2851
|
+
const rows = this.getTableRowsArray()
|
|
2291
2852
|
|
|
2292
|
-
this.table
|
|
2853
|
+
this.table
|
|
2854
|
+
.find('th')
|
|
2293
2855
|
.filter('[aria-sort="ascending"], [aria-sort="descending"]')
|
|
2294
2856
|
.first()
|
|
2295
2857
|
.each((index, el) => {
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
this.addRows(sortedRows)
|
|
2858
|
+
const sortDirection = $(el).attr('aria-sort')
|
|
2859
|
+
const columnNumber = $(el).find('button').attr('data-index')
|
|
2860
|
+
const sortedRows = this.sort(rows, columnNumber, sortDirection)
|
|
2861
|
+
this.addRows(sortedRows)
|
|
2300
2862
|
})
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
|
-
MOJFrontend.SortableTable.prototype.onSortButtonClick = function(e) {
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
}
|
|
2318
|
-
|
|
2319
|
-
MOJFrontend.SortableTable.prototype.updateButtonState = function(
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
MOJFrontend.SortableTable.prototype.onSortButtonClick = function (e) {
|
|
2866
|
+
const columnNumber = e.currentTarget.getAttribute('data-index')
|
|
2867
|
+
const sortDirection = $(e.currentTarget).parent().attr('aria-sort')
|
|
2868
|
+
let newSortDirection
|
|
2869
|
+
if (sortDirection === 'none' || sortDirection === 'descending') {
|
|
2870
|
+
newSortDirection = 'ascending'
|
|
2871
|
+
} else {
|
|
2872
|
+
newSortDirection = 'descending'
|
|
2873
|
+
}
|
|
2874
|
+
const rows = this.getTableRowsArray()
|
|
2875
|
+
const sortedRows = this.sort(rows, columnNumber, newSortDirection)
|
|
2876
|
+
this.addRows(sortedRows)
|
|
2877
|
+
this.removeButtonStates()
|
|
2878
|
+
this.updateButtonState($(e.currentTarget), newSortDirection)
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
MOJFrontend.SortableTable.prototype.updateButtonState = function (
|
|
2882
|
+
button,
|
|
2883
|
+
direction
|
|
2884
|
+
) {
|
|
2885
|
+
button.parent().attr('aria-sort', direction)
|
|
2886
|
+
let message = this.statusMessage
|
|
2887
|
+
message = message.replace(/%heading%/, button.text())
|
|
2888
|
+
message = message.replace(/%direction%/, this[`${direction}Text`])
|
|
2889
|
+
this.status.text(message)
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
MOJFrontend.SortableTable.prototype.removeButtonStates = function () {
|
|
2893
|
+
this.table.find('thead th').attr('aria-sort', 'none')
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
MOJFrontend.SortableTable.prototype.addRows = function (rows) {
|
|
2897
|
+
for (let i = 0; i < rows.length; i++) {
|
|
2898
|
+
this.body.append(rows[i])
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
MOJFrontend.SortableTable.prototype.getTableRowsArray = function () {
|
|
2903
|
+
const rows = []
|
|
2904
|
+
const trs = this.body.find('tr')
|
|
2905
|
+
for (let i = 0; i < trs.length; i++) {
|
|
2906
|
+
rows.push(trs[i])
|
|
2907
|
+
}
|
|
2908
|
+
return rows
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
MOJFrontend.SortableTable.prototype.sort = function (
|
|
2912
|
+
rows,
|
|
2913
|
+
columnNumber,
|
|
2914
|
+
sortDirection
|
|
2915
|
+
) {
|
|
2916
|
+
const newRows = rows.sort(
|
|
2917
|
+
function (rowA, rowB) {
|
|
2918
|
+
const tdA = $(rowA).find('td,th').eq(columnNumber)
|
|
2919
|
+
const tdB = $(rowB).find('td,th').eq(columnNumber)
|
|
2920
|
+
|
|
2921
|
+
const valueA =
|
|
2922
|
+
sortDirection === 'ascending'
|
|
2923
|
+
? this.getCellValue(tdA)
|
|
2924
|
+
: this.getCellValue(tdB)
|
|
2925
|
+
const valueB =
|
|
2926
|
+
sortDirection === 'ascending'
|
|
2927
|
+
? this.getCellValue(tdB)
|
|
2928
|
+
: this.getCellValue(tdA)
|
|
2929
|
+
|
|
2930
|
+
if (typeof valueA === 'string' || typeof valueB === 'string')
|
|
2931
|
+
return valueA.toString().localeCompare(valueB.toString())
|
|
2932
|
+
return valueA - valueB
|
|
2933
|
+
}.bind(this)
|
|
2934
|
+
)
|
|
2935
|
+
return newRows
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
MOJFrontend.SortableTable.prototype.getCellValue = function (cell) {
|
|
2939
|
+
const val = cell.attr('data-sort-value') || cell.html()
|
|
2940
|
+
|
|
2941
|
+
const valAsNumber = Number(val)
|
|
2942
|
+
return isNaN(valAsNumber) ? val : valAsNumber
|
|
2943
|
+
}
|
|
2366
2944
|
|
|
2367
2945
|
return MOJFrontend;
|
|
2368
2946
|
}));
|