@ministryofjustice/frontend 3.3.0 → 3.4.0

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