@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.
Files changed (87) hide show
  1. package/README.md +4 -10
  2. package/govuk-prototype-kit.config.json +5 -16
  3. package/moj/all.jquery.min.js +77 -3
  4. package/moj/all.js +2021 -1436
  5. package/moj/all.scss +2 -0
  6. package/moj/all.spec.js +15 -13
  7. package/moj/components/_all.scss +1 -0
  8. package/moj/components/action-bar/_action-bar.scss +4 -6
  9. package/moj/components/add-another/_add-another.scss +9 -7
  10. package/moj/components/add-another/add-another.js +90 -69
  11. package/moj/components/add-another/add-another.spec.js +165 -0
  12. package/moj/components/alert/README.md +0 -0
  13. package/moj/components/alert/_alert.scss +142 -0
  14. package/moj/components/alert/alert.js +247 -0
  15. package/moj/components/alert/alert.spec.helper.js +67 -0
  16. package/moj/components/alert/alert.spec.js +229 -0
  17. package/moj/components/alert/macro.njk +3 -0
  18. package/moj/components/alert/template.njk +83 -0
  19. package/moj/components/badge/_badge.scss +3 -4
  20. package/moj/components/banner/_banner.scss +5 -10
  21. package/moj/components/button-menu/_button-menu.scss +10 -9
  22. package/moj/components/button-menu/button-menu.js +139 -136
  23. package/moj/components/button-menu/button-menu.spec.js +295 -296
  24. package/moj/components/cookie-banner/_cookie-banner.scss +6 -5
  25. package/moj/components/currency-input/_currency-input.scss +4 -4
  26. package/moj/components/date-picker/README.md +14 -17
  27. package/moj/components/date-picker/_date-picker.scss +122 -106
  28. package/moj/components/date-picker/date-picker.js +473 -471
  29. package/moj/components/date-picker/date-picker.spec.js +971 -923
  30. package/moj/components/filter/README.md +1 -1
  31. package/moj/components/filter/_filter.scss +53 -75
  32. package/moj/components/filter-toggle-button/filter-toggle-button.js +71 -67
  33. package/moj/components/filter-toggle-button/filter-toggle-button.spec.js +203 -205
  34. package/moj/components/form-validator/form-validator.js +117 -109
  35. package/moj/components/header/_header.scss +17 -19
  36. package/moj/components/identity-bar/_identity-bar.scss +5 -5
  37. package/moj/components/interruption-card/_interruption-card.scss +2 -2
  38. package/moj/components/messages/_messages.scss +12 -19
  39. package/moj/components/multi-file-upload/README.md +1 -1
  40. package/moj/components/multi-file-upload/_multi-file-upload.scss +34 -30
  41. package/moj/components/multi-file-upload/multi-file-upload.js +188 -152
  42. package/moj/components/multi-file-upload/multi-file-upload.spec.js +510 -0
  43. package/moj/components/multi-select/_multi-select.scss +4 -3
  44. package/moj/components/multi-select/multi-select.js +55 -50
  45. package/moj/components/multi-select/multi-select.spec.js +72 -79
  46. package/moj/components/notification-badge/_notification-badge.scss +12 -12
  47. package/moj/components/organisation-switcher/_organisation-switcher.scss +1 -1
  48. package/moj/components/page-header-actions/_page-header-actions.scss +3 -2
  49. package/moj/components/pagination/_pagination.scss +26 -31
  50. package/moj/components/password-reveal/_password-reveal.scss +1 -2
  51. package/moj/components/password-reveal/password-reveal.js +22 -21
  52. package/moj/components/password-reveal/password-reveal.spec.js +39 -37
  53. package/moj/components/primary-navigation/_primary-navigation.scss +26 -29
  54. package/moj/components/progress-bar/_progress-bar.scss +21 -26
  55. package/moj/components/rich-text-editor/_rich-text-editor.scss +17 -16
  56. package/moj/components/rich-text-editor/rich-text-editor.js +117 -103
  57. package/moj/components/search/_search.scss +6 -4
  58. package/moj/components/search-toggle/search-toggle.js +29 -30
  59. package/moj/components/search-toggle/search-toggle.scss +21 -15
  60. package/moj/components/search-toggle/search-toggle.spec.js +65 -70
  61. package/moj/components/side-navigation/_side-navigation.scss +12 -21
  62. package/moj/components/sortable-table/_sortable-table.scss +25 -23
  63. package/moj/components/sortable-table/sortable-table.js +139 -117
  64. package/moj/components/sortable-table/sortable-table.spec.js +362 -0
  65. package/moj/components/sub-navigation/_sub-navigation.scss +24 -28
  66. package/moj/components/tag/_tag.scss +8 -9
  67. package/moj/components/task-list/_task-list.scss +8 -7
  68. package/moj/components/ticket-panel/_ticket-panel.scss +14 -6
  69. package/moj/components/timeline/_timeline.scss +18 -20
  70. package/moj/filters/all.js +28 -30
  71. package/moj/filters/prototype-kit-13-filters.js +2 -1
  72. package/moj/helpers/_all.scss +1 -0
  73. package/moj/helpers/_hidden.scss +1 -1
  74. package/moj/helpers/_links.scss +20 -0
  75. package/moj/helpers.js +160 -31
  76. package/moj/helpers.spec.js +235 -0
  77. package/moj/init.js +2 -2
  78. package/moj/moj-frontend.min.css +2 -2
  79. package/moj/moj-frontend.min.js +77 -3
  80. package/moj/namespace.js +2 -1
  81. package/moj/objects/_filter-layout.scss +11 -10
  82. package/moj/objects/_scrollable-pane.scss +11 -14
  83. package/moj/settings/_colours.scss +5 -0
  84. package/moj/settings/_measurements.scss +0 -2
  85. package/moj/utilities/_hidden.scss +3 -3
  86. package/moj/utilities/_width-container.scss +1 -1
  87. 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
