@ministryofjustice/frontend 3.3.1 → 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 +2021 -1436
- 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 +971 -923
- 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 +2 -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 +72 -79
- 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 +65 -70
- 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,113 +260,456 @@ MOJFrontend.initAll = function (options) {
|
|
|
113
260
|
search: {
|
|
114
261
|
container: $($searchToggle.querySelector('.moj-search'))
|
|
115
262
|
}
|
|
116
|
-
})
|
|
117
|
-
})
|
|
263
|
+
})
|
|
264
|
+
})
|
|
118
265
|
|
|
119
|
-
|
|
266
|
+
const $sortableTables = scope.querySelectorAll(
|
|
267
|
+
'[data-module="moj-sortable-table"]'
|
|
268
|
+
)
|
|
120
269
|
MOJFrontend.nodeListForEach($sortableTables, function ($table) {
|
|
121
270
|
new MOJFrontend.SortableTable({
|
|
122
271
|
table: $table
|
|
123
|
-
})
|
|
124
|
-
})
|
|
272
|
+
})
|
|
273
|
+
})
|
|
125
274
|
|
|
126
275
|
const $datepickers = scope.querySelectorAll('[data-module="moj-date-picker"]')
|
|
127
276
|
MOJFrontend.nodeListForEach($datepickers, function ($datepicker) {
|
|
128
|
-
new MOJFrontend.DatePicker($datepicker, {}).init()
|
|
277
|
+
new MOJFrontend.DatePicker($datepicker, {}).init()
|
|
129
278
|
})
|
|
130
279
|
|
|
131
280
|
const $buttonMenus = scope.querySelectorAll('[data-module="moj-button-menu"]')
|
|
132
281
|
MOJFrontend.nodeListForEach($buttonMenus, function ($buttonmenu) {
|
|
133
|
-
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
|
+
}
|
|
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
|
+
}
|
|
134
423
|
})
|
|
135
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
|
+
}
|
|
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
|
|
136
630
|
}
|
|
137
631
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
MOJFrontend.AddAnother.prototype.focusHeading = function() {
|
|
221
|
-
this.container.find('.moj-add-another__heading').focus();
|
|
222
|
-
};
|
|
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
|
+
}
|
|
223
713
|
|
|
224
714
|
/**
|
|
225
715
|
* @typedef {object} ButtonMenuConfig
|
|
@@ -231,157 +721,157 @@ MOJFrontend.AddAnother.prototype.focusHeading = function() {
|
|
|
231
721
|
/**
|
|
232
722
|
* @param {HTMLElement} $module
|
|
233
723
|
* @param {ButtonMenuConfig} config
|
|
234
|
-
* @
|
|
724
|
+
* @class
|
|
235
725
|
*/
|
|
236
726
|
MOJFrontend.ButtonMenu = function ($module, config = {}) {
|
|
237
727
|
if (!$module) {
|
|
238
|
-
return this
|
|
728
|
+
return this
|
|
239
729
|
}
|
|
240
730
|
|
|
241
731
|
const schema = Object.freeze({
|
|
242
732
|
properties: {
|
|
243
|
-
buttonText: { type:
|
|
244
|
-
buttonClasses: { type:
|
|
245
|
-
alignMenu: { type:
|
|
246
|
-
}
|
|
247
|
-
})
|
|
733
|
+
buttonText: { type: 'string' },
|
|
734
|
+
buttonClasses: { type: 'string' },
|
|
735
|
+
alignMenu: { type: 'string' }
|
|
736
|
+
}
|
|
737
|
+
})
|
|
248
738
|
|
|
249
739
|
const defaults = {
|
|
250
|
-
buttonText:
|
|
251
|
-
alignMenu:
|
|
252
|
-
buttonClasses:
|
|
253
|
-
}
|
|
740
|
+
buttonText: 'Actions',
|
|
741
|
+
alignMenu: 'left',
|
|
742
|
+
buttonClasses: ''
|
|
743
|
+
}
|
|
254
744
|
// data attributes override JS config, which overrides defaults
|
|
255
745
|
this.config = this.mergeConfigs(
|
|
256
746
|
defaults,
|
|
257
747
|
config,
|
|
258
|
-
this.parseDataset(schema, $module.dataset)
|
|
259
|
-
)
|
|
748
|
+
this.parseDataset(schema, $module.dataset)
|
|
749
|
+
)
|
|
260
750
|
|
|
261
|
-
this.$module = $module
|
|
262
|
-
}
|
|
751
|
+
this.$module = $module
|
|
752
|
+
}
|
|
263
753
|
|
|
264
754
|
MOJFrontend.ButtonMenu.prototype.init = function () {
|
|
265
755
|
// If only one button is provided, don't initiate a menu and toggle button
|
|
266
756
|
// if classes have been provided for the toggleButton, apply them to the single item
|
|
267
|
-
if (this.$module.children.length
|
|
268
|
-
const button = this.$module.children[0]
|
|
757
|
+
if (this.$module.children.length === 1) {
|
|
758
|
+
const button = this.$module.children[0]
|
|
269
759
|
button.classList.forEach((className) => {
|
|
270
|
-
if (className.startsWith(
|
|
271
|
-
button.classList.remove(className)
|
|
760
|
+
if (className.startsWith('govuk-button-')) {
|
|
761
|
+
button.classList.remove(className)
|
|
272
762
|
}
|
|
273
|
-
button.classList.remove(
|
|
274
|
-
})
|
|
763
|
+
button.classList.remove('moj-button-menu__item')
|
|
764
|
+
})
|
|
275
765
|
if (this.config.buttonClasses) {
|
|
276
|
-
button.classList.add(...this.config.buttonClasses.split(
|
|
766
|
+
button.classList.add(...this.config.buttonClasses.split(' '))
|
|
277
767
|
}
|
|
278
768
|
}
|
|
279
769
|
// Otherwise intialise a button menu
|
|
280
770
|
if (this.$module.children.length > 1) {
|
|
281
|
-
this.initMenu()
|
|
771
|
+
this.initMenu()
|
|
282
772
|
}
|
|
283
|
-
}
|
|
773
|
+
}
|
|
284
774
|
|
|
285
775
|
MOJFrontend.ButtonMenu.prototype.initMenu = function () {
|
|
286
|
-
this.$menu = this.createMenu()
|
|
287
|
-
this.$module.insertAdjacentHTML(
|
|
288
|
-
this.setupMenuItems()
|
|
776
|
+
this.$menu = this.createMenu()
|
|
777
|
+
this.$module.insertAdjacentHTML('afterbegin', this.toggleTemplate())
|
|
778
|
+
this.setupMenuItems()
|
|
289
779
|
|
|
290
|
-
this.$menuToggle = this.$module.querySelector(
|
|
291
|
-
this.items = this.$menu.querySelectorAll(
|
|
780
|
+
this.$menuToggle = this.$module.querySelector(':scope > button')
|
|
781
|
+
this.items = this.$menu.querySelectorAll('a, button')
|
|
292
782
|
|
|
293
|
-
this.$menuToggle.addEventListener(
|
|
294
|
-
this.toggleMenu(event)
|
|
295
|
-
})
|
|
783
|
+
this.$menuToggle.addEventListener('click', (event) => {
|
|
784
|
+
this.toggleMenu(event)
|
|
785
|
+
})
|
|
296
786
|
|
|
297
|
-
this.$module.addEventListener(
|
|
298
|
-
this.handleKeyDown(event)
|
|
299
|
-
})
|
|
787
|
+
this.$module.addEventListener('keydown', (event) => {
|
|
788
|
+
this.handleKeyDown(event)
|
|
789
|
+
})
|
|
300
790
|
|
|
301
|
-
document.addEventListener(
|
|
791
|
+
document.addEventListener('click', (event) => {
|
|
302
792
|
if (!this.$module.contains(event.target)) {
|
|
303
|
-
this.closeMenu(false)
|
|
793
|
+
this.closeMenu(false)
|
|
304
794
|
}
|
|
305
|
-
})
|
|
306
|
-
}
|
|
795
|
+
})
|
|
796
|
+
}
|
|
307
797
|
|
|
308
798
|
MOJFrontend.ButtonMenu.prototype.createMenu = function () {
|
|
309
|
-
const $menu = document.createElement(
|
|
310
|
-
$menu.setAttribute(
|
|
311
|
-
$menu.hidden = true
|
|
312
|
-
$menu.classList.add(
|
|
313
|
-
if (this.config.alignMenu
|
|
314
|
-
$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')
|
|
315
805
|
}
|
|
316
806
|
|
|
317
|
-
this.$module.appendChild($menu)
|
|
807
|
+
this.$module.appendChild($menu)
|
|
318
808
|
while (this.$module.firstChild !== $menu) {
|
|
319
|
-
$menu.appendChild(this.$module.firstChild)
|
|
809
|
+
$menu.appendChild(this.$module.firstChild)
|
|
320
810
|
}
|
|
321
811
|
|
|
322
|
-
return $menu
|
|
323
|
-
}
|
|
812
|
+
return $menu
|
|
813
|
+
}
|
|
324
814
|
|
|
325
815
|
MOJFrontend.ButtonMenu.prototype.setupMenuItems = function () {
|
|
326
816
|
Array.from(this.$menu.children).forEach((item) => {
|
|
327
817
|
// wrap item in li tag
|
|
328
|
-
const listItem = document.createElement(
|
|
329
|
-
this.$menu.insertBefore(listItem, item)
|
|
330
|
-
listItem.appendChild(item)
|
|
818
|
+
const listItem = document.createElement('li')
|
|
819
|
+
this.$menu.insertBefore(listItem, item)
|
|
820
|
+
listItem.appendChild(item)
|
|
331
821
|
|
|
332
|
-
item.setAttribute(
|
|
822
|
+
item.setAttribute('tabindex', -1)
|
|
333
823
|
|
|
334
|
-
if (item.tagName
|
|
335
|
-
item.setAttribute(
|
|
824
|
+
if (item.tagName === 'BUTTON') {
|
|
825
|
+
item.setAttribute('type', 'button')
|
|
336
826
|
}
|
|
337
827
|
|
|
338
828
|
item.classList.forEach((className) => {
|
|
339
|
-
if (className.startsWith(
|
|
340
|
-
item.classList.remove(className)
|
|
829
|
+
if (className.startsWith('govuk-button')) {
|
|
830
|
+
item.classList.remove(className)
|
|
341
831
|
}
|
|
342
|
-
})
|
|
832
|
+
})
|
|
343
833
|
|
|
344
834
|
// add a slight delay after click before closing the menu, makes it *feel* better
|
|
345
|
-
item.addEventListener(
|
|
835
|
+
item.addEventListener('click', (event) => {
|
|
346
836
|
setTimeout(() => {
|
|
347
|
-
this.closeMenu(false)
|
|
348
|
-
}, 50)
|
|
349
|
-
})
|
|
350
|
-
})
|
|
351
|
-
}
|
|
837
|
+
this.closeMenu(false)
|
|
838
|
+
}, 50)
|
|
839
|
+
})
|
|
840
|
+
})
|
|
841
|
+
}
|
|
352
842
|
|
|
353
843
|
MOJFrontend.ButtonMenu.prototype.toggleTemplate = function () {
|
|
354
844
|
return `
|
|
355
|
-
<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">
|
|
356
846
|
<span>
|
|
357
847
|
${this.config.buttonText}
|
|
358
848
|
<svg width="11" height="5" viewBox="0 0 11 5" xmlns="http://www.w3.org/2000/svg">
|
|
359
849
|
<path d="M5.5 0L11 5L0 5L5.5 0Z" fill="currentColor"/>
|
|
360
850
|
</svg>
|
|
361
851
|
</span>
|
|
362
|
-
</button
|
|
363
|
-
}
|
|
852
|
+
</button>`
|
|
853
|
+
}
|
|
364
854
|
|
|
365
855
|
/**
|
|
366
856
|
* @returns {boolean}
|
|
367
857
|
*/
|
|
368
858
|
MOJFrontend.ButtonMenu.prototype.isOpen = function () {
|
|
369
|
-
return this.$menuToggle.getAttribute(
|
|
370
|
-
}
|
|
859
|
+
return this.$menuToggle.getAttribute('aria-expanded') === 'true'
|
|
860
|
+
}
|
|
371
861
|
|
|
372
862
|
MOJFrontend.ButtonMenu.prototype.toggleMenu = function (event) {
|
|
373
|
-
event.preventDefault()
|
|
863
|
+
event.preventDefault()
|
|
374
864
|
|
|
375
865
|
// If menu is triggered with mouse don't move focus to first item
|
|
376
|
-
const keyboardEvent = event.detail
|
|
377
|
-
const focusIndex = keyboardEvent ? 0 : -1
|
|
866
|
+
const keyboardEvent = event.detail === 0
|
|
867
|
+
const focusIndex = keyboardEvent ? 0 : -1
|
|
378
868
|
|
|
379
869
|
if (this.isOpen()) {
|
|
380
|
-
this.closeMenu()
|
|
870
|
+
this.closeMenu()
|
|
381
871
|
} else {
|
|
382
|
-
this.openMenu(focusIndex)
|
|
872
|
+
this.openMenu(focusIndex)
|
|
383
873
|
}
|
|
384
|
-
}
|
|
874
|
+
}
|
|
385
875
|
|
|
386
876
|
/**
|
|
387
877
|
* Opens the menu and optionally sets the focus to the item with given index
|
|
@@ -389,12 +879,12 @@ MOJFrontend.ButtonMenu.prototype.toggleMenu = function (event) {
|
|
|
389
879
|
* @param {number} focusIndex - The index of the item to focus
|
|
390
880
|
*/
|
|
391
881
|
MOJFrontend.ButtonMenu.prototype.openMenu = function (focusIndex = 0) {
|
|
392
|
-
this.$menu.hidden = false
|
|
393
|
-
this.$menuToggle.setAttribute(
|
|
882
|
+
this.$menu.hidden = false
|
|
883
|
+
this.$menuToggle.setAttribute('aria-expanded', 'true')
|
|
394
884
|
if (focusIndex !== -1) {
|
|
395
|
-
this.focusItem(focusIndex)
|
|
885
|
+
this.focusItem(focusIndex)
|
|
396
886
|
}
|
|
397
|
-
}
|
|
887
|
+
}
|
|
398
888
|
|
|
399
889
|
/**
|
|
400
890
|
* Closes the menu and optionally returns focus back to menuToggle
|
|
@@ -402,12 +892,12 @@ MOJFrontend.ButtonMenu.prototype.openMenu = function (focusIndex = 0) {
|
|
|
402
892
|
* @param {boolean} moveFocus - whether to return focus to the toggle button
|
|
403
893
|
*/
|
|
404
894
|
MOJFrontend.ButtonMenu.prototype.closeMenu = function (moveFocus = true) {
|
|
405
|
-
this.$menu.hidden = true
|
|
406
|
-
this.$menuToggle.setAttribute(
|
|
895
|
+
this.$menu.hidden = true
|
|
896
|
+
this.$menuToggle.setAttribute('aria-expanded', 'false')
|
|
407
897
|
if (moveFocus) {
|
|
408
|
-
this.$menuToggle.focus()
|
|
898
|
+
this.$menuToggle.focus()
|
|
409
899
|
}
|
|
410
|
-
}
|
|
900
|
+
}
|
|
411
901
|
|
|
412
902
|
/**
|
|
413
903
|
* Focuses the menu item at the specified index
|
|
@@ -415,65 +905,68 @@ MOJFrontend.ButtonMenu.prototype.closeMenu = function (moveFocus = true) {
|
|
|
415
905
|
* @param {number} index - the index of the item to focus
|
|
416
906
|
*/
|
|
417
907
|
MOJFrontend.ButtonMenu.prototype.focusItem = function (index) {
|
|
418
|
-
if (index >= this.items.length) index = 0
|
|
419
|
-
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
|
|
420
910
|
|
|
421
|
-
this.items.item(index)
|
|
422
|
-
|
|
911
|
+
const menuItem = this.items.item(index)
|
|
912
|
+
if (menuItem) {
|
|
913
|
+
menuItem.focus()
|
|
914
|
+
}
|
|
915
|
+
}
|
|
423
916
|
|
|
424
917
|
MOJFrontend.ButtonMenu.prototype.currentFocusIndex = function () {
|
|
425
|
-
const activeElement = document.activeElement
|
|
426
|
-
const menuItems = Array.from(this.items)
|
|
918
|
+
const activeElement = document.activeElement
|
|
919
|
+
const menuItems = Array.from(this.items)
|
|
427
920
|
|
|
428
|
-
return menuItems.indexOf(activeElement)
|
|
429
|
-
}
|
|
921
|
+
return menuItems.indexOf(activeElement)
|
|
922
|
+
}
|
|
430
923
|
|
|
431
924
|
MOJFrontend.ButtonMenu.prototype.handleKeyDown = function (event) {
|
|
432
|
-
if (event.target
|
|
925
|
+
if (event.target === this.$menuToggle) {
|
|
433
926
|
switch (event.key) {
|
|
434
|
-
case
|
|
435
|
-
event.preventDefault()
|
|
436
|
-
this.openMenu()
|
|
437
|
-
break
|
|
438
|
-
case
|
|
439
|
-
event.preventDefault()
|
|
440
|
-
this.openMenu(this.items.length - 1)
|
|
441
|
-
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
|
|
442
935
|
}
|
|
443
936
|
}
|
|
444
937
|
|
|
445
938
|
if (this.$menu.contains(event.target) && this.isOpen()) {
|
|
446
939
|
switch (event.key) {
|
|
447
|
-
case
|
|
448
|
-
event.preventDefault()
|
|
940
|
+
case 'ArrowDown':
|
|
941
|
+
event.preventDefault()
|
|
449
942
|
if (this.currentFocusIndex() !== -1) {
|
|
450
|
-
this.focusItem(this.currentFocusIndex() + 1)
|
|
943
|
+
this.focusItem(this.currentFocusIndex() + 1)
|
|
451
944
|
}
|
|
452
|
-
break
|
|
453
|
-
case
|
|
454
|
-
event.preventDefault()
|
|
945
|
+
break
|
|
946
|
+
case 'ArrowUp':
|
|
947
|
+
event.preventDefault()
|
|
455
948
|
if (this.currentFocusIndex() !== -1) {
|
|
456
|
-
this.focusItem(this.currentFocusIndex() - 1)
|
|
949
|
+
this.focusItem(this.currentFocusIndex() - 1)
|
|
457
950
|
}
|
|
458
|
-
break
|
|
459
|
-
case
|
|
460
|
-
event.preventDefault()
|
|
461
|
-
this.focusItem(0)
|
|
462
|
-
break
|
|
463
|
-
case
|
|
464
|
-
event.preventDefault()
|
|
465
|
-
this.focusItem(this.items.length - 1)
|
|
466
|
-
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
|
|
467
960
|
}
|
|
468
961
|
}
|
|
469
962
|
|
|
470
|
-
if (event.key
|
|
471
|
-
this.closeMenu()
|
|
963
|
+
if (event.key === 'Escape' && this.isOpen()) {
|
|
964
|
+
this.closeMenu()
|
|
472
965
|
}
|
|
473
|
-
if (event.key
|
|
474
|
-
this.closeMenu(false)
|
|
966
|
+
if (event.key === 'Tab' && this.isOpen()) {
|
|
967
|
+
this.closeMenu(false)
|
|
475
968
|
}
|
|
476
|
-
}
|
|
969
|
+
}
|
|
477
970
|
|
|
478
971
|
/**
|
|
479
972
|
* Parse dataset
|
|
@@ -481,23 +974,23 @@ MOJFrontend.ButtonMenu.prototype.handleKeyDown = function (event) {
|
|
|
481
974
|
* Loop over an object and normalise each value using {@link normaliseString},
|
|
482
975
|
* optionally expanding nested `i18n.field`
|
|
483
976
|
*
|
|
484
|
-
* @param {
|
|
977
|
+
* @param {Schema} schema - component schema
|
|
485
978
|
* @param {DOMStringMap} dataset - HTML element dataset
|
|
486
|
-
* @returns {
|
|
979
|
+
* @returns {object} Normalised dataset
|
|
487
980
|
*/
|
|
488
981
|
MOJFrontend.ButtonMenu.prototype.parseDataset = function (schema, dataset) {
|
|
489
|
-
const parsed = {}
|
|
982
|
+
const parsed = {}
|
|
490
983
|
|
|
491
|
-
for (const [field,
|
|
984
|
+
for (const [field, ,] of Object.entries(schema.properties)) {
|
|
492
985
|
if (field in dataset) {
|
|
493
986
|
if (dataset[field]) {
|
|
494
|
-
parsed[field] = dataset[field]
|
|
987
|
+
parsed[field] = dataset[field]
|
|
495
988
|
}
|
|
496
989
|
}
|
|
497
990
|
}
|
|
498
991
|
|
|
499
|
-
return parsed
|
|
500
|
-
}
|
|
992
|
+
return parsed
|
|
993
|
+
}
|
|
501
994
|
|
|
502
995
|
/**
|
|
503
996
|
* Config merging function
|
|
@@ -509,28 +1002,28 @@ MOJFrontend.ButtonMenu.prototype.parseDataset = function (schema, dataset) {
|
|
|
509
1002
|
* @returns {{ [key: string]: unknown }} A merged config object
|
|
510
1003
|
*/
|
|
511
1004
|
MOJFrontend.ButtonMenu.prototype.mergeConfigs = function (...configObjects) {
|
|
512
|
-
const formattedConfigObject = {}
|
|
1005
|
+
const formattedConfigObject = {}
|
|
513
1006
|
|
|
514
1007
|
// Loop through each of the passed objects
|
|
515
1008
|
for (const configObject of configObjects) {
|
|
516
1009
|
for (const key of Object.keys(configObject)) {
|
|
517
|
-
const option = formattedConfigObject[key]
|
|
518
|
-
const override = configObject[key]
|
|
1010
|
+
const option = formattedConfigObject[key]
|
|
1011
|
+
const override = configObject[key]
|
|
519
1012
|
|
|
520
1013
|
// Push their keys one-by-one into formattedConfigObject. Any duplicate
|
|
521
1014
|
// keys with object values will be merged, otherwise the new value will
|
|
522
1015
|
// override the existing value.
|
|
523
|
-
if (typeof option ===
|
|
1016
|
+
if (typeof option === 'object' && typeof override === 'object') {
|
|
524
1017
|
// @ts-expect-error Index signature for type 'string' is missing
|
|
525
|
-
formattedConfigObject[key] = this.mergeConfigs(option, override)
|
|
1018
|
+
formattedConfigObject[key] = this.mergeConfigs(option, override)
|
|
526
1019
|
} else {
|
|
527
|
-
formattedConfigObject[key] = override
|
|
1020
|
+
formattedConfigObject[key] = override
|
|
528
1021
|
}
|
|
529
1022
|
}
|
|
530
1023
|
}
|
|
531
1024
|
|
|
532
|
-
return formattedConfigObject
|
|
533
|
-
}
|
|
1025
|
+
return formattedConfigObject
|
|
1026
|
+
}
|
|
534
1027
|
|
|
535
1028
|
/**
|
|
536
1029
|
* Schema for component config
|
|
@@ -550,234 +1043,232 @@ MOJFrontend.ButtonMenu.prototype.mergeConfigs = function (...configObjects) {
|
|
|
550
1043
|
* Datepicker config
|
|
551
1044
|
*
|
|
552
1045
|
* @typedef {object} DatepickerConfig
|
|
553
|
-
* @property {string}
|
|
554
|
-
* @property {string}
|
|
1046
|
+
* @property {string} [excludedDates] - Dates that cannot be selected
|
|
1047
|
+
* @property {string} [excludedDays] - Days that cannot be selected
|
|
555
1048
|
* @property {boolean} [leadingZeroes] - Whether to add leading zeroes when populating the field
|
|
556
|
-
* @property {string}
|
|
557
|
-
* @property {string}
|
|
558
|
-
* @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
|
|
559
1052
|
*/
|
|
560
1053
|
|
|
561
1054
|
/**
|
|
562
1055
|
* @param {HTMLElement} $module - HTML element
|
|
563
1056
|
* @param {DatepickerConfig} config - config object
|
|
564
|
-
* @
|
|
1057
|
+
* @class
|
|
565
1058
|
*/
|
|
566
1059
|
function Datepicker($module, config = {}) {
|
|
567
1060
|
if (!$module) {
|
|
568
|
-
return this
|
|
1061
|
+
return this
|
|
569
1062
|
}
|
|
570
1063
|
|
|
571
1064
|
const schema = Object.freeze({
|
|
572
1065
|
properties: {
|
|
573
|
-
excludedDates: { type:
|
|
574
|
-
excludedDays: { type:
|
|
575
|
-
leadingZeros: { type:
|
|
576
|
-
maxDate: { type:
|
|
577
|
-
minDate: { type:
|
|
578
|
-
weekStartDay: { type:
|
|
579
|
-
}
|
|
580
|
-
})
|
|
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
|
+
})
|
|
581
1074
|
|
|
582
1075
|
const defaults = {
|
|
583
1076
|
leadingZeros: false,
|
|
584
|
-
weekStartDay:
|
|
585
|
-
}
|
|
1077
|
+
weekStartDay: 'monday'
|
|
1078
|
+
}
|
|
586
1079
|
|
|
587
1080
|
// data attributes override JS config, which overrides defaults
|
|
588
1081
|
this.config = this.mergeConfigs(
|
|
589
1082
|
defaults,
|
|
590
1083
|
config,
|
|
591
|
-
this.parseDataset(schema, $module.dataset)
|
|
592
|
-
)
|
|
1084
|
+
this.parseDataset(schema, $module.dataset)
|
|
1085
|
+
)
|
|
593
1086
|
|
|
594
1087
|
this.dayLabels = [
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
]
|
|
1088
|
+
'Monday',
|
|
1089
|
+
'Tuesday',
|
|
1090
|
+
'Wednesday',
|
|
1091
|
+
'Thursday',
|
|
1092
|
+
'Friday',
|
|
1093
|
+
'Saturday',
|
|
1094
|
+
'Sunday'
|
|
1095
|
+
]
|
|
603
1096
|
|
|
604
1097
|
this.monthLabels = [
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
]
|
|
618
|
-
|
|
619
|
-
this.currentDate = new Date()
|
|
620
|
-
this.currentDate.setHours(0, 0, 0, 0)
|
|
621
|
-
this.calendarDays = []
|
|
622
|
-
this.excludedDates = []
|
|
623
|
-
this.excludedDays = []
|
|
624
|
-
|
|
625
|
-
this.buttonClass =
|
|
626
|
-
this.selectedDayButtonClass =
|
|
627
|
-
this.currentDayButtonClass =
|
|
628
|
-
this.todayButtonClass =
|
|
629
|
-
|
|
630
|
-
this.$module = $module
|
|
631
|
-
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')
|
|
632
1125
|
}
|
|
633
1126
|
|
|
634
1127
|
Datepicker.prototype.init = function () {
|
|
635
1128
|
// Check that required elements are present
|
|
636
1129
|
if (!this.$input) {
|
|
637
|
-
return
|
|
1130
|
+
return
|
|
638
1131
|
}
|
|
639
1132
|
if (this.$module.dataset.initialized) {
|
|
640
|
-
return
|
|
1133
|
+
return
|
|
641
1134
|
}
|
|
642
1135
|
|
|
643
|
-
this.setOptions()
|
|
644
|
-
this.initControls()
|
|
645
|
-
this.$module.setAttribute(
|
|
646
|
-
}
|
|
1136
|
+
this.setOptions()
|
|
1137
|
+
this.initControls()
|
|
1138
|
+
this.$module.setAttribute('data-initialized', 'true')
|
|
1139
|
+
}
|
|
647
1140
|
|
|
648
1141
|
Datepicker.prototype.initControls = function () {
|
|
649
|
-
this.id = `datepicker-${this.$input.id}
|
|
1142
|
+
this.id = `datepicker-${this.$input.id}`
|
|
650
1143
|
|
|
651
|
-
this.$dialog = this.createDialog()
|
|
652
|
-
this.createCalendarHeaders()
|
|
1144
|
+
this.$dialog = this.createDialog()
|
|
1145
|
+
this.createCalendarHeaders()
|
|
653
1146
|
|
|
654
|
-
const $componentWrapper = document.createElement(
|
|
655
|
-
const $inputWrapper = document.createElement(
|
|
656
|
-
$componentWrapper.classList.add(
|
|
657
|
-
$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')
|
|
658
1151
|
|
|
659
|
-
this.$input.parentNode.insertBefore($componentWrapper, this.$input)
|
|
660
|
-
$componentWrapper.appendChild($inputWrapper)
|
|
661
|
-
$inputWrapper.appendChild(this.$input)
|
|
1152
|
+
this.$input.parentNode.insertBefore($componentWrapper, this.$input)
|
|
1153
|
+
$componentWrapper.appendChild($inputWrapper)
|
|
1154
|
+
$inputWrapper.appendChild(this.$input)
|
|
662
1155
|
|
|
663
|
-
$inputWrapper.insertAdjacentHTML(
|
|
664
|
-
$componentWrapper.insertAdjacentElement(
|
|
1156
|
+
$inputWrapper.insertAdjacentHTML('beforeend', this.toggleTemplate())
|
|
1157
|
+
$componentWrapper.insertAdjacentElement('beforeend', this.$dialog)
|
|
665
1158
|
|
|
666
|
-
this.$calendarButton = this.$module.querySelector(
|
|
667
|
-
".moj-js-datepicker-toggle",
|
|
668
|
-
);
|
|
1159
|
+
this.$calendarButton = this.$module.querySelector('.moj-js-datepicker-toggle')
|
|
669
1160
|
this.$dialogTitle = this.$dialog.querySelector(
|
|
670
|
-
|
|
671
|
-
)
|
|
1161
|
+
'.moj-js-datepicker-month-year'
|
|
1162
|
+
)
|
|
672
1163
|
|
|
673
|
-
this.createCalendar()
|
|
1164
|
+
this.createCalendar()
|
|
674
1165
|
|
|
675
1166
|
this.$prevMonthButton = this.$dialog.querySelector(
|
|
676
|
-
|
|
677
|
-
)
|
|
1167
|
+
'.moj-js-datepicker-prev-month'
|
|
1168
|
+
)
|
|
678
1169
|
this.$prevYearButton = this.$dialog.querySelector(
|
|
679
|
-
|
|
680
|
-
)
|
|
1170
|
+
'.moj-js-datepicker-prev-year'
|
|
1171
|
+
)
|
|
681
1172
|
this.$nextMonthButton = this.$dialog.querySelector(
|
|
682
|
-
|
|
683
|
-
)
|
|
1173
|
+
'.moj-js-datepicker-next-month'
|
|
1174
|
+
)
|
|
684
1175
|
this.$nextYearButton = this.$dialog.querySelector(
|
|
685
|
-
|
|
686
|
-
)
|
|
687
|
-
this.$cancelButton = this.$dialog.querySelector(
|
|
688
|
-
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')
|
|
689
1180
|
|
|
690
1181
|
// add event listeners
|
|
691
|
-
this.$prevMonthButton.addEventListener(
|
|
692
|
-
this.focusPreviousMonth(event, false)
|
|
693
|
-
)
|
|
694
|
-
this.$prevYearButton.addEventListener(
|
|
695
|
-
this.focusPreviousYear(event, false)
|
|
696
|
-
)
|
|
697
|
-
this.$nextMonthButton.addEventListener(
|
|
698
|
-
this.focusNextMonth(event, false)
|
|
699
|
-
)
|
|
700
|
-
this.$nextYearButton.addEventListener(
|
|
701
|
-
this.focusNextYear(event, false)
|
|
702
|
-
)
|
|
703
|
-
this.$cancelButton.addEventListener(
|
|
704
|
-
event.preventDefault()
|
|
705
|
-
this.closeDialog(event)
|
|
706
|
-
})
|
|
707
|
-
this.$okButton.addEventListener(
|
|
708
|
-
this.selectDate(this.currentDate)
|
|
709
|
-
})
|
|
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
|
+
})
|
|
710
1201
|
|
|
711
1202
|
const dialogButtons = this.$dialog.querySelectorAll(
|
|
712
|
-
'button:not([disabled="true"])'
|
|
713
|
-
)
|
|
1203
|
+
'button:not([disabled="true"])'
|
|
1204
|
+
)
|
|
714
1205
|
// eslint-disable-next-line prefer-destructuring
|
|
715
|
-
this.$firstButtonInDialog = dialogButtons[0]
|
|
716
|
-
this.$lastButtonInDialog = dialogButtons[dialogButtons.length - 1]
|
|
717
|
-
this.$firstButtonInDialog.addEventListener(
|
|
718
|
-
this.firstButtonKeydown(event)
|
|
719
|
-
)
|
|
720
|
-
this.$lastButtonInDialog.addEventListener(
|
|
721
|
-
this.lastButtonKeydown(event)
|
|
722
|
-
)
|
|
723
|
-
|
|
724
|
-
this.$calendarButton.addEventListener(
|
|
725
|
-
this.toggleDialog(event)
|
|
726
|
-
)
|
|
727
|
-
|
|
728
|
-
this.$dialog.addEventListener(
|
|
729
|
-
if (event.key ===
|
|
730
|
-
this.closeDialog()
|
|
731
|
-
event.preventDefault()
|
|
732
|
-
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()
|
|
733
1224
|
}
|
|
734
|
-
})
|
|
1225
|
+
})
|
|
735
1226
|
|
|
736
|
-
document.body.addEventListener(
|
|
737
|
-
this.backgroundClick(event)
|
|
738
|
-
)
|
|
1227
|
+
document.body.addEventListener('mouseup', (event) =>
|
|
1228
|
+
this.backgroundClick(event)
|
|
1229
|
+
)
|
|
739
1230
|
|
|
740
1231
|
// populates calendar with initial dates, avoids Wave errors about null buttons
|
|
741
|
-
this.updateCalendar()
|
|
742
|
-
}
|
|
1232
|
+
this.updateCalendar()
|
|
1233
|
+
}
|
|
743
1234
|
|
|
744
1235
|
Datepicker.prototype.createDialog = function () {
|
|
745
|
-
const titleId = `datepicker-title-${this.$input.id}
|
|
746
|
-
const $dialog = document.createElement(
|
|
747
|
-
|
|
748
|
-
$dialog.id = this.id
|
|
749
|
-
$dialog.setAttribute(
|
|
750
|
-
$dialog.setAttribute(
|
|
751
|
-
$dialog.setAttribute(
|
|
752
|
-
$dialog.setAttribute(
|
|
753
|
-
$dialog.innerHTML = this.dialogTemplate(titleId)
|
|
754
|
-
$dialog.hidden = true
|
|
755
|
-
|
|
756
|
-
return $dialog
|
|
757
|
-
}
|
|
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
|
+
}
|
|
758
1249
|
|
|
759
1250
|
Datepicker.prototype.createCalendar = function () {
|
|
760
|
-
const $tbody = this.$dialog.querySelector(
|
|
761
|
-
let dayCount = 0
|
|
1251
|
+
const $tbody = this.$dialog.querySelector('tbody')
|
|
1252
|
+
let dayCount = 0
|
|
762
1253
|
for (let i = 0; i < 6; i++) {
|
|
763
1254
|
// create row
|
|
764
|
-
const $row = $tbody.insertRow(i)
|
|
1255
|
+
const $row = $tbody.insertRow(i)
|
|
765
1256
|
|
|
766
1257
|
for (let j = 0; j < 7; j++) {
|
|
767
1258
|
// create cell (day)
|
|
768
|
-
const $cell = document.createElement(
|
|
769
|
-
const $dateButton = document.createElement(
|
|
1259
|
+
const $cell = document.createElement('td')
|
|
1260
|
+
const $dateButton = document.createElement('button')
|
|
770
1261
|
|
|
771
|
-
$cell.appendChild($dateButton)
|
|
772
|
-
$row.appendChild($cell)
|
|
1262
|
+
$cell.appendChild($dateButton)
|
|
1263
|
+
$row.appendChild($cell)
|
|
773
1264
|
|
|
774
|
-
const calendarDay = new DSCalendarDay($dateButton, dayCount, i, j, this)
|
|
775
|
-
calendarDay.init()
|
|
776
|
-
this.calendarDays.push(calendarDay)
|
|
777
|
-
dayCount
|
|
1265
|
+
const calendarDay = new DSCalendarDay($dateButton, dayCount, i, j, this)
|
|
1266
|
+
calendarDay.init()
|
|
1267
|
+
this.calendarDays.push(calendarDay)
|
|
1268
|
+
dayCount++
|
|
778
1269
|
}
|
|
779
1270
|
}
|
|
780
|
-
}
|
|
1271
|
+
}
|
|
781
1272
|
|
|
782
1273
|
Datepicker.prototype.toggleTemplate = function () {
|
|
783
1274
|
return `<button class="moj-datepicker__toggle moj-js-datepicker-toggle" type="button" aria-haspopup="dialog" aria-controls="${this.id}" aria-expanded="false">
|
|
@@ -792,14 +1283,14 @@ Datepicker.prototype.toggleTemplate = function () {
|
|
|
792
1283
|
<rect x="3.66669" width="1.46667" height="5.13333" rx="0.733333" fill="currentColor"></rect>
|
|
793
1284
|
<rect x="16.8667" width="1.46667" height="5.13333" rx="0.733333" fill="currentColor"></rect>
|
|
794
1285
|
</svg>
|
|
795
|
-
</button
|
|
796
|
-
}
|
|
1286
|
+
</button>`
|
|
1287
|
+
}
|
|
797
1288
|
|
|
798
1289
|
/**
|
|
799
1290
|
* HTML template for calendar dialog
|
|
800
1291
|
*
|
|
801
1292
|
* @param {string} [titleId] - Id attribute for dialog title
|
|
802
|
-
* @
|
|
1293
|
+
* @returns {string}
|
|
803
1294
|
*/
|
|
804
1295
|
Datepicker.prototype.dialogTemplate = function (titleId) {
|
|
805
1296
|
return `<div class="moj-datepicker__dialog-header">
|
|
@@ -851,213 +1342,220 @@ Datepicker.prototype.dialogTemplate = function (titleId) {
|
|
|
851
1342
|
<div class="govuk-button-group">
|
|
852
1343
|
<button type="button" class="govuk-button moj-js-datepicker-ok">Select</button>
|
|
853
1344
|
<button type="button" class="govuk-button govuk-button--secondary moj-js-datepicker-cancel">Close</button>
|
|
854
|
-
</div
|
|
855
|
-
}
|
|
1345
|
+
</div>`
|
|
1346
|
+
}
|
|
856
1347
|
|
|
857
1348
|
Datepicker.prototype.createCalendarHeaders = function () {
|
|
858
1349
|
this.dayLabels.forEach((day) => {
|
|
859
|
-
const html = `<th scope="col"><span aria-hidden="true">${day.substring(0, 3)}</span><span class="govuk-visually-hidden">${day}</span></th
|
|
860
|
-
const $headerRow = this.$dialog.querySelector(
|
|
861
|
-
$headerRow.insertAdjacentHTML(
|
|
862
|
-
})
|
|
863
|
-
}
|
|
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
|
+
}
|
|
864
1355
|
|
|
865
1356
|
/**
|
|
866
1357
|
* Pads given number with leading zeros
|
|
867
1358
|
*
|
|
868
1359
|
* @param {number} value - The value to be padded
|
|
869
1360
|
* @param {number} length - The length in characters of the output
|
|
870
|
-
* @
|
|
1361
|
+
* @returns {string}
|
|
871
1362
|
*/
|
|
872
1363
|
Datepicker.prototype.leadingZeros = function (value, length = 2) {
|
|
873
|
-
let ret = value.toString()
|
|
1364
|
+
let ret = value.toString()
|
|
874
1365
|
|
|
875
1366
|
while (ret.length < length) {
|
|
876
|
-
ret = `0${ret}
|
|
1367
|
+
ret = `0${ret}`
|
|
877
1368
|
}
|
|
878
1369
|
|
|
879
|
-
return ret
|
|
880
|
-
}
|
|
1370
|
+
return ret
|
|
1371
|
+
}
|
|
881
1372
|
|
|
882
1373
|
Datepicker.prototype.setOptions = function () {
|
|
883
|
-
this.setMinAndMaxDatesOnCalendar()
|
|
884
|
-
this.setExcludedDates()
|
|
885
|
-
this.setExcludedDays()
|
|
886
|
-
this.setLeadingZeros()
|
|
887
|
-
this.setWeekStartDay()
|
|
888
|
-
}
|
|
1374
|
+
this.setMinAndMaxDatesOnCalendar()
|
|
1375
|
+
this.setExcludedDates()
|
|
1376
|
+
this.setExcludedDays()
|
|
1377
|
+
this.setLeadingZeros()
|
|
1378
|
+
this.setWeekStartDay()
|
|
1379
|
+
}
|
|
889
1380
|
|
|
890
1381
|
Datepicker.prototype.setMinAndMaxDatesOnCalendar = function () {
|
|
891
1382
|
if (this.config.minDate) {
|
|
892
|
-
this.minDate = this.formattedDateFromString(this.config.minDate, null)
|
|
1383
|
+
this.minDate = this.formattedDateFromString(this.config.minDate, null)
|
|
893
1384
|
if (this.minDate && this.currentDate < this.minDate) {
|
|
894
|
-
this.currentDate = this.minDate
|
|
1385
|
+
this.currentDate = this.minDate
|
|
895
1386
|
}
|
|
896
1387
|
}
|
|
897
1388
|
|
|
898
1389
|
if (this.config.maxDate) {
|
|
899
|
-
this.maxDate = this.formattedDateFromString(this.config.maxDate, null)
|
|
1390
|
+
this.maxDate = this.formattedDateFromString(this.config.maxDate, null)
|
|
900
1391
|
if (this.maxDate && this.currentDate > this.maxDate) {
|
|
901
|
-
this.currentDate = this.maxDate
|
|
1392
|
+
this.currentDate = this.maxDate
|
|
902
1393
|
}
|
|
903
1394
|
}
|
|
904
|
-
}
|
|
1395
|
+
}
|
|
905
1396
|
|
|
906
1397
|
Datepicker.prototype.setExcludedDates = function () {
|
|
907
1398
|
if (this.config.excludedDates) {
|
|
908
1399
|
this.excludedDates = this.config.excludedDates
|
|
909
|
-
.replace(/\s+/,
|
|
910
|
-
.split(
|
|
1400
|
+
.replace(/\s+/, ' ')
|
|
1401
|
+
.split(' ')
|
|
911
1402
|
.map((item) => {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
.split("-")
|
|
916
|
-
.map((d) => this.formattedDateFromString(d, null));
|
|
917
|
-
if (startDate && endDate) {
|
|
918
|
-
const date = new Date(startDate.getTime());
|
|
919
|
-
const dates = [];
|
|
920
|
-
while (date <= endDate) {
|
|
921
|
-
dates.push(new Date(date));
|
|
922
|
-
date.setDate(date.getDate() + 1);
|
|
923
|
-
}
|
|
924
|
-
return dates;
|
|
925
|
-
}
|
|
926
|
-
} else {
|
|
927
|
-
return this.formattedDateFromString(item, null);
|
|
928
|
-
}
|
|
1403
|
+
return item.includes('-')
|
|
1404
|
+
? this.parseDateRangeString(item)
|
|
1405
|
+
: this.formattedDateFromString(item)
|
|
929
1406
|
})
|
|
930
1407
|
.flat()
|
|
931
|
-
.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 */
|
|
932
1431
|
}
|
|
933
|
-
|
|
1432
|
+
return dates
|
|
1433
|
+
}
|
|
934
1434
|
|
|
935
1435
|
Datepicker.prototype.setExcludedDays = function () {
|
|
936
1436
|
if (this.config.excludedDays) {
|
|
937
1437
|
// lowercase and arrange dayLabels to put indexOf sunday == 0 for comparison
|
|
938
1438
|
// with getDay() function
|
|
939
|
-
|
|
940
|
-
if (this.config.weekStartDay ===
|
|
941
|
-
weekDays.unshift(weekDays.pop())
|
|
1439
|
+
const weekDays = this.dayLabels.map((item) => item.toLowerCase())
|
|
1440
|
+
if (this.config.weekStartDay === 'monday') {
|
|
1441
|
+
weekDays.unshift(weekDays.pop())
|
|
942
1442
|
}
|
|
943
1443
|
|
|
944
1444
|
this.excludedDays = this.config.excludedDays
|
|
945
|
-
.replace(/\s+/,
|
|
1445
|
+
.replace(/\s+/, ' ')
|
|
946
1446
|
.toLowerCase()
|
|
947
|
-
.split(
|
|
1447
|
+
.split(' ')
|
|
948
1448
|
.map((item) => weekDays.indexOf(item))
|
|
949
|
-
.filter((item) => item !== -1)
|
|
1449
|
+
.filter((item) => item !== -1)
|
|
950
1450
|
}
|
|
951
|
-
}
|
|
1451
|
+
}
|
|
952
1452
|
|
|
953
1453
|
Datepicker.prototype.setLeadingZeros = function () {
|
|
954
|
-
if (typeof this.config.leadingZeros !==
|
|
955
|
-
if (this.config.leadingZeros.toLowerCase() ===
|
|
956
|
-
this.config.leadingZeros = true
|
|
957
|
-
return
|
|
1454
|
+
if (typeof this.config.leadingZeros !== 'boolean') {
|
|
1455
|
+
if (this.config.leadingZeros.toLowerCase() === 'true') {
|
|
1456
|
+
this.config.leadingZeros = true
|
|
1457
|
+
return
|
|
958
1458
|
}
|
|
959
|
-
if (this.config.leadingZeros.toLowerCase() ===
|
|
960
|
-
this.config.leadingZeros = false
|
|
961
|
-
return;
|
|
1459
|
+
if (this.config.leadingZeros.toLowerCase() === 'false') {
|
|
1460
|
+
this.config.leadingZeros = false
|
|
962
1461
|
}
|
|
963
1462
|
}
|
|
964
|
-
}
|
|
1463
|
+
}
|
|
965
1464
|
|
|
966
1465
|
Datepicker.prototype.setWeekStartDay = function () {
|
|
967
|
-
const weekStartDayParam = this.config.weekStartDay
|
|
968
|
-
if (weekStartDayParam
|
|
969
|
-
this.config.weekStartDay =
|
|
1466
|
+
const weekStartDayParam = this.config.weekStartDay
|
|
1467
|
+
if (weekStartDayParam && weekStartDayParam.toLowerCase() === 'sunday') {
|
|
1468
|
+
this.config.weekStartDay = 'sunday'
|
|
970
1469
|
// Rotate dayLabels array to put Sunday as the first item
|
|
971
|
-
this.dayLabels.unshift(this.dayLabels.pop())
|
|
1470
|
+
this.dayLabels.unshift(this.dayLabels.pop())
|
|
972
1471
|
} else {
|
|
973
|
-
this.config.weekStartDay =
|
|
1472
|
+
this.config.weekStartDay = 'monday'
|
|
974
1473
|
}
|
|
975
|
-
}
|
|
1474
|
+
}
|
|
976
1475
|
|
|
977
1476
|
/**
|
|
978
1477
|
* Determine if a date is selecteable
|
|
979
1478
|
*
|
|
980
1479
|
* @param {Date} date - the date to check
|
|
981
|
-
* @
|
|
982
|
-
*
|
|
1480
|
+
* @returns {boolean}
|
|
983
1481
|
*/
|
|
984
1482
|
Datepicker.prototype.isExcludedDate = function (date) {
|
|
985
1483
|
// This comparison does not work correctly - it will exclude the mindate itself
|
|
986
1484
|
// see: https://github.com/ministryofjustice/moj-frontend/issues/923
|
|
987
1485
|
if (this.minDate && this.minDate > date) {
|
|
988
|
-
return true
|
|
1486
|
+
return true
|
|
989
1487
|
}
|
|
990
1488
|
|
|
991
1489
|
// This comparison works as expected - the maxdate will not be excluded
|
|
992
1490
|
if (this.maxDate && this.maxDate < date) {
|
|
993
|
-
return true
|
|
1491
|
+
return true
|
|
994
1492
|
}
|
|
995
1493
|
|
|
996
1494
|
for (const excludedDate of this.excludedDates) {
|
|
997
1495
|
if (date.toDateString() === excludedDate.toDateString()) {
|
|
998
|
-
return true
|
|
1496
|
+
return true
|
|
999
1497
|
}
|
|
1000
1498
|
}
|
|
1001
1499
|
|
|
1002
1500
|
if (this.excludedDays.includes(date.getDay())) {
|
|
1003
|
-
return true
|
|
1501
|
+
return true
|
|
1004
1502
|
}
|
|
1005
1503
|
|
|
1006
|
-
return false
|
|
1007
|
-
}
|
|
1504
|
+
return false
|
|
1505
|
+
}
|
|
1008
1506
|
|
|
1009
1507
|
/**
|
|
1010
1508
|
* Get a Date object from a string
|
|
1011
1509
|
*
|
|
1012
1510
|
* @param {string} dateString - string in the format d/m/yyyy dd/mm/yyyy
|
|
1013
1511
|
* @param {Date} fallback - date object to return if formatting fails
|
|
1014
|
-
* @
|
|
1512
|
+
* @returns {Date}
|
|
1015
1513
|
*/
|
|
1016
1514
|
Datepicker.prototype.formattedDateFromString = function (
|
|
1017
1515
|
dateString,
|
|
1018
|
-
fallback = new Date()
|
|
1516
|
+
fallback = new Date()
|
|
1019
1517
|
) {
|
|
1020
|
-
let formattedDate = null
|
|
1518
|
+
let formattedDate = null
|
|
1021
1519
|
// Accepts d/m/yyyy and dd/mm/yyyy
|
|
1022
|
-
const dateFormatPattern = /(\d{1,2})([-/,. ])(\d{1,2})\2(\d{4})
|
|
1520
|
+
const dateFormatPattern = /(\d{1,2})([-/,. ])(\d{1,2})\2(\d{4})/
|
|
1023
1521
|
|
|
1024
|
-
if (!dateFormatPattern.test(dateString)) return fallback
|
|
1522
|
+
if (!dateFormatPattern.test(dateString)) return fallback
|
|
1025
1523
|
|
|
1026
|
-
const match = dateString.match(dateFormatPattern)
|
|
1027
|
-
const day = match[1]
|
|
1028
|
-
const month = match[3]
|
|
1029
|
-
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]
|
|
1030
1528
|
|
|
1031
|
-
formattedDate = new Date(`${year}-${month}-${day}`)
|
|
1529
|
+
formattedDate = new Date(`${year}-${month}-${day}`)
|
|
1032
1530
|
if (formattedDate instanceof Date && !isNaN(formattedDate)) {
|
|
1033
|
-
return formattedDate
|
|
1531
|
+
return formattedDate
|
|
1034
1532
|
}
|
|
1035
|
-
return fallback
|
|
1036
|
-
}
|
|
1533
|
+
return fallback
|
|
1534
|
+
}
|
|
1037
1535
|
|
|
1038
1536
|
/**
|
|
1039
1537
|
* Get a formatted date string from a Date object
|
|
1040
1538
|
*
|
|
1041
1539
|
* @param {Date} date - date to format to a string
|
|
1042
|
-
* @
|
|
1540
|
+
* @returns {string}
|
|
1043
1541
|
*/
|
|
1044
1542
|
Datepicker.prototype.formattedDateFromDate = function (date) {
|
|
1045
1543
|
if (this.config.leadingZeros) {
|
|
1046
|
-
return `${this.leadingZeros(date.getDate())}/${this.leadingZeros(date.getMonth() + 1)}/${date.getFullYear()}
|
|
1047
|
-
} else {
|
|
1048
|
-
return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`;
|
|
1544
|
+
return `${this.leadingZeros(date.getDate())}/${this.leadingZeros(date.getMonth() + 1)}/${date.getFullYear()}`
|
|
1049
1545
|
}
|
|
1050
|
-
|
|
1546
|
+
|
|
1547
|
+
return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`
|
|
1548
|
+
}
|
|
1051
1549
|
|
|
1052
1550
|
/**
|
|
1053
1551
|
* Get a human readable date in the format Monday 2 March 2024
|
|
1054
1552
|
*
|
|
1055
|
-
* @param {Date} - date to format
|
|
1056
|
-
* @
|
|
1553
|
+
* @param {Date} date - date to format
|
|
1554
|
+
* @returns {string}
|
|
1057
1555
|
*/
|
|
1058
1556
|
Datepicker.prototype.formattedDateHuman = function (date) {
|
|
1059
|
-
return `${this.dayLabels[(date.getDay() + 6) % 7]} ${date.getDate()} ${this.monthLabels[date.getMonth()]} ${date.getFullYear()}
|
|
1060
|
-
}
|
|
1557
|
+
return `${this.dayLabels[(date.getDay() + 6) % 7]} ${date.getDate()} ${this.monthLabels[date.getMonth()]} ${date.getFullYear()}`
|
|
1558
|
+
}
|
|
1061
1559
|
|
|
1062
1560
|
Datepicker.prototype.backgroundClick = function (event) {
|
|
1063
1561
|
if (
|
|
@@ -1066,75 +1564,75 @@ Datepicker.prototype.backgroundClick = function (event) {
|
|
|
1066
1564
|
!this.$input.contains(event.target) &&
|
|
1067
1565
|
!this.$calendarButton.contains(event.target)
|
|
1068
1566
|
) {
|
|
1069
|
-
event.preventDefault()
|
|
1070
|
-
this.closeDialog()
|
|
1567
|
+
event.preventDefault()
|
|
1568
|
+
this.closeDialog()
|
|
1071
1569
|
}
|
|
1072
|
-
}
|
|
1570
|
+
}
|
|
1073
1571
|
|
|
1074
1572
|
Datepicker.prototype.firstButtonKeydown = function (event) {
|
|
1075
|
-
if (event.key ===
|
|
1076
|
-
this.$lastButtonInDialog.focus()
|
|
1077
|
-
event.preventDefault()
|
|
1573
|
+
if (event.key === 'Tab' && event.shiftKey) {
|
|
1574
|
+
this.$lastButtonInDialog.focus()
|
|
1575
|
+
event.preventDefault()
|
|
1078
1576
|
}
|
|
1079
|
-
}
|
|
1577
|
+
}
|
|
1080
1578
|
|
|
1081
1579
|
Datepicker.prototype.lastButtonKeydown = function (event) {
|
|
1082
|
-
if (event.key ===
|
|
1083
|
-
this.$firstButtonInDialog.focus()
|
|
1084
|
-
event.preventDefault()
|
|
1580
|
+
if (event.key === 'Tab' && !event.shiftKey) {
|
|
1581
|
+
this.$firstButtonInDialog.focus()
|
|
1582
|
+
event.preventDefault()
|
|
1085
1583
|
}
|
|
1086
|
-
}
|
|
1584
|
+
}
|
|
1087
1585
|
|
|
1088
1586
|
// render calendar
|
|
1089
1587
|
Datepicker.prototype.updateCalendar = function () {
|
|
1090
|
-
this.$dialogTitle.innerHTML = `${this.monthLabels[this.currentDate.getMonth()]} ${this.currentDate.getFullYear()}
|
|
1588
|
+
this.$dialogTitle.innerHTML = `${this.monthLabels[this.currentDate.getMonth()]} ${this.currentDate.getFullYear()}`
|
|
1091
1589
|
|
|
1092
|
-
const day = this.currentDate
|
|
1093
|
-
const firstOfMonth = new Date(day.getFullYear(), day.getMonth(), 1)
|
|
1094
|
-
let dayOfWeek
|
|
1590
|
+
const day = this.currentDate
|
|
1591
|
+
const firstOfMonth = new Date(day.getFullYear(), day.getMonth(), 1)
|
|
1592
|
+
let dayOfWeek
|
|
1095
1593
|
|
|
1096
|
-
if (this.config.weekStartDay ===
|
|
1097
|
-
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
|
|
1098
1596
|
} else {
|
|
1099
|
-
dayOfWeek = firstOfMonth.getDay()
|
|
1597
|
+
dayOfWeek = firstOfMonth.getDay()
|
|
1100
1598
|
}
|
|
1101
1599
|
|
|
1102
|
-
firstOfMonth.setDate(firstOfMonth.getDate() - dayOfWeek)
|
|
1600
|
+
firstOfMonth.setDate(firstOfMonth.getDate() - dayOfWeek)
|
|
1103
1601
|
|
|
1104
|
-
const thisDay = new Date(firstOfMonth)
|
|
1602
|
+
const thisDay = new Date(firstOfMonth)
|
|
1105
1603
|
|
|
1106
1604
|
// loop through our days
|
|
1107
1605
|
for (let i = 0; i < this.calendarDays.length; i++) {
|
|
1108
|
-
const hidden = thisDay.getMonth() !== day.getMonth()
|
|
1109
|
-
const disabled = this.isExcludedDate(thisDay)
|
|
1606
|
+
const hidden = thisDay.getMonth() !== day.getMonth()
|
|
1607
|
+
const disabled = this.isExcludedDate(thisDay)
|
|
1110
1608
|
|
|
1111
|
-
this.calendarDays[i].update(thisDay, hidden, disabled)
|
|
1609
|
+
this.calendarDays[i].update(thisDay, hidden, disabled)
|
|
1112
1610
|
|
|
1113
|
-
thisDay.setDate(thisDay.getDate() + 1)
|
|
1611
|
+
thisDay.setDate(thisDay.getDate() + 1)
|
|
1114
1612
|
}
|
|
1115
|
-
}
|
|
1613
|
+
}
|
|
1116
1614
|
|
|
1117
1615
|
Datepicker.prototype.setCurrentDate = function (focus = true) {
|
|
1118
|
-
const { currentDate } = this
|
|
1616
|
+
const { currentDate } = this
|
|
1119
1617
|
this.calendarDays.forEach((calendarDay) => {
|
|
1120
|
-
calendarDay.button.classList.add(
|
|
1121
|
-
calendarDay.button.classList.add(
|
|
1122
|
-
calendarDay.button.setAttribute(
|
|
1123
|
-
calendarDay.button.classList.remove(this.selectedDayButtonClass)
|
|
1124
|
-
const calendarDayDate = calendarDay.date
|
|
1125
|
-
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)
|
|
1126
1624
|
|
|
1127
|
-
const today = new Date()
|
|
1128
|
-
today.setHours(0, 0, 0, 0)
|
|
1625
|
+
const today = new Date()
|
|
1626
|
+
today.setHours(0, 0, 0, 0)
|
|
1129
1627
|
|
|
1130
1628
|
if (
|
|
1131
1629
|
calendarDayDate.getTime() ===
|
|
1132
1630
|
currentDate.getTime() /* && !calendarDay.button.disabled */
|
|
1133
1631
|
) {
|
|
1134
1632
|
if (focus) {
|
|
1135
|
-
calendarDay.button.setAttribute(
|
|
1136
|
-
calendarDay.button.focus()
|
|
1137
|
-
calendarDay.button.classList.add(this.selectedDayButtonClass)
|
|
1633
|
+
calendarDay.button.setAttribute('tabindex', 0)
|
|
1634
|
+
calendarDay.button.focus()
|
|
1635
|
+
calendarDay.button.classList.add(this.selectedDayButtonClass)
|
|
1138
1636
|
}
|
|
1139
1637
|
}
|
|
1140
1638
|
|
|
@@ -1142,213 +1640,210 @@ Datepicker.prototype.setCurrentDate = function (focus = true) {
|
|
|
1142
1640
|
this.inputDate &&
|
|
1143
1641
|
calendarDayDate.getTime() === this.inputDate.getTime()
|
|
1144
1642
|
) {
|
|
1145
|
-
calendarDay.button.classList.add(this.currentDayButtonClass)
|
|
1146
|
-
calendarDay.button.setAttribute(
|
|
1643
|
+
calendarDay.button.classList.add(this.currentDayButtonClass)
|
|
1644
|
+
calendarDay.button.setAttribute('aria-current', 'date')
|
|
1147
1645
|
} else {
|
|
1148
|
-
calendarDay.button.classList.remove(this.currentDayButtonClass)
|
|
1149
|
-
calendarDay.button.removeAttribute(
|
|
1646
|
+
calendarDay.button.classList.remove(this.currentDayButtonClass)
|
|
1647
|
+
calendarDay.button.removeAttribute('aria-current')
|
|
1150
1648
|
}
|
|
1151
1649
|
|
|
1152
1650
|
if (calendarDayDate.getTime() === today.getTime()) {
|
|
1153
|
-
calendarDay.button.classList.add(this.todayButtonClass)
|
|
1651
|
+
calendarDay.button.classList.add(this.todayButtonClass)
|
|
1154
1652
|
} else {
|
|
1155
|
-
calendarDay.button.classList.remove(this.todayButtonClass)
|
|
1653
|
+
calendarDay.button.classList.remove(this.todayButtonClass)
|
|
1156
1654
|
}
|
|
1157
|
-
})
|
|
1655
|
+
})
|
|
1158
1656
|
|
|
1159
1657
|
// if no date is tab-able, make the first non-disabled date tab-able
|
|
1160
1658
|
if (!focus) {
|
|
1161
1659
|
const enabledDays = this.calendarDays.filter((calendarDay) => {
|
|
1162
1660
|
return (
|
|
1163
|
-
window.getComputedStyle(calendarDay.button).display ===
|
|
1661
|
+
window.getComputedStyle(calendarDay.button).display === 'block' &&
|
|
1164
1662
|
!calendarDay.button.disabled
|
|
1165
|
-
)
|
|
1166
|
-
})
|
|
1663
|
+
)
|
|
1664
|
+
})
|
|
1167
1665
|
|
|
1168
|
-
enabledDays[0].button.setAttribute(
|
|
1666
|
+
enabledDays[0].button.setAttribute('tabindex', 0)
|
|
1169
1667
|
|
|
1170
|
-
this.currentDate = enabledDays[0].date
|
|
1668
|
+
this.currentDate = enabledDays[0].date
|
|
1171
1669
|
}
|
|
1172
|
-
}
|
|
1670
|
+
}
|
|
1173
1671
|
|
|
1174
1672
|
Datepicker.prototype.selectDate = function (date) {
|
|
1175
1673
|
if (this.isExcludedDate(date)) {
|
|
1176
|
-
return
|
|
1674
|
+
return
|
|
1177
1675
|
}
|
|
1178
1676
|
|
|
1179
|
-
this.$calendarButton.querySelector(
|
|
1180
|
-
`Choose date. Selected date is ${this.formattedDateHuman(date)}
|
|
1181
|
-
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)
|
|
1182
1680
|
|
|
1183
|
-
const changeEvent = new Event(
|
|
1184
|
-
this.$input.dispatchEvent(changeEvent)
|
|
1681
|
+
const changeEvent = new Event('change', { bubbles: true, cancelable: true })
|
|
1682
|
+
this.$input.dispatchEvent(changeEvent)
|
|
1185
1683
|
|
|
1186
|
-
this.closeDialog()
|
|
1187
|
-
}
|
|
1684
|
+
this.closeDialog()
|
|
1685
|
+
}
|
|
1188
1686
|
|
|
1189
1687
|
Datepicker.prototype.isOpen = function () {
|
|
1190
|
-
return this.$dialog.classList.contains(
|
|
1191
|
-
}
|
|
1688
|
+
return this.$dialog.classList.contains('moj-datepicker__dialog--open')
|
|
1689
|
+
}
|
|
1192
1690
|
|
|
1193
1691
|
Datepicker.prototype.toggleDialog = function (event) {
|
|
1194
|
-
event.preventDefault()
|
|
1692
|
+
event.preventDefault()
|
|
1195
1693
|
if (this.isOpen()) {
|
|
1196
|
-
this.closeDialog()
|
|
1694
|
+
this.closeDialog()
|
|
1197
1695
|
} else {
|
|
1198
|
-
this.setMinAndMaxDatesOnCalendar()
|
|
1199
|
-
this.openDialog()
|
|
1696
|
+
this.setMinAndMaxDatesOnCalendar()
|
|
1697
|
+
this.openDialog()
|
|
1200
1698
|
}
|
|
1201
|
-
}
|
|
1699
|
+
}
|
|
1202
1700
|
|
|
1203
1701
|
Datepicker.prototype.openDialog = function () {
|
|
1204
|
-
this.$dialog.hidden = false
|
|
1205
|
-
this.$dialog.classList.add(
|
|
1206
|
-
this.$calendarButton.setAttribute(
|
|
1702
|
+
this.$dialog.hidden = false
|
|
1703
|
+
this.$dialog.classList.add('moj-datepicker__dialog--open')
|
|
1704
|
+
this.$calendarButton.setAttribute('aria-expanded', 'true')
|
|
1207
1705
|
|
|
1208
1706
|
// position the dialog
|
|
1209
1707
|
// if input is wider than dialog pin it to the right
|
|
1210
1708
|
if (this.$input.offsetWidth > this.$dialog.offsetWidth) {
|
|
1211
|
-
this.$dialog.style.right = `0px
|
|
1709
|
+
this.$dialog.style.right = `0px`
|
|
1212
1710
|
}
|
|
1213
|
-
this.$dialog.style.top = `${this.$input.offsetHeight + 3}px
|
|
1711
|
+
this.$dialog.style.top = `${this.$input.offsetHeight + 3}px`
|
|
1214
1712
|
|
|
1215
1713
|
// get the date from the input element
|
|
1216
|
-
this.inputDate = this.formattedDateFromString(this.$input.value)
|
|
1217
|
-
this.currentDate = this.inputDate
|
|
1218
|
-
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)
|
|
1219
1717
|
|
|
1220
|
-
this.updateCalendar()
|
|
1221
|
-
this.setCurrentDate()
|
|
1222
|
-
}
|
|
1718
|
+
this.updateCalendar()
|
|
1719
|
+
this.setCurrentDate()
|
|
1720
|
+
}
|
|
1223
1721
|
|
|
1224
1722
|
Datepicker.prototype.closeDialog = function () {
|
|
1225
|
-
this.$dialog.hidden = true
|
|
1226
|
-
this.$dialog.classList.remove(
|
|
1227
|
-
this.$calendarButton.setAttribute(
|
|
1228
|
-
this.$calendarButton.focus()
|
|
1229
|
-
}
|
|
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
|
+
}
|
|
1230
1728
|
|
|
1231
1729
|
Datepicker.prototype.goToDate = function (date, focus) {
|
|
1232
|
-
const current = this.currentDate
|
|
1233
|
-
this.currentDate = date
|
|
1730
|
+
const current = this.currentDate
|
|
1731
|
+
this.currentDate = date
|
|
1234
1732
|
|
|
1235
1733
|
if (
|
|
1236
1734
|
current.getMonth() !== this.currentDate.getMonth() ||
|
|
1237
1735
|
current.getFullYear() !== this.currentDate.getFullYear()
|
|
1238
1736
|
) {
|
|
1239
|
-
this.updateCalendar()
|
|
1737
|
+
this.updateCalendar()
|
|
1240
1738
|
}
|
|
1241
1739
|
|
|
1242
|
-
this.setCurrentDate(focus)
|
|
1243
|
-
}
|
|
1740
|
+
this.setCurrentDate(focus)
|
|
1741
|
+
}
|
|
1244
1742
|
|
|
1245
1743
|
// day navigation
|
|
1246
1744
|
Datepicker.prototype.focusNextDay = function () {
|
|
1247
|
-
const date = new Date(this.currentDate)
|
|
1248
|
-
date.setDate(date.getDate() + 1)
|
|
1249
|
-
this.goToDate(date)
|
|
1250
|
-
}
|
|
1745
|
+
const date = new Date(this.currentDate)
|
|
1746
|
+
date.setDate(date.getDate() + 1)
|
|
1747
|
+
this.goToDate(date)
|
|
1748
|
+
}
|
|
1251
1749
|
|
|
1252
1750
|
Datepicker.prototype.focusPreviousDay = function () {
|
|
1253
|
-
const date = new Date(this.currentDate)
|
|
1254
|
-
date.setDate(date.getDate() - 1)
|
|
1255
|
-
this.goToDate(date)
|
|
1256
|
-
}
|
|
1751
|
+
const date = new Date(this.currentDate)
|
|
1752
|
+
date.setDate(date.getDate() - 1)
|
|
1753
|
+
this.goToDate(date)
|
|
1754
|
+
}
|
|
1257
1755
|
|
|
1258
1756
|
// week navigation
|
|
1259
1757
|
Datepicker.prototype.focusNextWeek = function () {
|
|
1260
|
-
const date = new Date(this.currentDate)
|
|
1261
|
-
date.setDate(date.getDate() + 7)
|
|
1262
|
-
this.goToDate(date)
|
|
1263
|
-
}
|
|
1758
|
+
const date = new Date(this.currentDate)
|
|
1759
|
+
date.setDate(date.getDate() + 7)
|
|
1760
|
+
this.goToDate(date)
|
|
1761
|
+
}
|
|
1264
1762
|
|
|
1265
1763
|
Datepicker.prototype.focusPreviousWeek = function () {
|
|
1266
|
-
const date = new Date(this.currentDate)
|
|
1267
|
-
date.setDate(date.getDate() - 7)
|
|
1268
|
-
this.goToDate(date)
|
|
1269
|
-
}
|
|
1764
|
+
const date = new Date(this.currentDate)
|
|
1765
|
+
date.setDate(date.getDate() - 7)
|
|
1766
|
+
this.goToDate(date)
|
|
1767
|
+
}
|
|
1270
1768
|
|
|
1271
1769
|
Datepicker.prototype.focusFirstDayOfWeek = function () {
|
|
1272
|
-
const date = new Date(this.currentDate)
|
|
1273
|
-
const firstDayOfWeekIndex = this.config.weekStartDay
|
|
1274
|
-
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()
|
|
1275
1773
|
const diff =
|
|
1276
1774
|
dayOfWeek >= firstDayOfWeekIndex
|
|
1277
1775
|
? dayOfWeek - firstDayOfWeekIndex
|
|
1278
|
-
: 6 - dayOfWeek
|
|
1776
|
+
: 6 - dayOfWeek
|
|
1279
1777
|
|
|
1280
|
-
date.setDate(date.getDate() - diff)
|
|
1281
|
-
date.setHours(0, 0, 0, 0)
|
|
1778
|
+
date.setDate(date.getDate() - diff)
|
|
1779
|
+
date.setHours(0, 0, 0, 0)
|
|
1282
1780
|
|
|
1283
|
-
this.goToDate(date)
|
|
1284
|
-
}
|
|
1781
|
+
this.goToDate(date)
|
|
1782
|
+
}
|
|
1285
1783
|
|
|
1286
1784
|
Datepicker.prototype.focusLastDayOfWeek = function () {
|
|
1287
|
-
const date = new Date(this.currentDate)
|
|
1288
|
-
const lastDayOfWeekIndex = this.config.weekStartDay
|
|
1289
|
-
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()
|
|
1290
1788
|
const diff =
|
|
1291
1789
|
dayOfWeek <= lastDayOfWeekIndex
|
|
1292
1790
|
? lastDayOfWeekIndex - dayOfWeek
|
|
1293
|
-
: 7 - dayOfWeek
|
|
1791
|
+
: 7 - dayOfWeek
|
|
1294
1792
|
|
|
1295
|
-
date.setDate(date.getDate() + diff)
|
|
1296
|
-
date.setHours(0, 0, 0, 0)
|
|
1793
|
+
date.setDate(date.getDate() + diff)
|
|
1794
|
+
date.setHours(0, 0, 0, 0)
|
|
1297
1795
|
|
|
1298
|
-
this.goToDate(date)
|
|
1299
|
-
}
|
|
1796
|
+
this.goToDate(date)
|
|
1797
|
+
}
|
|
1300
1798
|
|
|
1301
1799
|
// month navigation
|
|
1302
1800
|
Datepicker.prototype.focusNextMonth = function (event, focus = true) {
|
|
1303
|
-
event.preventDefault()
|
|
1304
|
-
const date = new Date(this.currentDate)
|
|
1305
|
-
date.setMonth(date.getMonth() + 1, 1)
|
|
1306
|
-
this.goToDate(date, focus)
|
|
1307
|
-
}
|
|
1801
|
+
event.preventDefault()
|
|
1802
|
+
const date = new Date(this.currentDate)
|
|
1803
|
+
date.setMonth(date.getMonth() + 1, 1)
|
|
1804
|
+
this.goToDate(date, focus)
|
|
1805
|
+
}
|
|
1308
1806
|
|
|
1309
1807
|
Datepicker.prototype.focusPreviousMonth = 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
|
-
}
|
|
1808
|
+
event.preventDefault()
|
|
1809
|
+
const date = new Date(this.currentDate)
|
|
1810
|
+
date.setMonth(date.getMonth() - 1, 1)
|
|
1811
|
+
this.goToDate(date, focus)
|
|
1812
|
+
}
|
|
1315
1813
|
|
|
1316
1814
|
// year navigation
|
|
1317
1815
|
Datepicker.prototype.focusNextYear = function (event, focus = true) {
|
|
1318
|
-
event.preventDefault()
|
|
1319
|
-
const date = new Date(this.currentDate)
|
|
1320
|
-
date.setFullYear(date.getFullYear() + 1, date.getMonth(), 1)
|
|
1321
|
-
this.goToDate(date, focus)
|
|
1322
|
-
}
|
|
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
|
+
}
|
|
1323
1821
|
|
|
1324
1822
|
Datepicker.prototype.focusPreviousYear = 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
|
-
}
|
|
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
|
+
}
|
|
1330
1828
|
|
|
1331
1829
|
/**
|
|
1332
1830
|
* Parse dataset
|
|
1333
1831
|
*
|
|
1334
|
-
*
|
|
1335
|
-
* optionally expanding nested `i18n.field`
|
|
1336
|
-
*
|
|
1337
|
-
* @param {{ schema: Schema }} Component - Component class
|
|
1832
|
+
* @param {Schema} schema - Component class
|
|
1338
1833
|
* @param {DOMStringMap} dataset - HTML element dataset
|
|
1339
|
-
* @returns {
|
|
1834
|
+
* @returns {object} Normalised dataset
|
|
1340
1835
|
*/
|
|
1341
1836
|
Datepicker.prototype.parseDataset = function (schema, dataset) {
|
|
1342
|
-
const parsed = {}
|
|
1837
|
+
const parsed = {}
|
|
1343
1838
|
|
|
1344
|
-
for (const [field,
|
|
1839
|
+
for (const [field, ,] of Object.entries(schema.properties)) {
|
|
1345
1840
|
if (field in dataset) {
|
|
1346
|
-
parsed[field] = dataset[field]
|
|
1841
|
+
parsed[field] = dataset[field]
|
|
1347
1842
|
}
|
|
1348
1843
|
}
|
|
1349
1844
|
|
|
1350
|
-
return parsed
|
|
1351
|
-
}
|
|
1845
|
+
return parsed
|
|
1846
|
+
}
|
|
1352
1847
|
|
|
1353
1848
|
/**
|
|
1354
1849
|
* Config merging function
|
|
@@ -1360,28 +1855,28 @@ Datepicker.prototype.parseDataset = function (schema, dataset) {
|
|
|
1360
1855
|
* @returns {{ [key: string]: unknown }} A merged config object
|
|
1361
1856
|
*/
|
|
1362
1857
|
Datepicker.prototype.mergeConfigs = function (...configObjects) {
|
|
1363
|
-
const formattedConfigObject = {}
|
|
1858
|
+
const formattedConfigObject = {}
|
|
1364
1859
|
|
|
1365
1860
|
// Loop through each of the passed objects
|
|
1366
1861
|
for (const configObject of configObjects) {
|
|
1367
1862
|
for (const key of Object.keys(configObject)) {
|
|
1368
|
-
const option = formattedConfigObject[key]
|
|
1369
|
-
const override = configObject[key]
|
|
1863
|
+
const option = formattedConfigObject[key]
|
|
1864
|
+
const override = configObject[key]
|
|
1370
1865
|
|
|
1371
1866
|
// Push their keys one-by-one into formattedConfigObject. Any duplicate
|
|
1372
1867
|
// keys with object values will be merged, otherwise the new value will
|
|
1373
1868
|
// override the existing value.
|
|
1374
|
-
if (typeof option ===
|
|
1869
|
+
if (typeof option === 'object' && typeof override === 'object') {
|
|
1375
1870
|
// @ts-expect-error Index signature for type 'string' is missing
|
|
1376
|
-
formattedConfigObject[key] = this.mergeConfigs(option, override)
|
|
1871
|
+
formattedConfigObject[key] = this.mergeConfigs(option, override)
|
|
1377
1872
|
} else {
|
|
1378
|
-
formattedConfigObject[key] = override
|
|
1873
|
+
formattedConfigObject[key] = override
|
|
1379
1874
|
}
|
|
1380
1875
|
}
|
|
1381
1876
|
}
|
|
1382
1877
|
|
|
1383
|
-
return formattedConfigObject
|
|
1384
|
-
}
|
|
1878
|
+
return formattedConfigObject
|
|
1879
|
+
}
|
|
1385
1880
|
|
|
1386
1881
|
/**
|
|
1387
1882
|
*
|
|
@@ -1390,22 +1885,22 @@ Datepicker.prototype.mergeConfigs = function (...configObjects) {
|
|
|
1390
1885
|
* @param {number} row
|
|
1391
1886
|
* @param {number} column
|
|
1392
1887
|
* @param {Datepicker} picker
|
|
1393
|
-
* @
|
|
1888
|
+
* @class
|
|
1394
1889
|
*/
|
|
1395
1890
|
function DSCalendarDay(button, index, row, column, picker) {
|
|
1396
|
-
this.index = index
|
|
1397
|
-
this.row = row
|
|
1398
|
-
this.column = column
|
|
1399
|
-
this.button = button
|
|
1400
|
-
this.picker = picker
|
|
1891
|
+
this.index = index
|
|
1892
|
+
this.row = row
|
|
1893
|
+
this.column = column
|
|
1894
|
+
this.button = button
|
|
1895
|
+
this.picker = picker
|
|
1401
1896
|
|
|
1402
|
-
this.date = new Date()
|
|
1897
|
+
this.date = new Date()
|
|
1403
1898
|
}
|
|
1404
1899
|
|
|
1405
1900
|
DSCalendarDay.prototype.init = function () {
|
|
1406
|
-
this.button.addEventListener(
|
|
1407
|
-
this.button.addEventListener(
|
|
1408
|
-
}
|
|
1901
|
+
this.button.addEventListener('keydown', this.keyPress.bind(this))
|
|
1902
|
+
this.button.addEventListener('click', this.click.bind(this))
|
|
1903
|
+
}
|
|
1409
1904
|
|
|
1410
1905
|
/**
|
|
1411
1906
|
* @param {Date} day - the Date for the calendar day
|
|
@@ -1413,84 +1908,84 @@ DSCalendarDay.prototype.init = function () {
|
|
|
1413
1908
|
* @param {boolean} disabled - is the day selectable or excluded
|
|
1414
1909
|
*/
|
|
1415
1910
|
DSCalendarDay.prototype.update = function (day, hidden, disabled) {
|
|
1416
|
-
|
|
1417
|
-
let accessibleLabel = this.picker.formattedDateHuman(day)
|
|
1911
|
+
const label = day.getDate()
|
|
1912
|
+
let accessibleLabel = this.picker.formattedDateHuman(day)
|
|
1418
1913
|
|
|
1419
1914
|
if (disabled) {
|
|
1420
|
-
this.button.setAttribute(
|
|
1421
|
-
accessibleLabel =
|
|
1915
|
+
this.button.setAttribute('aria-disabled', true)
|
|
1916
|
+
accessibleLabel = `Excluded date, ${accessibleLabel}`
|
|
1422
1917
|
} else {
|
|
1423
|
-
this.button.removeAttribute(
|
|
1918
|
+
this.button.removeAttribute('aria-disabled')
|
|
1424
1919
|
}
|
|
1425
1920
|
|
|
1426
1921
|
if (hidden) {
|
|
1427
|
-
this.button.style.display =
|
|
1922
|
+
this.button.style.display = 'none'
|
|
1428
1923
|
} else {
|
|
1429
|
-
this.button.style.display =
|
|
1924
|
+
this.button.style.display = 'block'
|
|
1430
1925
|
}
|
|
1431
1926
|
this.button.setAttribute(
|
|
1432
|
-
|
|
1433
|
-
this.picker.formattedDateFromDate(day)
|
|
1434
|
-
)
|
|
1927
|
+
'data-testid',
|
|
1928
|
+
this.picker.formattedDateFromDate(day)
|
|
1929
|
+
)
|
|
1435
1930
|
|
|
1436
|
-
this.button.innerHTML = `<span class="govuk-visually-hidden">${accessibleLabel}</span><span aria-hidden="true">${label}</span
|
|
1437
|
-
this.date = new Date(day)
|
|
1438
|
-
}
|
|
1931
|
+
this.button.innerHTML = `<span class="govuk-visually-hidden">${accessibleLabel}</span><span aria-hidden="true">${label}</span>`
|
|
1932
|
+
this.date = new Date(day)
|
|
1933
|
+
}
|
|
1439
1934
|
|
|
1440
1935
|
DSCalendarDay.prototype.click = function (event) {
|
|
1441
|
-
this.picker.goToDate(this.date)
|
|
1442
|
-
this.picker.selectDate(this.date)
|
|
1936
|
+
this.picker.goToDate(this.date)
|
|
1937
|
+
this.picker.selectDate(this.date)
|
|
1443
1938
|
|
|
1444
|
-
event.stopPropagation()
|
|
1445
|
-
event.preventDefault()
|
|
1446
|
-
}
|
|
1939
|
+
event.stopPropagation()
|
|
1940
|
+
event.preventDefault()
|
|
1941
|
+
}
|
|
1447
1942
|
|
|
1448
1943
|
DSCalendarDay.prototype.keyPress = function (event) {
|
|
1449
|
-
let calendarNavKey = true
|
|
1944
|
+
let calendarNavKey = true
|
|
1450
1945
|
|
|
1451
1946
|
switch (event.key) {
|
|
1452
|
-
case
|
|
1453
|
-
this.picker.focusPreviousDay()
|
|
1454
|
-
break
|
|
1455
|
-
case
|
|
1456
|
-
this.picker.focusNextDay()
|
|
1457
|
-
break
|
|
1458
|
-
case
|
|
1459
|
-
this.picker.focusPreviousWeek()
|
|
1460
|
-
break
|
|
1461
|
-
case
|
|
1462
|
-
this.picker.focusNextWeek()
|
|
1463
|
-
break
|
|
1464
|
-
case
|
|
1465
|
-
this.picker.focusFirstDayOfWeek()
|
|
1466
|
-
break
|
|
1467
|
-
case
|
|
1468
|
-
this.picker.focusLastDayOfWeek()
|
|
1469
|
-
break
|
|
1470
|
-
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':
|
|
1471
1966
|
// eslint-disable-next-line no-unused-expressions
|
|
1472
1967
|
event.shiftKey
|
|
1473
1968
|
? this.picker.focusPreviousYear(event)
|
|
1474
|
-
: this.picker.focusPreviousMonth(event)
|
|
1475
|
-
break
|
|
1476
|
-
case
|
|
1969
|
+
: this.picker.focusPreviousMonth(event)
|
|
1970
|
+
break
|
|
1971
|
+
case 'PageDown':
|
|
1477
1972
|
// eslint-disable-next-line no-unused-expressions
|
|
1478
1973
|
event.shiftKey
|
|
1479
1974
|
? this.picker.focusNextYear(event)
|
|
1480
|
-
: this.picker.focusNextMonth(event)
|
|
1481
|
-
break
|
|
1975
|
+
: this.picker.focusNextMonth(event)
|
|
1976
|
+
break
|
|
1482
1977
|
default:
|
|
1483
|
-
calendarNavKey = false
|
|
1484
|
-
break
|
|
1978
|
+
calendarNavKey = false
|
|
1979
|
+
break
|
|
1485
1980
|
}
|
|
1486
1981
|
|
|
1487
1982
|
if (calendarNavKey) {
|
|
1488
|
-
event.preventDefault()
|
|
1489
|
-
event.stopPropagation()
|
|
1983
|
+
event.preventDefault()
|
|
1984
|
+
event.stopPropagation()
|
|
1490
1985
|
}
|
|
1491
|
-
}
|
|
1986
|
+
}
|
|
1492
1987
|
|
|
1493
|
-
MOJFrontend.DatePicker = Datepicker
|
|
1988
|
+
MOJFrontend.DatePicker = Datepicker
|
|
1494
1989
|
|
|
1495
1990
|
/**
|
|
1496
1991
|
* Schema for component config
|
|
@@ -1506,102 +2001,107 @@ MOJFrontend.DatePicker = Datepicker;
|
|
|
1506
2001
|
* @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
|
|
1507
2002
|
*/
|
|
1508
2003
|
|
|
1509
|
-
MOJFrontend.FilterToggleButton = function(options) {
|
|
1510
|
-
this.options = options
|
|
1511
|
-
this.container = $(this.options.toggleButton.container)
|
|
1512
|
-
this.filterContainer = $(this.options.filter.container)
|
|
1513
|
-
|
|
1514
|
-
this.createToggleButton()
|
|
1515
|
-
this.setupResponsiveChecks()
|
|
1516
|
-
this.filterContainer.attr('tabindex', '-1')
|
|
1517
|
-
if(this.options.startHidden) {
|
|
1518
|
-
this.hideMenu()
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
MOJFrontend.FilterToggleButton.prototype.setupResponsiveChecks = function() {
|
|
1523
|
-
this.mq = window.matchMedia(this.options.bigModeMediaQuery)
|
|
1524
|
-
this.mq.addListener($.proxy(this, 'checkMode'))
|
|
1525
|
-
this.checkMode(this.mq)
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
MOJFrontend.FilterToggleButton.prototype.createToggleButton = function() {
|
|
1529
|
-
this.menuButton = $(
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
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()
|
|
1537
2034
|
} else {
|
|
1538
|
-
this.enableSmallMode()
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
MOJFrontend.FilterToggleButton.prototype.enableBigMode = function() {
|
|
1543
|
-
this.showMenu()
|
|
1544
|
-
this.removeCloseButton()
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
MOJFrontend.FilterToggleButton.prototype.enableSmallMode = function() {
|
|
1548
|
-
this.hideMenu()
|
|
1549
|
-
this.addCloseButton()
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
MOJFrontend.FilterToggleButton.prototype.addCloseButton = function() {
|
|
1553
|
-
if(this.options.closeButton) {
|
|
1554
|
-
this.closeButton = $(
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
this.menuButton.
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
this.menuButton.
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
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()
|
|
1592
2091
|
} else {
|
|
1593
|
-
this.hideMenu()
|
|
2092
|
+
this.hideMenu()
|
|
1594
2093
|
}
|
|
1595
|
-
}
|
|
2094
|
+
}
|
|
1596
2095
|
|
|
1597
|
-
MOJFrontend.FormValidator = function(form, options) {
|
|
1598
|
-
this.form = form
|
|
1599
|
-
this.errors = []
|
|
1600
|
-
this.validators = []
|
|
1601
|
-
$(this.form).on('submit', $.proxy(this, 'onSubmit'))
|
|
1602
|
-
this.summary =
|
|
1603
|
-
|
|
1604
|
-
|
|
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
|
+
}
|
|
1605
2105
|
|
|
1606
2106
|
MOJFrontend.FormValidator.entityMap = {
|
|
1607
2107
|
'&': '&',
|
|
@@ -1612,146 +2112,158 @@ MOJFrontend.FormValidator.entityMap = {
|
|
|
1612
2112
|
'/': '/',
|
|
1613
2113
|
'`': '`',
|
|
1614
2114
|
'=': '='
|
|
1615
|
-
}
|
|
2115
|
+
}
|
|
1616
2116
|
|
|
1617
|
-
MOJFrontend.FormValidator.prototype.escapeHtml = function(string) {
|
|
1618
|
-
return String(string).replace(/[&<>"'
|
|
1619
|
-
return MOJFrontend.FormValidator.entityMap[s]
|
|
1620
|
-
})
|
|
1621
|
-
}
|
|
2117
|
+
MOJFrontend.FormValidator.prototype.escapeHtml = function (string) {
|
|
2118
|
+
return String(string).replace(/[&<>"'`=/]/g, function fromEntityMap(s) {
|
|
2119
|
+
return MOJFrontend.FormValidator.entityMap[s]
|
|
2120
|
+
})
|
|
2121
|
+
}
|
|
1622
2122
|
|
|
1623
|
-
MOJFrontend.FormValidator.prototype.resetTitle = function() {
|
|
1624
|
-
document.title = this.originalTitle
|
|
1625
|
-
}
|
|
2123
|
+
MOJFrontend.FormValidator.prototype.resetTitle = function () {
|
|
2124
|
+
document.title = this.originalTitle
|
|
2125
|
+
}
|
|
1626
2126
|
|
|
1627
|
-
MOJFrontend.FormValidator.prototype.updateTitle = function() {
|
|
1628
|
-
document.title =
|
|
1629
|
-
}
|
|
2127
|
+
MOJFrontend.FormValidator.prototype.updateTitle = function () {
|
|
2128
|
+
document.title = `${this.errors.length} errors - ${document.title}`
|
|
2129
|
+
}
|
|
1630
2130
|
|
|
1631
2131
|
MOJFrontend.FormValidator.prototype.showSummary = function () {
|
|
1632
|
-
this.summary.html(this.getSummaryHtml())
|
|
1633
|
-
this.summary.removeClass('moj-hidden')
|
|
1634
|
-
this.summary.attr('aria-labelledby', 'errorSummary-heading')
|
|
1635
|
-
this.summary.focus()
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
MOJFrontend.FormValidator.prototype.getSummaryHtml = function() {
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
html += '<
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
html +=
|
|
1646
|
-
html +=
|
|
1647
|
-
html +=
|
|
1648
|
-
html += '</
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
html += '</
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
this.summary.
|
|
1658
|
-
|
|
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
|
+
}
|
|
1659
2160
|
|
|
1660
2161
|
MOJFrontend.FormValidator.prototype.onSubmit = function (e) {
|
|
1661
|
-
this.removeInlineErrors()
|
|
1662
|
-
this.hideSummary()
|
|
1663
|
-
this.resetTitle()
|
|
1664
|
-
if(!this.validate()) {
|
|
1665
|
-
e.preventDefault()
|
|
1666
|
-
this.updateTitle()
|
|
1667
|
-
this.showSummary()
|
|
1668
|
-
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()
|
|
1669
2170
|
}
|
|
1670
|
-
}
|
|
2171
|
+
}
|
|
1671
2172
|
|
|
1672
|
-
MOJFrontend.FormValidator.prototype.showInlineErrors = function() {
|
|
1673
|
-
for (
|
|
1674
|
-
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])
|
|
1675
2176
|
}
|
|
1676
|
-
}
|
|
2177
|
+
}
|
|
1677
2178
|
|
|
1678
2179
|
MOJFrontend.FormValidator.prototype.showInlineError = function (error) {
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
fieldContainer.
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
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)
|
|
1691
2194
|
} else {
|
|
1692
|
-
label.after(errorSpan)
|
|
1693
|
-
control.attr('aria-invalid', 'true')
|
|
1694
|
-
MOJFrontend.addAttributeValue(control[0], 'aria-describedby', errorSpanId)
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
MOJFrontend.FormValidator.prototype.removeInlineErrors = function() {
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
fieldContainer.find('
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
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) {
|
|
1717
2222
|
this.validators.push({
|
|
1718
|
-
fieldName
|
|
1719
|
-
rules
|
|
2223
|
+
fieldName,
|
|
2224
|
+
rules,
|
|
1720
2225
|
field: this.form.elements[fieldName]
|
|
1721
|
-
})
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
MOJFrontend.FormValidator.prototype.validate = function() {
|
|
1725
|
-
this.errors = []
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
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
|
|
1730
2235
|
for (i = 0; i < this.validators.length; i++) {
|
|
1731
|
-
validator = this.validators[i]
|
|
2236
|
+
validator = this.validators[i]
|
|
1732
2237
|
for (j = 0; j < validator.rules.length; j++) {
|
|
1733
|
-
validatorReturnValue = validator.rules[j].method(
|
|
1734
|
-
validator.
|
|
2238
|
+
validatorReturnValue = validator.rules[j].method(
|
|
2239
|
+
validator.field,
|
|
2240
|
+
validator.rules[j].params
|
|
2241
|
+
)
|
|
1735
2242
|
|
|
1736
2243
|
if (typeof validatorReturnValue === 'boolean' && !validatorReturnValue) {
|
|
1737
2244
|
this.errors.push({
|
|
1738
2245
|
fieldName: validator.fieldName,
|
|
1739
2246
|
message: validator.rules[j].message
|
|
1740
|
-
})
|
|
1741
|
-
break
|
|
1742
|
-
} else if(typeof validatorReturnValue === 'string') {
|
|
2247
|
+
})
|
|
2248
|
+
break
|
|
2249
|
+
} else if (typeof validatorReturnValue === 'string') {
|
|
1743
2250
|
this.errors.push({
|
|
1744
2251
|
fieldName: validatorReturnValue,
|
|
1745
2252
|
message: validator.rules[j].message
|
|
1746
|
-
})
|
|
1747
|
-
break
|
|
2253
|
+
})
|
|
2254
|
+
break
|
|
1748
2255
|
}
|
|
1749
2256
|
}
|
|
1750
2257
|
}
|
|
1751
|
-
return this.errors.length === 0
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
|
|
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) {
|
|
1755
2267
|
this.defaultParams = {
|
|
1756
2268
|
uploadFileEntryHook: $.noop,
|
|
1757
2269
|
uploadFileExitHook: $.noop,
|
|
@@ -1760,120 +2272,131 @@ if(MOJFrontend.dragAndDropSupported() && MOJFrontend.formDataSupported() && MOJF
|
|
|
1760
2272
|
uploadStatusText: 'Uploading files, please wait',
|
|
1761
2273
|
dropzoneHintText: 'Drag and drop files here or',
|
|
1762
2274
|
dropzoneButtonText: 'Choose files'
|
|
1763
|
-
};
|
|
1764
|
-
|
|
1765
|
-
this.params = $.extend({}, this.defaultParams, params);
|
|
1766
|
-
this.container = $(this.params.container);
|
|
1767
|
-
|
|
1768
|
-
this.container.addClass('moj-multi-file-upload--enhanced');
|
|
1769
|
-
|
|
1770
|
-
this.feedbackContainer = this.container.find('.moj-multi-file__uploaded-files');
|
|
1771
|
-
this.setupFileInput();
|
|
1772
|
-
this.setupDropzone();
|
|
1773
|
-
this.setupLabel();
|
|
1774
|
-
this.setupStatusBox();
|
|
1775
|
-
this.container.on('click', '.moj-multi-file-upload__delete', $.proxy(this, 'onFileDeleteClick'));
|
|
1776
|
-
};
|
|
1777
|
-
|
|
1778
|
-
MOJFrontend.MultiFileUpload.prototype.setupDropzone = function() {
|
|
1779
|
-
this.fileInput.wrap('<div class="moj-multi-file-upload__dropzone" />');
|
|
1780
|
-
this.dropzone = this.container.find('.moj-multi-file-upload__dropzone');
|
|
1781
|
-
this.dropzone.on('dragover', $.proxy(this, 'onDragOver'));
|
|
1782
|
-
this.dropzone.on('dragleave', $.proxy(this, 'onDragLeave'));
|
|
1783
|
-
this.dropzone.on('drop', $.proxy(this, 'onDrop'));
|
|
1784
|
-
};
|
|
1785
|
-
|
|
1786
|
-
MOJFrontend.MultiFileUpload.prototype.setupLabel = function() {
|
|
1787
|
-
this.label = $('<label for="'+this.fileInput[0].id+'" class="govuk-button govuk-button--secondary">'+ this.params.dropzoneButtonText +'</label>');
|
|
1788
|
-
this.dropzone.append('<p class="govuk-body">' + this.params.dropzoneHintText + '</p>');
|
|
1789
|
-
this.dropzone.append(this.label);
|
|
1790
|
-
};
|
|
1791
|
-
|
|
1792
|
-
MOJFrontend.MultiFileUpload.prototype.setupFileInput = function() {
|
|
1793
|
-
this.fileInput = this.container.find('.moj-multi-file-upload__input');
|
|
1794
|
-
this.fileInput.on('change', $.proxy(this, 'onFileChange'));
|
|
1795
|
-
this.fileInput.on('focus', $.proxy(this, 'onFileFocus'));
|
|
1796
|
-
this.fileInput.on('blur', $.proxy(this, 'onFileBlur'));
|
|
1797
|
-
};
|
|
1798
|
-
|
|
1799
|
-
MOJFrontend.MultiFileUpload.prototype.setupStatusBox = function() {
|
|
1800
|
-
this.status = $('<div aria-live="polite" role="status" class="govuk-visually-hidden" />');
|
|
1801
|
-
this.dropzone.append(this.status);
|
|
1802
|
-
};
|
|
1803
|
-
|
|
1804
|
-
MOJFrontend.MultiFileUpload.prototype.onDragOver = function(e) {
|
|
1805
|
-
e.preventDefault();
|
|
1806
|
-
this.dropzone.addClass('moj-multi-file-upload--dragover');
|
|
1807
|
-
};
|
|
1808
|
-
|
|
1809
|
-
MOJFrontend.MultiFileUpload.prototype.onDragLeave = function() {
|
|
1810
|
-
this.dropzone.removeClass('moj-multi-file-upload--dragover');
|
|
1811
|
-
};
|
|
1812
|
-
|
|
1813
|
-
MOJFrontend.MultiFileUpload.prototype.onDrop = function(e) {
|
|
1814
|
-
e.preventDefault();
|
|
1815
|
-
this.dropzone.removeClass('moj-multi-file-upload--dragover');
|
|
1816
|
-
this.feedbackContainer.removeClass('moj-hidden');
|
|
1817
|
-
this.status.html(this.params.uploadStatusText);
|
|
1818
|
-
this.uploadFiles(e.originalEvent.dataTransfer.files);
|
|
1819
|
-
};
|
|
1820
|
-
|
|
1821
|
-
MOJFrontend.MultiFileUpload.prototype.uploadFiles = function(files) {
|
|
1822
|
-
for(var i = 0; i < files.length; i++) {
|
|
1823
|
-
this.uploadFile(files[i]);
|
|
1824
2275
|
}
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
this.
|
|
1830
|
-
|
|
1831
|
-
this.
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
this.
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
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
|
-
|
|
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)
|
|
1877
2400
|
|
|
1878
2401
|
$.ajax({
|
|
1879
2402
|
url: this.params.uploadUrl,
|
|
@@ -1881,481 +2404,543 @@ if(MOJFrontend.dragAndDropSupported() && MOJFrontend.formDataSupported() && MOJF
|
|
|
1881
2404
|
data: formData,
|
|
1882
2405
|
processData: false,
|
|
1883
2406
|
contentType: false,
|
|
1884
|
-
success: $.proxy(function(response){
|
|
1885
|
-
if(response.error) {
|
|
1886
|
-
item
|
|
1887
|
-
|
|
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)
|
|
1888
2413
|
} else {
|
|
1889
|
-
item
|
|
1890
|
-
|
|
2414
|
+
item
|
|
2415
|
+
.find('.moj-multi-file-upload__message')
|
|
2416
|
+
.html(this.getSuccessHtml(response.success))
|
|
2417
|
+
this.status.html(response.success.messageText)
|
|
1891
2418
|
}
|
|
1892
|
-
item
|
|
1893
|
-
|
|
2419
|
+
item
|
|
2420
|
+
.find('.moj-multi-file-upload__actions')
|
|
2421
|
+
.append(this.getDeleteButtonHtml(response.file))
|
|
2422
|
+
this.params.uploadFileExitHook(this, file, response)
|
|
1894
2423
|
}, this),
|
|
1895
|
-
error: $.proxy(function(jqXHR, textStatus, errorThrown) {
|
|
1896
|
-
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
|
+
)
|
|
1897
2432
|
}, this),
|
|
1898
|
-
xhr: function() {
|
|
1899
|
-
|
|
1900
|
-
xhr.upload.addEventListener(
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
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
|
|
1908
2449
|
}
|
|
1909
|
-
})
|
|
1910
|
-
}
|
|
1911
|
-
|
|
1912
|
-
MOJFrontend.MultiFileUpload.prototype.onFileDeleteClick = function(e) {
|
|
1913
|
-
e.preventDefault()
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
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
|
|
1917
2458
|
$.ajax({
|
|
1918
2459
|
url: this.params.deleteUrl,
|
|
1919
2460
|
type: 'post',
|
|
1920
2461
|
dataType: 'json',
|
|
1921
|
-
data
|
|
1922
|
-
success: $.proxy(function(response){
|
|
1923
|
-
if(response.error) {
|
|
2462
|
+
data,
|
|
2463
|
+
success: $.proxy(function (response) {
|
|
2464
|
+
if (response.error) {
|
|
1924
2465
|
// handle error
|
|
1925
2466
|
} else {
|
|
1926
|
-
button.parents('.moj-multi-file-upload__row').remove()
|
|
1927
|
-
if(
|
|
1928
|
-
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')
|
|
1929
2473
|
}
|
|
1930
2474
|
}
|
|
1931
|
-
this.params.fileDeleteHook(this, response)
|
|
2475
|
+
this.params.fileDeleteHook(this, response)
|
|
1932
2476
|
}, this)
|
|
1933
|
-
})
|
|
1934
|
-
}
|
|
2477
|
+
})
|
|
2478
|
+
}
|
|
1935
2479
|
}
|
|
1936
2480
|
|
|
1937
|
-
MOJFrontend.MultiSelect = function(options) {
|
|
1938
|
-
this.container = $(options.container)
|
|
2481
|
+
MOJFrontend.MultiSelect = function (options) {
|
|
2482
|
+
this.container = $(options.container)
|
|
1939
2483
|
|
|
1940
2484
|
if (this.container.data('moj-multi-select-initialised')) {
|
|
1941
2485
|
return
|
|
1942
2486
|
}
|
|
1943
2487
|
|
|
1944
|
-
this.container.data('moj-multi-select-initialised', true)
|
|
2488
|
+
this.container.data('moj-multi-select-initialised', true)
|
|
1945
2489
|
|
|
1946
|
-
const idPrefix = options.id_prefix
|
|
1947
|
-
let allId = 'checkboxes-all'
|
|
2490
|
+
const idPrefix = options.id_prefix
|
|
2491
|
+
let allId = 'checkboxes-all'
|
|
1948
2492
|
if (typeof idPrefix !== 'undefined') {
|
|
1949
|
-
allId = idPrefix
|
|
2493
|
+
allId = `${idPrefix}checkboxes-all`
|
|
1950
2494
|
}
|
|
1951
2495
|
|
|
1952
|
-
this.toggle = $(this.getToggleHtml(allId))
|
|
1953
|
-
this.toggleButton = this.toggle.find('input')
|
|
1954
|
-
this.toggleButton.on('click', $.proxy(this, 'onButtonClick'))
|
|
1955
|
-
this.container.append(this.toggle)
|
|
1956
|
-
this.checkboxes = $(options.checkboxes)
|
|
1957
|
-
this.checkboxes.on('click', $.proxy(this, 'onCheckboxClick'))
|
|
1958
|
-
this.checked = options.checked || false
|
|
1959
|
-
}
|
|
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
|
+
}
|
|
1960
2504
|
|
|
1961
2505
|
MOJFrontend.MultiSelect.prototype.getToggleHtml = function (allId) {
|
|
1962
|
-
let html = ''
|
|
1963
|
-
html +=
|
|
1964
|
-
|
|
1965
|
-
html += ` <
|
|
1966
|
-
html +=
|
|
1967
|
-
html += '
|
|
1968
|
-
html += '</
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
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
|
|
1976
2521
|
} else {
|
|
1977
|
-
this.checkAll()
|
|
1978
|
-
this.toggleButton[0].checked = true
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
|
-
MOJFrontend.MultiSelect.prototype.checkAll = function() {
|
|
1983
|
-
this.checkboxes.each(
|
|
1984
|
-
el
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
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
|
|
2000
2549
|
} else {
|
|
2001
|
-
if(this.checkboxes.filter(':checked').length === this.checkboxes.length) {
|
|
2002
|
-
this.toggleButton[0].checked = true
|
|
2003
|
-
this.checked = true
|
|
2550
|
+
if (this.checkboxes.filter(':checked').length === this.checkboxes.length) {
|
|
2551
|
+
this.toggleButton[0].checked = true
|
|
2552
|
+
this.checked = true
|
|
2004
2553
|
}
|
|
2005
2554
|
}
|
|
2006
|
-
}
|
|
2555
|
+
}
|
|
2007
2556
|
|
|
2008
|
-
MOJFrontend.PasswordReveal = function(element) {
|
|
2009
|
-
this.el = element
|
|
2010
|
-
|
|
2557
|
+
MOJFrontend.PasswordReveal = function (element) {
|
|
2558
|
+
this.el = element
|
|
2559
|
+
const $el = $(this.el)
|
|
2011
2560
|
|
|
2012
2561
|
if ($el.data('moj-password-reveal-initialised')) {
|
|
2013
2562
|
return
|
|
2014
2563
|
}
|
|
2015
2564
|
|
|
2016
|
-
$el.data('moj-password-reveal-initialised', true)
|
|
2017
|
-
$el.attr('spellcheck', 'false')
|
|
2565
|
+
$el.data('moj-password-reveal-initialised', true)
|
|
2566
|
+
$el.attr('spellcheck', 'false')
|
|
2018
2567
|
|
|
2019
|
-
$el.wrap('<div class="moj-password-reveal"></div>')
|
|
2020
|
-
this.container = $(this.el).parent()
|
|
2021
|
-
this.createButton()
|
|
2022
|
-
}
|
|
2568
|
+
$el.wrap('<div class="moj-password-reveal"></div>')
|
|
2569
|
+
this.container = $(this.el).parent()
|
|
2570
|
+
this.createButton()
|
|
2571
|
+
}
|
|
2023
2572
|
|
|
2024
|
-
MOJFrontend.PasswordReveal.prototype.createButton = function() {
|
|
2025
|
-
this.button = $(
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
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
|
+
}
|
|
2029
2580
|
|
|
2030
|
-
MOJFrontend.PasswordReveal.prototype.onButtonClick = function() {
|
|
2581
|
+
MOJFrontend.PasswordReveal.prototype.onButtonClick = function () {
|
|
2031
2582
|
if (this.el.type === 'password') {
|
|
2032
|
-
this.el.type = 'text'
|
|
2033
|
-
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>')
|
|
2034
2585
|
} else {
|
|
2035
|
-
this.el.type = 'password'
|
|
2036
|
-
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>')
|
|
2037
2588
|
}
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2589
|
+
}
|
|
2040
2590
|
|
|
2041
|
-
if('contentEditable' in document.documentElement) {
|
|
2042
|
-
MOJFrontend.RichTextEditor = function(options) {
|
|
2043
|
-
this.options = options
|
|
2591
|
+
if ('contentEditable' in document.documentElement) {
|
|
2592
|
+
MOJFrontend.RichTextEditor = function (options) {
|
|
2593
|
+
this.options = options
|
|
2044
2594
|
this.options.toolbar = this.options.toolbar || {
|
|
2045
2595
|
bold: false,
|
|
2046
2596
|
italic: false,
|
|
2047
2597
|
underline: false,
|
|
2048
2598
|
bullets: true,
|
|
2049
2599
|
numbers: true
|
|
2050
|
-
}
|
|
2051
|
-
this.textarea = this.options.textarea
|
|
2052
|
-
this.container = $(this.textarea).parent()
|
|
2600
|
+
}
|
|
2601
|
+
this.textarea = this.options.textarea
|
|
2602
|
+
this.container = $(this.textarea).parent()
|
|
2053
2603
|
|
|
2054
2604
|
if (this.container.data('moj-rich-text-editor-initialised')) {
|
|
2055
2605
|
return
|
|
2056
2606
|
}
|
|
2057
2607
|
|
|
2058
|
-
this.container.data('moj-rich-text-editor-initialised', true)
|
|
2608
|
+
this.container.data('moj-rich-text-editor-initialised', true)
|
|
2059
2609
|
|
|
2060
|
-
this.createToolbar()
|
|
2061
|
-
this.hideDefault()
|
|
2062
|
-
this.configureToolbar()
|
|
2610
|
+
this.createToolbar()
|
|
2611
|
+
this.hideDefault()
|
|
2612
|
+
this.configureToolbar()
|
|
2063
2613
|
this.keys = {
|
|
2064
2614
|
left: 37,
|
|
2065
2615
|
right: 39,
|
|
2066
2616
|
up: 38,
|
|
2067
2617
|
down: 40
|
|
2068
|
-
}
|
|
2069
|
-
this.container.on(
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
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) {
|
|
2078
2634
|
case this.keys.right:
|
|
2079
|
-
case this.keys.down:
|
|
2080
|
-
focusableButton = this.toolbar.find('button[tabindex=0]')
|
|
2081
|
-
|
|
2082
|
-
if(nextButton[0]) {
|
|
2083
|
-
nextButton.focus()
|
|
2084
|
-
focusableButton.attr('tabindex', '-1')
|
|
2085
|
-
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')
|
|
2086
2642
|
}
|
|
2087
|
-
break
|
|
2643
|
+
break
|
|
2644
|
+
}
|
|
2088
2645
|
case this.keys.left:
|
|
2089
|
-
case this.keys.up:
|
|
2090
|
-
focusableButton = this.toolbar.find('button[tabindex=0]')
|
|
2091
|
-
|
|
2092
|
-
if(previousButton[0]) {
|
|
2093
|
-
previousButton.focus()
|
|
2094
|
-
focusableButton.attr('tabindex', '-1')
|
|
2095
|
-
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')
|
|
2096
2653
|
}
|
|
2097
|
-
break
|
|
2654
|
+
break
|
|
2655
|
+
}
|
|
2098
2656
|
}
|
|
2099
|
-
}
|
|
2657
|
+
}
|
|
2100
2658
|
|
|
2101
|
-
MOJFrontend.RichTextEditor.prototype.getToolbarHtml = function() {
|
|
2102
|
-
|
|
2659
|
+
MOJFrontend.RichTextEditor.prototype.getToolbarHtml = function () {
|
|
2660
|
+
let html = ''
|
|
2103
2661
|
|
|
2104
|
-
html += '<div class="moj-rich-text-editor__toolbar" role="toolbar">'
|
|
2662
|
+
html += '<div class="moj-rich-text-editor__toolbar" role="toolbar">'
|
|
2105
2663
|
|
|
2106
|
-
if(this.options.toolbar.bold) {
|
|
2107
|
-
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>'
|
|
2108
2667
|
}
|
|
2109
2668
|
|
|
2110
|
-
if(this.options.toolbar.italic) {
|
|
2111
|
-
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>'
|
|
2112
2672
|
}
|
|
2113
2673
|
|
|
2114
|
-
if(this.options.toolbar.underline) {
|
|
2115
|
-
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>'
|
|
2116
2677
|
}
|
|
2117
2678
|
|
|
2118
|
-
if(this.options.toolbar.bullets) {
|
|
2119
|
-
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>'
|
|
2120
2682
|
}
|
|
2121
2683
|
|
|
2122
|
-
if(this.options.toolbar.numbers) {
|
|
2123
|
-
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>'
|
|
2124
2687
|
}
|
|
2125
2688
|
|
|
2126
|
-
html += '</div>'
|
|
2127
|
-
return html
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
MOJFrontend.RichTextEditor.prototype.getEnhancedHtml = function(val) {
|
|
2131
|
-
return this.getToolbarHtml()
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
|
-
MOJFrontend.RichTextEditor.prototype.hideDefault = function() {
|
|
2135
|
-
this.textarea = this.container.find('textarea')
|
|
2136
|
-
this.textarea.addClass('govuk-visually-hidden')
|
|
2137
|
-
this.textarea.attr('aria-hidden', true)
|
|
2138
|
-
this.textarea.attr('tabindex', '-1')
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
MOJFrontend.RichTextEditor.prototype.createToolbar = function() {
|
|
2142
|
-
this.toolbar = document.createElement('div')
|
|
2143
|
-
this.toolbar.className = 'moj-rich-text-editor'
|
|
2144
|
-
this.toolbar.innerHTML = this.getEnhancedHtml()
|
|
2145
|
-
this.container.append(this.toolbar)
|
|
2146
|
-
this.toolbar = this.container.find('.moj-rich-text-editor__toolbar')
|
|
2147
|
-
this.container
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
e.preventDefault();
|
|
2176
|
-
this.container.find('.moj-rich-text-editor__content').focus();
|
|
2177
|
-
};
|
|
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
|
+
}
|
|
2178
2738
|
|
|
2739
|
+
MOJFrontend.RichTextEditor.prototype.onLabelClick = function (e) {
|
|
2740
|
+
e.preventDefault()
|
|
2741
|
+
this.container.find('.moj-rich-text-editor__content').focus()
|
|
2742
|
+
}
|
|
2179
2743
|
}
|
|
2180
2744
|
|
|
2181
2745
|
MOJFrontend.SearchToggle = function (options) {
|
|
2182
|
-
this.options = options
|
|
2183
|
-
this.container = $(this.options.search.container)
|
|
2184
|
-
this.toggleButtonContainer = $(this.options.toggleButton.container)
|
|
2746
|
+
this.options = options
|
|
2747
|
+
this.container = $(this.options.search.container)
|
|
2748
|
+
this.toggleButtonContainer = $(this.options.toggleButton.container)
|
|
2185
2749
|
|
|
2186
|
-
if (this.container.data(
|
|
2187
|
-
return
|
|
2750
|
+
if (this.container.data('moj-search-toggle-initialised')) {
|
|
2751
|
+
return
|
|
2188
2752
|
}
|
|
2189
2753
|
|
|
2190
|
-
this.container.data(
|
|
2754
|
+
this.container.data('moj-search-toggle-initialised', true)
|
|
2191
2755
|
|
|
2192
2756
|
const svg =
|
|
2193
|
-
'<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>'
|
|
2194
2758
|
|
|
2195
2759
|
this.toggleButton = $(
|
|
2196
|
-
|
|
2197
|
-
this.options.toggleButton.text
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
)
|
|
2201
|
-
this.
|
|
2202
|
-
this.
|
|
2203
|
-
$(document).on(
|
|
2204
|
-
|
|
2205
|
-
};
|
|
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
|
+
}
|
|
2206
2769
|
|
|
2207
2770
|
MOJFrontend.SearchToggle.prototype.showMenu = function () {
|
|
2208
|
-
this.toggleButton.attr(
|
|
2209
|
-
this.container.removeClass(
|
|
2210
|
-
this.container.find(
|
|
2211
|
-
}
|
|
2771
|
+
this.toggleButton.attr('aria-expanded', 'true')
|
|
2772
|
+
this.container.removeClass('moj-js-hidden')
|
|
2773
|
+
this.container.find('input').first().get(0).focus()
|
|
2774
|
+
}
|
|
2212
2775
|
|
|
2213
2776
|
MOJFrontend.SearchToggle.prototype.hideMenu = function () {
|
|
2214
|
-
this.container.addClass(
|
|
2215
|
-
this.toggleButton.attr(
|
|
2216
|
-
}
|
|
2777
|
+
this.container.addClass('moj-js-hidden')
|
|
2778
|
+
this.toggleButton.attr('aria-expanded', 'false')
|
|
2779
|
+
}
|
|
2217
2780
|
|
|
2218
2781
|
MOJFrontend.SearchToggle.prototype.onToggleButtonClick = function () {
|
|
2219
|
-
if (this.toggleButton.attr(
|
|
2220
|
-
this.showMenu()
|
|
2782
|
+
if (this.toggleButton.attr('aria-expanded') === 'false') {
|
|
2783
|
+
this.showMenu()
|
|
2221
2784
|
} else {
|
|
2222
|
-
this.hideMenu()
|
|
2785
|
+
this.hideMenu()
|
|
2223
2786
|
}
|
|
2224
|
-
}
|
|
2787
|
+
}
|
|
2225
2788
|
|
|
2226
2789
|
MOJFrontend.SearchToggle.prototype.onDocumentClick = function (e) {
|
|
2227
2790
|
if (
|
|
2228
2791
|
!$.contains(this.toggleButtonContainer[0], e.target) &&
|
|
2229
2792
|
!$.contains(this.container[0], e.target)
|
|
2230
2793
|
) {
|
|
2231
|
-
this.hideMenu()
|
|
2232
|
-
}
|
|
2233
|
-
}
|
|
2234
|
-
|
|
2235
|
-
MOJFrontend.SortableTable = function(params) {
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
this.initialiseSortedColumn()
|
|
2249
|
-
|
|
2250
|
-
}
|
|
2251
|
-
|
|
2252
|
-
MOJFrontend.SortableTable.prototype.setupOptions = function(params) {
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
|
-
MOJFrontend.SortableTable.prototype.createHeadingButtons = function() {
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
MOJFrontend.SortableTable.prototype.createHeadingButton = function(
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
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
|
+
}
|
|
2281
2849
|
|
|
2282
2850
|
MOJFrontend.SortableTable.prototype.initialiseSortedColumn = function () {
|
|
2283
|
-
|
|
2851
|
+
const rows = this.getTableRowsArray()
|
|
2284
2852
|
|
|
2285
|
-
this.table
|
|
2853
|
+
this.table
|
|
2854
|
+
.find('th')
|
|
2286
2855
|
.filter('[aria-sort="ascending"], [aria-sort="descending"]')
|
|
2287
2856
|
.first()
|
|
2288
2857
|
.each((index, el) => {
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
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)
|
|
2293
2862
|
})
|
|
2294
|
-
}
|
|
2295
|
-
|
|
2296
|
-
MOJFrontend.SortableTable.prototype.onSortButtonClick = function(e) {
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
}
|
|
2311
|
-
|
|
2312
|
-
MOJFrontend.SortableTable.prototype.updateButtonState = function(
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
2359
2944
|
|
|
2360
2945
|
return MOJFrontend;
|
|
2361
2946
|
}));
|