- var MOJFrontend = {};
11
- MOJFrontend.removeAttributeValue = function(el, attr, value) {
12
- var re, m;
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) == value) {
15
- el.removeAttribute(attr);
16
+ if (el.getAttribute(attr) === value) {
17
+ el.removeAttribute(attr)
16
18
  } else {
17
- re = new RegExp('(^|\\s)' + value + '(\\s|$)');
18
- m = el.getAttribute(attr).match(re);
19
- if (m && m.length == 3) {
20
- el.setAttribute(attr, el.getAttribute(attr).replace(re, (m[1] && m[2])?' ':''))
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
- var re;
31
+ MOJFrontend.addAttributeValue = function (el, attr, value) {
32
+ let re
28
33
  if (!el.getAttribute(attr)) {
29
- el.setAttribute(attr, value);
30
- }
31
- else {
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) + ' ' + value);
38
+ el.setAttribute(attr, `${el.getAttribute(attr)} ${value}`)
35
39
  }
36
40
  }
37
- };
41
+ }
38
42
 
39
- MOJFrontend.dragAndDropSupported = function() {
40
- var div = document.createElement('div');
41
- return typeof div.ondrop != 'undefined';
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 == 'function';
46
- };
48
+ MOJFrontend.formDataSupported = function () {
49
+ return typeof FormData === 'function'
50
+ }
47
51
 
48
- MOJFrontend.fileApiSupported = function() {
49
- var input = document.createElement('input');
50
- input.type = 'file';
51
- return typeof input.files != 'undefined';
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 (var i = 0; i < nodes.length; i++) {
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
- var scope = typeof options.scope !== 'undefined' ? options.scope : document;
202
+ const scope = typeof options.scope !== 'undefined' ? options.scope : document
70
203
 
71
- var $addAnothers = scope.querySelectorAll('[data-module="moj-add-another"]');
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
- var $multiSelects = scope.querySelectorAll('[data-module="moj-multi-select"]');
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($multiSelect.getAttribute('data-multi-select-checkbox')),
80
- checkboxes: $multiSelect.querySelectorAll('tbody .govuk-checkboxes__input'),
81
- id_prefix: $multiSelect.getAttribute('data-multi-select-idprefix'),
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
- var $passwordReveals = scope.querySelectorAll('[data-module="moj-password-reveal"]');
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
- var $richTextEditors = scope.querySelectorAll('[data-module="moj-rich-text-editor"]');
231
+ const $richTextEditors = scope.querySelectorAll(
232
+ '[data-module="moj-rich-text-editor"]'
233
+ )
91
234
  MOJFrontend.nodeListForEach($richTextEditors, function ($richTextEditor) {
92
- var options = {
235
+ const options = {
93
236
  textarea: $($richTextEditor)
94
- };
237
+ }
95
238
 
96
- var toolbarAttr = $richTextEditor.getAttribute('data-moj-rich-text-editor-toolbar');
239
+ const toolbarAttr = $richTextEditor.getAttribute(
240
+ 'data-moj-rich-text-editor-toolbar'
241
+ )
97
242
  if (toolbarAttr) {
98
- var toolbar = toolbarAttr.split(',');
99
- options.toolbar = {};
100
- for (var item in toolbar) options.toolbar[toolbar[item]] = true;
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
- var $searchToggles = scope.querySelectorAll('[data-module="moj-search-toggle"]');
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
- var $sortableTables = scope.querySelectorAll('[data-module="moj-sortable-table"]');
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
- MOJFrontend.version = '3.3.1'
139
-
140
- MOJFrontend.AddAnother = function(container) {
141
- this.container = $(container);
142
-
143
- if (this.container.data('moj-add-another-initialised')) {
144
- return
145
- }
146
-
147
- this.container.data('moj-add-another-initialised', true);
148
-
149
- this.container.on('click', '.moj-add-another__remove-button', $.proxy(this, 'onRemoveButtonClick'));
150
- this.container.on('click', '.moj-add-another__add-button', $.proxy(this, 'onAddButtonClick'));
151
- this.container.find('.moj-add-another__add-button, moj-add-another__remove-button').prop('type', 'button');
152
- };
153
-
154
- MOJFrontend.AddAnother.prototype.onAddButtonClick = function(e) {
155
- var item = this.getNewItem();
156
- this.updateAttributes(this.getItems().length, item);
157
- this.resetItem(item);
158
- var firstItem = this.getItems().first();
159
- if(!this.hasRemoveButton(firstItem)) {
160
- this.createRemoveButton(firstItem);
161
- }
162
- this.getItems().last().after(item);
163
- item.find('input, textarea, select').first().focus();
164
- };
165
-
166
- MOJFrontend.AddAnother.prototype.hasRemoveButton = function(item) {
167
- return item.find('.moj-add-another__remove-button').length;
168
- };
169
-
170
- MOJFrontend.AddAnother.prototype.getItems = function() {
171
- return this.container.find('.moj-add-another__item');
172
- };
173
-
174
- MOJFrontend.AddAnother.prototype.getNewItem = function() {
175
- var item = this.getItems().first().clone();
176
- if(!this.hasRemoveButton(item)) {
177
- this.createRemoveButton(item);
178
- }
179
- return item;
180
- };
181
-
182
- MOJFrontend.AddAnother.prototype.updateAttributes = function(index, item) {
183
- item.find('[data-name]').each(function(i, el) {
184
- var originalId = el.id
185
-
186
- el.name = $(el).attr('data-name').replace(/%index%/, index);
187
- el.id = $(el).attr('data-id').replace(/%index%/, index);
188
-
189
- var label = $(el).siblings('label')[0] || $(el).parents('label')[0] || item.find('[for="' + originalId + '"]')[0];
190
- label.htmlFor = el.id;
191
- });
192
- };
193
-
194
- MOJFrontend.AddAnother.prototype.createRemoveButton = function(item) {
195
- item.append('<button type="button" class="govuk-button govuk-button--secondary moj-add-another__remove-button">Remove</button>');
196
- };
197
-
198
- MOJFrontend.AddAnother.prototype.resetItem = function(item) {
199
- item.find('[data-name], [data-id]').each(function(index, el) {
200
- if(el.type == 'checkbox' || el.type == 'radio') {
201
- el.checked = false;
202
- } else {
203
- el.value = '';
204
- }
205
- });
206
- };
207
-
208
- MOJFrontend.AddAnother.prototype.onRemoveButtonClick = function(e) {
209
- $(e.currentTarget).parents('.moj-add-another__item').remove();
210
- var items = this.getItems();
211
- if(items.length === 1) {
212
- items.find('.moj-add-another__remove-button').remove();
213
- }
214
- items.each($.proxy(function(index, el) {
215
- this.updateAttributes(index, $(el));
216
- }, this));
217
- this.focusHeading();
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&#39;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&#39;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
- * @constructor
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: "string" },
244
- buttonClasses: { type: "string" },
245
- alignMenu: { type: "string" },
246
- },
247
- });
733
+ buttonText: { type: 'string' },
734
+ buttonClasses: { type: 'string' },
735
+ alignMenu: { type: 'string' }
736
+ }
737
+ })
248
738
 
249
739
  const defaults = {
250
- buttonText: "Actions",
251
- alignMenu: "left",
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 == 1) {
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("govuk-button-")) {
271
- button.classList.remove(className);
760
+ if (className.startsWith('govuk-button-')) {
761
+ button.classList.remove(className)
272
762
  }
273
- button.classList.remove("moj-button-menu__item")
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("afterbegin", this.toggleTemplate());
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(":scope > button");
291
- this.items = this.$menu.querySelectorAll("a, button");
780
+ this.$menuToggle = this.$module.querySelector(':scope > button')
781
+ this.items = this.$menu.querySelectorAll('a, button')
292
782
 
293
- this.$menuToggle.addEventListener("click", (event) => {
294
- this.toggleMenu(event);
295
- });
783
+ this.$menuToggle.addEventListener('click', (event) => {
784
+ this.toggleMenu(event)
785
+ })
296
786
 
297
- this.$module.addEventListener("keydown", (event) => {
298
- this.handleKeyDown(event);
299
- });
787
+ this.$module.addEventListener('keydown', (event) => {
788
+ this.handleKeyDown(event)
789
+ })
300
790
 
301
- document.addEventListener("click", (event) => {
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("ul");
310
- $menu.setAttribute("role", "list");
311
- $menu.hidden = true;
312
- $menu.classList.add("moj-button-menu__wrapper");
313
- if (this.config.alignMenu == "right") {
314
- $menu.classList.add("moj-button-menu__wrapper--right");
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("li");
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("tabindex", -1);
822
+ item.setAttribute('tabindex', -1)
333
823
 
334
- if (item.tagName == "BUTTON") {
335
- item.setAttribute("type", "button");
824
+ if (item.tagName === 'BUTTON') {
825
+ item.setAttribute('type', 'button')
336
826
  }
337
827
 
338
828
  item.classList.forEach((className) => {
339
- if (className.startsWith("govuk-button")) {
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("click", (event) => {
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 || ""}" aria-haspopup="true" aria-expanded="false">
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("aria-expanded") === "true";
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 == 0;
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("aria-expanded", "true");
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("aria-expanded", "false");
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)?.focus();
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 == this.$menuToggle) {
925
+ if (event.target === this.$menuToggle) {
433
926
  switch (event.key) {
434
- case "ArrowDown":
435
- event.preventDefault();
436
- this.openMenu();
437
- break;
438
- case "ArrowUp":
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 "ArrowDown":
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 "ArrowUp":
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 "Home":
460
- event.preventDefault();
461
- this.focusItem(0);
462
- break;
463
- case "End":
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 == "Escape" && this.isOpen()) {
471
- this.closeMenu();
963
+ if (event.key === 'Escape' && this.isOpen()) {
964
+ this.closeMenu()
472
965
  }
473
- if (event.key == "Tab" && this.isOpen()) {
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 {{ schema: Schema }} Component - Component class
977
+ * @param {Schema} schema - component schema
485
978
  * @param {DOMStringMap} dataset - HTML element dataset
486
- * @returns {Object} Normalised dataset
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, attributes] of Object.entries(schema.properties)) {
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 === "object" && typeof override === "object") {
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} [excludedDates] - Dates that cannot be selected
554
- * @property {string} [excludedDays] - Days that cannot be selected
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} [minDate] - The earliest available date
557
- * @property {string} [maxDate] - The latest available date
558
- * @property {string} [weekStartDay] - First day of the week in calendar view
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
- * @constructor
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: "string" },
574
- excludedDays: { type: "string" },
575
- leadingZeros: { type: "string" },
576
- maxDate: { type: "string" },
577
- minDate: { type: "string" },
578
- weekStartDay: { type: "string" },
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: "monday",
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
- "Monday",
596
- "Tuesday",
597
- "Wednesday",
598
- "Thursday",
599
- "Friday",
600
- "Saturday",
601
- "Sunday",
602
- ];
1088
+ 'Monday',
1089
+ 'Tuesday',
1090
+ 'Wednesday',
1091
+ 'Thursday',
1092
+ 'Friday',
1093
+ 'Saturday',
1094
+ 'Sunday'
1095
+ ]
603
1096
 
604
1097
  this.monthLabels = [
605
- "January",
606
- "February",
607
- "March",
608
- "April",
609
- "May",
610
- "June",
611
- "July",
612
- "August",
613
- "September",
614
- "October",
615
- "November",
616
- "December",
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 = "moj-datepicker__button";
626
- this.selectedDayButtonClass = "moj-datepicker__button--selected";
627
- this.currentDayButtonClass = "moj-datepicker__button--current";
628
- this.todayButtonClass = "moj-datepicker__button--today";
629
-
630
- this.$module = $module;
631
- this.$input = $module.querySelector(".moj-js-datepicker-input");
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("data-initialized", "true");
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("div");
655
- const $inputWrapper = document.createElement("div");
656
- $componentWrapper.classList.add("moj-datepicker__wrapper");
657
- $inputWrapper.classList.add("govuk-input__wrapper");
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("beforeend", this.toggleTemplate());
664
- $componentWrapper.insertAdjacentElement("beforeend", this.$dialog);
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
- ".moj-js-datepicker-month-year",
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
- ".moj-js-datepicker-prev-month",
677
- );
1167
+ '.moj-js-datepicker-prev-month'
1168
+ )
678
1169
  this.$prevYearButton = this.$dialog.querySelector(
679
- ".moj-js-datepicker-prev-year",
680
- );
1170
+ '.moj-js-datepicker-prev-year'
1171
+ )
681
1172
  this.$nextMonthButton = this.$dialog.querySelector(
682
- ".moj-js-datepicker-next-month",
683
- );
1173
+ '.moj-js-datepicker-next-month'
1174
+ )
684
1175
  this.$nextYearButton = this.$dialog.querySelector(
685
- ".moj-js-datepicker-next-year",
686
- );
687
- this.$cancelButton = this.$dialog.querySelector(".moj-js-datepicker-cancel");
688
- this.$okButton = this.$dialog.querySelector(".moj-js-datepicker-ok");
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("click", (event) =>
692
- this.focusPreviousMonth(event, false),
693
- );
694
- this.$prevYearButton.addEventListener("click", (event) =>
695
- this.focusPreviousYear(event, false),
696
- );
697
- this.$nextMonthButton.addEventListener("click", (event) =>
698
- this.focusNextMonth(event, false),
699
- );
700
- this.$nextYearButton.addEventListener("click", (event) =>
701
- this.focusNextYear(event, false),
702
- );
703
- this.$cancelButton.addEventListener("click", (event) => {
704
- event.preventDefault();
705
- this.closeDialog(event);
706
- });
707
- this.$okButton.addEventListener("click", () => {
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("keydown", (event) =>
718
- this.firstButtonKeydown(event),
719
- );
720
- this.$lastButtonInDialog.addEventListener("keydown", (event) =>
721
- this.lastButtonKeydown(event),
722
- );
723
-
724
- this.$calendarButton.addEventListener("click", (event) =>
725
- this.toggleDialog(event),
726
- );
727
-
728
- this.$dialog.addEventListener("keydown", (event) => {
729
- if (event.key === "Escape") {
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("mouseup", (event) =>
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("div");
747
-
748
- $dialog.id = this.id;
749
- $dialog.setAttribute("class", "moj-datepicker__dialog");
750
- $dialog.setAttribute("role", "dialog");
751
- $dialog.setAttribute("aria-modal", "true");
752
- $dialog.setAttribute("aria-labelledby", titleId);
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("tbody");
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("td");
769
- const $dateButton = document.createElement("button");
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
- * @return {string}
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("thead > tr");
861
- $headerRow.insertAdjacentHTML("beforeend", html);
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
- * @return {string}
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
- if (item.includes("-")) {
913
- // parse the date range from the format "dd/mm/yyyy-dd/mm/yyyy"
914
- const [startDate, endDate] = item
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
- let weekDays = this.dayLabels.map((item) => item.toLowerCase());
940
- if (this.config.weekStartDay === "monday") {
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 !== "boolean") {
955
- if (this.config.leadingZeros.toLowerCase() === "true") {
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() === "false") {
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?.toLowerCase() === "sunday") {
969
- this.config.weekStartDay = "sunday";
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 = "monday";
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
- * @return {boolean}
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
- * @return {Date}
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
- * @return {string}
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
- * @return {string}
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 === "Tab" && event.shiftKey) {
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 === "Tab" && !event.shiftKey) {
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 === "monday") {
1097
- dayOfWeek = firstOfMonth.getDay() === 0 ? 6 : firstOfMonth.getDay() - 1; // Change logic to make Monday first day of week, i.e. 0
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("moj-datepicker__button");
1121
- calendarDay.button.classList.add("moj-datepicker__calendar-day");
1122
- calendarDay.button.setAttribute("tabindex", -1);
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("tabindex", 0);
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("aria-current", "date");
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("aria-current");
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 === "block" &&
1661
+ window.getComputedStyle(calendarDay.button).display === 'block' &&
1164
1662
  !calendarDay.button.disabled
1165
- );
1166
- });
1663
+ )
1664
+ })
1167
1665
 
1168
- enabledDays[0].button.setAttribute("tabindex", 0);
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("span").innerText =
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("change", { bubbles: true, cancelable: true });
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("moj-datepicker__dialog--open");
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("moj-datepicker__dialog--open");
1206
- this.$calendarButton.setAttribute("aria-expanded", "true");
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("moj-datepicker__dialog--open");
1227
- this.$calendarButton.setAttribute("aria-expanded", "false");
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 == "sunday" ? 0 : 1;
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 == "sunday" ? 6 : 0;
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
- * Loop over an object and normalise each value using {@link normaliseString},
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 {Object} Normalised dataset
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, attributes] of Object.entries(schema.properties)) {
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 === "object" && typeof override === "object") {
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
- * @constructor
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("keydown", this.keyPress.bind(this));
1407
- this.button.addEventListener("click", this.click.bind(this));
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
- let label = day.getDate();
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("aria-disabled", true);
1421
- accessibleLabel = "Excluded date, " + accessibleLabel;
1915
+ this.button.setAttribute('aria-disabled', true)
1916
+ accessibleLabel = `Excluded date, ${accessibleLabel}`
1422
1917
  } else {
1423
- this.button.removeAttribute("aria-disabled");
1918
+ this.button.removeAttribute('aria-disabled')
1424
1919
  }
1425
1920
 
1426
1921
  if (hidden) {
1427
- this.button.style.display = "none";
1922
+ this.button.style.display = 'none'
1428
1923
  } else {
1429
- this.button.style.display = "block";
1924
+ this.button.style.display = 'block'
1430
1925
  }
1431
1926
  this.button.setAttribute(
1432
- "data-testid",
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 "ArrowLeft":
1453
- this.picker.focusPreviousDay();
1454
- break;
1455
- case "ArrowRight":
1456
- this.picker.focusNextDay();
1457
- break;
1458
- case "ArrowUp":
1459
- this.picker.focusPreviousWeek();
1460
- break;
1461
- case "ArrowDown":
1462
- this.picker.focusNextWeek();
1463
- break;
1464
- case "Home":
1465
- this.picker.focusFirstDayOfWeek();
1466
- break;
1467
- case "End":
1468
- this.picker.focusLastDayOfWeek();
1469
- break;
1470
- case "PageUp":
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 "PageDown":
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 = $('<button class="govuk-button '+this.options.toggleButton.classes+'" type="button" aria-haspopup="true" aria-expanded="false">'+this.options.toggleButton.showText+'</button>');
1530
- this.menuButton.on('click', $.proxy(this, 'onMenuButtonClick'));
1531
- this.container.append(this.menuButton);
1532
- };
1533
-
1534
- MOJFrontend.FilterToggleButton.prototype.checkMode = function(mq) {
1535
- if(mq.matches) {
1536
- this.enableBigMode();
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 = $('<button class="moj-filter__close" type="button">'+this.options.closeButton.text+'</button>');
1555
- this.closeButton.on('click', $.proxy(this, 'onCloseClick'));
1556
- $(this.options.closeButton.container).append(this.closeButton);
1557
- }
1558
- };
1559
-
1560
- MOJFrontend.FilterToggleButton.prototype.onCloseClick = function() {
1561
- this.hideMenu();
1562
- this.menuButton.focus();
1563
- };
1564
-
1565
- MOJFrontend.FilterToggleButton.prototype.removeCloseButton = function() {
1566
- if(this.closeButton) {
1567
- this.closeButton.remove();
1568
- this.closeButton = null;
1569
- }
1570
- };
1571
-
1572
- MOJFrontend.FilterToggleButton.prototype.hideMenu = function() {
1573
- this.menuButton.attr('aria-expanded', 'false');
1574
- this.filterContainer.addClass('moj-js-hidden');
1575
- this.menuButton.text(this.options.toggleButton.showText);
1576
- };
1577
-
1578
- MOJFrontend.FilterToggleButton.prototype.showMenu = function() {
1579
- this.menuButton.attr('aria-expanded', 'true');
1580
- this.filterContainer.removeClass('moj-js-hidden');
1581
- this.menuButton.text(this.options.toggleButton.hideText);
1582
- };
1583
-
1584
- MOJFrontend.FilterToggleButton.prototype.onMenuButtonClick = function() {
1585
- this.toggle();
1586
- };
1587
-
1588
- MOJFrontend.FilterToggleButton.prototype.toggle = function() {
1589
- if(this.menuButton.attr('aria-expanded') == 'false') {
1590
- this.showMenu();
1591
- this.filterContainer.get(0).focus();
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 = (options && options.summary) ? $(options.summary) : $('.govuk-error-summary');
1603
- this.originalTitle = document.title;
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
  '&': '&amp;',
@@ -1612,146 +2112,158 @@ MOJFrontend.FormValidator.entityMap = {
1612
2112
  '/': '&#x2F;',
1613
2113
  '`': '&#x60;',
1614
2114
  '=': '&#x3D;'
1615
- };
2115
+ }
1616
2116
 
1617
- MOJFrontend.FormValidator.prototype.escapeHtml = function(string) {
1618
- return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) {
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 = "" + this.errors.length + " errors - " + 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
- var html = '<h2 id="error-summary-title" class="govuk-error-summary__title">There is a problem</h2>';
1640
- html += '<div class="govuk-error-summary__body">';
1641
- html += '<ul class="govuk-list govuk-error-summary__list">';
1642
- for (var i = 0, j = this.errors.length; i < j; i++) {
1643
- var error = this.errors[i];
1644
- html += '<li>';
1645
- html += '<a href="#' + this.escapeHtml(error.fieldName) + '">';
1646
- html += this.escapeHtml(error.message);
1647
- html += '</a>';
1648
- html += '</li>';
1649
- }
1650
- html += '</ul>';
1651
- html += '</div>';
1652
- return html;
1653
- };
1654
-
1655
- MOJFrontend.FormValidator.prototype.hideSummary = function() {
1656
- this.summary.addClass('moj-hidden');
1657
- this.summary.removeAttr('aria-labelledby');
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 (var i = 0, j = this.errors.length; i < j; i++) {
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
- var errorSpanId = error.fieldName + '-error';
1680
- var errorSpan = '<span class="govuk-error-message" id="'+ errorSpanId +'">'+this.escapeHtml(error.message)+'</span>';
1681
- var control = $("#" + error.fieldName);
1682
- var fieldContainer = control.parents(".govuk-form-group");
1683
- var label = fieldContainer.find('label');
1684
- var legend = fieldContainer.find("legend");
1685
- var fieldset = fieldContainer.find("fieldset");
1686
- fieldContainer.addClass('govuk-form-group--error');
1687
- if(legend.length) {
1688
- legend.after(errorSpan);
1689
- fieldContainer.attr('aria-invalid', 'true');
1690
- MOJFrontend.addAttributeValue(fieldset[0], 'aria-describedby', errorSpanId);
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
- var error;
1700
- var i;
1701
- for (var i = 0; i < this.errors.length; i++) {
1702
- this.removeInlineError(this.errors[i]);
1703
- }
1704
- };
1705
-
1706
- MOJFrontend.FormValidator.prototype.removeInlineError = function(error) {
1707
- var control = $("#" + error.fieldName);
1708
- var fieldContainer = control.parents(".govuk-form-group");
1709
- fieldContainer.find('.govuk-error-message').remove();
1710
- fieldContainer.removeClass('govuk-form-group--error');
1711
- fieldContainer.find("[aria-invalid]").attr('aria-invalid', 'false');
1712
- var errorSpanId = error.fieldName + '-error';
1713
- MOJFrontend.removeAttributeValue(fieldContainer.find('[aria-describedby]')[0], 'aria-describedby', errorSpanId);
1714
- };
1715
-
1716
- MOJFrontend.FormValidator.prototype.addValidator = function(fieldName, rules) {
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: fieldName,
1719
- rules: 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
- var validator = null,
1727
- validatorReturnValue = true,
1728
- i,
1729
- j;
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(validator.field,
1734
- validator.rules[j].params);
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
- if(MOJFrontend.dragAndDropSupported() && MOJFrontend.formDataSupported() && MOJFrontend.fileApiSupported()) {
1754
- MOJFrontend.MultiFileUpload = function(params) {
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
- MOJFrontend.MultiFileUpload.prototype.onFileChange = function(e) {
1828
- this.feedbackContainer.removeClass('moj-hidden');
1829
- this.status.html(this.params.uploadStatusText);
1830
- this.uploadFiles(e.currentTarget.files);
1831
- this.fileInput.replaceWith($(e.currentTarget).val('').clone(true));
1832
- this.setupFileInput();
1833
- this.fileInput.focus();
1834
- };
1835
-
1836
- MOJFrontend.MultiFileUpload.prototype.onFileFocus = function(e) {
1837
- this.label.addClass('moj-multi-file-upload--focused');
1838
- };
1839
-
1840
- MOJFrontend.MultiFileUpload.prototype.onFileBlur = function(e) {
1841
- this.label.removeClass('moj-multi-file-upload--focused');
1842
- };
1843
-
1844
- MOJFrontend.MultiFileUpload.prototype.getSuccessHtml = function(success) {
1845
- 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>';
1846
- };
1847
-
1848
- MOJFrontend.MultiFileUpload.prototype.getErrorHtml = function(error) {
1849
- 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>';
1850
- };
1851
-
1852
- MOJFrontend.MultiFileUpload.prototype.getFileRowHtml = function(file) {
1853
- var html = '';
1854
- html += '<div class="govuk-summary-list__row moj-multi-file-upload__row">';
1855
- html += ' <div class="govuk-summary-list__value moj-multi-file-upload__message">';
1856
- html += '<span class="moj-multi-file-upload__filename">'+file.name+'</span>';
1857
- html += '<span class="moj-multi-file-upload__progress">0%</span>';
1858
- html += ' </div>';
1859
- html += ' <div class="govuk-summary-list__actions moj-multi-file-upload__actions"></div>';
1860
- html += '</div>';
1861
- return html;
1862
- };
1863
-
1864
- MOJFrontend.MultiFileUpload.prototype.getDeleteButtonHtml = function(file) {
1865
- var html = '<button class="moj-multi-file-upload__delete govuk-button govuk-button--secondary govuk-!-margin-bottom-0" type="button" name="delete" value="' + file.filename + '">';
1866
- html += 'Delete <span class="govuk-visually-hidden">' + file.originalname + '</span>';
1867
- html += '</button>';
1868
- return html;
1869
- };
1870
-
1871
- MOJFrontend.MultiFileUpload.prototype.uploadFile = function(file) {
1872
- this.params.uploadFileEntryHook(this, file);
1873
- var formData = new FormData();
1874
- formData.append('documents', file);
1875
- var item = $(this.getFileRowHtml(file));
1876
- this.feedbackContainer.find('.moj-multi-file-upload__list').append(item);
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.find('.moj-multi-file-upload__message').html(this.getErrorHtml(response.error));
1887
- this.status.html(response.error.message);
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.find('.moj-multi-file-upload__message').html(this.getSuccessHtml(response.success));
1890
- this.status.html(response.success.messageText);
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.find('.moj-multi-file-upload__actions').append(this.getDeleteButtonHtml(response.file));
1893
- this.params.uploadFileExitHook(this, file, response);
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(this, file, jqXHR, textStatus, errorThrown);
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
- var xhr = new XMLHttpRequest();
1900
- xhr.upload.addEventListener('progress', function(e) {
1901
- if (e.lengthComputable) {
1902
- var percentComplete = e.loaded / e.total;
1903
- percentComplete = parseInt(percentComplete * 100, 10);
1904
- item.find('.moj-multi-file-upload__progress').text(' ' + percentComplete + '%');
1905
- }
1906
- }, false);
1907
- return xhr;
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(); // if user refreshes page and then deletes
1914
- var button = $(e.currentTarget);
1915
- var data = {};
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: 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(this.feedbackContainer.find('.moj-multi-file-upload__row').length === 0) {
1928
- this.feedbackContainer.addClass('moj-hidden');
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 + 'checkboxes-all';
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 += '<div class="govuk-checkboxes__item govuk-checkboxes--small moj-multi-select__checkbox">';
1964
- html += ` <input type="checkbox" class="govuk-checkboxes__input" id="${allId}">`;
1965
- html += ` <label class="govuk-label govuk-checkboxes__label moj-multi-select__toggle-label" for="${allId}">`;
1966
- html += ' <span class="govuk-visually-hidden">Select all</span>';
1967
- html += ' </label>';
1968
- html += '</div>';
1969
- return html;
1970
- };
1971
-
1972
- MOJFrontend.MultiSelect.prototype.onButtonClick = function(e) {
1973
- if(this.checked) {
1974
- this.uncheckAll();
1975
- this.toggleButton[0].checked = false;
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($.proxy(function(index, el) {
1984
- el.checked = true;
1985
- }, this));
1986
- this.checked = true;
1987
- };
1988
-
1989
- MOJFrontend.MultiSelect.prototype.uncheckAll = function() {
1990
- this.checkboxes.each($.proxy(function(index, el) {
1991
- el.checked = false;
1992
- }, this));
1993
- this.checked = false;
1994
- };
1995
-
1996
- MOJFrontend.MultiSelect.prototype.onCheckboxClick = function(e) {
1997
- if(!e.target.checked) {
1998
- this.toggleButton[0].checked = false;
1999
- this.checked = false;
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
- var $el = $(this.el)
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 = $('<button type="button" class="govuk-button govuk-button--secondary moj-password-reveal__button">Show <span class="govuk-visually-hidden">password</span></button>');
2026
- this.container.append(this.button);
2027
- this.button.on('click', $.proxy(this, 'onButtonClick'));
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('click', '.moj-rich-text-editor__toolbar-button', $.proxy(this, 'onButtonClick'));
2070
- this.container.find('.moj-rich-text-editor__content').on('input', $.proxy(this, 'onEditorInput'));
2071
- this.container.find('label').on('click', $.proxy(this, 'onLabelClick'));
2072
- this.toolbar.on('keydown', $.proxy(this, 'onToolbarKeydown'));
2073
- };
2074
-
2075
- MOJFrontend.RichTextEditor.prototype.onToolbarKeydown = function(e) {
2076
- var focusableButton;
2077
- switch(e.keyCode) {
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
- var nextButton = focusableButton.next('button');
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
- var previousButton = focusableButton.prev('button');
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
- var html = '';
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 += '<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>';
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 += '<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>';
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 += '<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>';
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 += '<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>';
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 += '<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>';
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() + '<div class="govuk-textarea moj-rich-text-editor__content" contenteditable="true" spellcheck="false"></div>';
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.find('.moj-rich-text-editor__content').html(this.textarea.val());
2148
- };
2149
-
2150
- MOJFrontend.RichTextEditor.prototype.configureToolbar = function() {
2151
- this.buttons = this.container.find('.moj-rich-text-editor__toolbar-button');
2152
- this.buttons.prop('tabindex', '-1');
2153
- var firstTab = this.buttons.first();
2154
- firstTab.prop('tabindex', '0');
2155
- };
2156
-
2157
- MOJFrontend.RichTextEditor.prototype.onButtonClick = function(e) {
2158
- document.execCommand($(e.currentTarget).data('command'), false, null);
2159
- };
2160
-
2161
- MOJFrontend.RichTextEditor.prototype.getContent = function() {
2162
- return this.container.find('.moj-rich-text-editor__content').html();
2163
- };
2164
-
2165
- MOJFrontend.RichTextEditor.prototype.onEditorInput = function(e) {
2166
- this.updateTextarea();
2167
- };
2168
-
2169
- MOJFrontend.RichTextEditor.prototype.updateTextarea = function() {
2170
- document.execCommand('defaultParagraphSeparator', false, 'p');
2171
- this.textarea.val(this.getContent());
2172
- };
2173
-
2174
- MOJFrontend.RichTextEditor.prototype.onLabelClick = function(e) {
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("moj-search-toggle-initialised")) {
2187
- return;
2750
+ if (this.container.data('moj-search-toggle-initialised')) {
2751
+ return
2188
2752
  }
2189
2753
 
2190
- this.container.data("moj-search-toggle-initialised", true);
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
- '<button class="moj-search-toggle__button" type="button" aria-haspopup="true" aria-expanded="false">' +
2197
- this.options.toggleButton.text +
2198
- svg +
2199
- "</button>"
2200
- );
2201
- this.toggleButton.on("click", $.proxy(this, "onToggleButtonClick"));
2202
- this.toggleButtonContainer.append(this.toggleButton);
2203
- $(document).on("click", this.onDocumentClick.bind(this));
2204
- $(document).on("focusin", this.onDocumentClick.bind(this));
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("aria-expanded", "true");
2209
- this.container.removeClass("moj-js-hidden");
2210
- this.container.find("input").first().get(0).focus();
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("moj-js-hidden");
2215
- this.toggleButton.attr("aria-expanded", "false");
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("aria-expanded") == "false") {
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
- this.table = $(params.table);
2237
-
2238
- if (this.table.data('moj-search-toggle-initialised')) {
2239
- return
2240
- }
2241
-
2242
- this.table.data('moj-search-toggle-initialised', true);
2243
-
2244
- this.setupOptions(params);
2245
- this.body = this.table.find('tbody');
2246
- this.createHeadingButtons();
2247
- this.createStatusBox();
2248
- this.initialiseSortedColumn();
2249
- this.table.on('click', 'th button', $.proxy(this, 'onSortButtonClick'));
2250
- };
2251
-
2252
- MOJFrontend.SortableTable.prototype.setupOptions = function(params) {
2253
- params = params || {};
2254
- this.statusMessage = params.statusMessage || 'Sort by %heading% (%direction%)';
2255
- this.ascendingText = params.ascendingText || 'ascending';
2256
- this.descendingText = params.descendingText || 'descending';
2257
- };
2258
-
2259
- MOJFrontend.SortableTable.prototype.createHeadingButtons = function() {
2260
- var headings = this.table.find('thead th');
2261
- var heading;
2262
- for(var i = 0; i < headings.length; i++) {
2263
- heading = $(headings[i]);
2264
- if(heading.attr('aria-sort')) {
2265
- this.createHeadingButton(heading, i);
2266
- }
2267
- }
2268
- };
2269
-
2270
- MOJFrontend.SortableTable.prototype.createHeadingButton = function(heading, i) {
2271
- var text = heading.text();
2272
- var button = $('<button type="button" data-index="'+i+'">'+text+'</button>');
2273
- heading.text('');
2274
- heading.append(button);
2275
- };
2276
-
2277
- MOJFrontend.SortableTable.prototype.createStatusBox = function() {
2278
- this.status = $('<div aria-live="polite" role="status" aria-atomic="true" class="govuk-visually-hidden" />');
2279
- this.table.parent().append(this.status);
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
- var rows = this.getTableRowsArray();
2851
+ const rows = this.getTableRowsArray()
2284
2852
 
2285
- this.table.find("th")
2853
+ this.table
2854
+ .find('th')
2286
2855
  .filter('[aria-sort="ascending"], [aria-sort="descending"]')
2287
2856
  .first()
2288
2857
  .each((index, el) => {
2289
- var sortDirection = $(el).attr('aria-sort');
2290
- var columnNumber = $(el).find('button').attr('data-index');
2291
- var sortedRows = this.sort(rows, columnNumber, sortDirection);
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
- var columnNumber = e.currentTarget.getAttribute('data-index');
2298
- var sortDirection = $(e.currentTarget).parent().attr('aria-sort');
2299
- var newSortDirection;
2300
- if(sortDirection === 'none' || sortDirection === 'descending') {
2301
- newSortDirection = 'ascending';
2302
- } else {
2303
- newSortDirection = 'descending';
2304
- }
2305
- var rows = this.getTableRowsArray();
2306
- var sortedRows = this.sort(rows, columnNumber, newSortDirection);
2307
- this.addRows(sortedRows);
2308
- this.removeButtonStates();
2309
- this.updateButtonState($(e.currentTarget), newSortDirection);
2310
- };
2311
-
2312
- MOJFrontend.SortableTable.prototype.updateButtonState = function(button, direction) {
2313
- button.parent().attr('aria-sort', direction);
2314
- var message = this.statusMessage;
2315
- message = message.replace(/%heading%/, button.text());
2316
- message = message.replace(/%direction%/, this[direction+'Text']);
2317
- this.status.text(message);
2318
- };
2319
-
2320
- MOJFrontend.SortableTable.prototype.removeButtonStates = function() {
2321
- this.table.find('thead th').attr('aria-sort', 'none');
2322
- };
2323
-
2324
- MOJFrontend.SortableTable.prototype.addRows = function(rows) {
2325
- for(var i = 0; i < rows.length; i++) {
2326
- this.body.append(rows[i]);
2327
- }
2328
- };
2329
-
2330
- MOJFrontend.SortableTable.prototype.getTableRowsArray = function() {
2331
- var rows = [];
2332
- var trs = this.body.find('tr');
2333
- for (var i = 0; i < trs.length; i++) {
2334
- rows.push(trs[i]);
2335
- }
2336
- return rows;
2337
- };
2338
-
2339
- MOJFrontend.SortableTable.prototype.sort = function(rows, columnNumber, sortDirection) {
2340
- var newRows = rows.sort((function(rowA, rowB) {
2341
- var tdA = $(rowA).find('td,th').eq(columnNumber);
2342
- var tdB = $(rowB).find('td,th').eq(columnNumber);
2343
-
2344
- var valueA = sortDirection === 'ascending' ? this.getCellValue(tdA) : this.getCellValue(tdB);
2345
- var valueB = sortDirection === 'ascending' ? this.getCellValue(tdB) : this.getCellValue(tdA);
2346
-
2347
- if (typeof valueA === 'string' || typeof valueB === 'string') return valueA.toString().localeCompare(valueB.toString());
2348
- return valueA-valueB;
2349
- }.bind(this)));
2350
- return newRows;
2351
- };
2352
-
2353
- MOJFrontend.SortableTable.prototype.getCellValue = function(cell) {
2354
- var val = cell.attr('data-sort-value') || cell.html();
2355
-
2356
- var floatVal = parseFloat(val)
2357
- return isNaN(floatVal) ? val : floatVal
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
  }));