@ministryofjustice/frontend 4.0.1 → 5.0.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 (244) hide show
  1. package/govuk-prototype-kit.config.json +19 -4
  2. package/moj/_base.scss +2 -0
  3. package/moj/_base.scss.map +1 -0
  4. package/moj/all.bundle.js +2523 -0
  5. package/moj/all.bundle.js.map +1 -0
  6. package/moj/all.bundle.mjs +2502 -0
  7. package/moj/all.bundle.mjs.map +1 -0
  8. package/moj/all.mjs +59 -69
  9. package/moj/all.mjs.map +1 -1
  10. package/moj/all.scss +2 -0
  11. package/moj/all.scss.map +1 -0
  12. package/moj/components/_all.scss +2 -0
  13. package/moj/components/_all.scss.map +1 -0
  14. package/moj/components/action-bar/_action-bar.scss +2 -0
  15. package/moj/components/action-bar/_action-bar.scss.map +1 -0
  16. package/moj/components/add-another/_add-another.scss +2 -0
  17. package/moj/components/add-another/_add-another.scss.map +1 -0
  18. package/moj/components/add-another/add-another.bundle.js +128 -0
  19. package/moj/components/add-another/add-another.bundle.js.map +1 -0
  20. package/moj/components/add-another/add-another.bundle.mjs +120 -0
  21. package/moj/components/add-another/add-another.bundle.mjs.map +1 -0
  22. package/moj/components/add-another/add-another.mjs +112 -99
  23. package/moj/components/add-another/add-another.mjs.map +1 -1
  24. package/moj/components/alert/_alert.scss +4 -0
  25. package/moj/components/alert/_alert.scss.map +1 -0
  26. package/moj/components/alert/alert.bundle.js +330 -0
  27. package/moj/components/alert/alert.bundle.js.map +1 -0
  28. package/moj/components/alert/alert.bundle.mjs +322 -0
  29. package/moj/components/alert/alert.bundle.mjs.map +1 -0
  30. package/moj/components/alert/alert.mjs +181 -217
  31. package/moj/components/alert/alert.mjs.map +1 -1
  32. package/moj/components/alert/{alert.spec.helper.js → alert.spec.helper.bundle.js} +1 -1
  33. package/moj/components/alert/alert.spec.helper.bundle.js.map +1 -0
  34. package/moj/components/alert/alert.spec.helper.bundle.mjs +67 -0
  35. package/moj/components/alert/alert.spec.helper.bundle.mjs.map +1 -0
  36. package/moj/components/alert/alert.spec.helper.mjs.map +1 -1
  37. package/moj/components/badge/_badge.scss +2 -0
  38. package/moj/components/badge/_badge.scss.map +1 -0
  39. package/moj/components/banner/_banner.scss +2 -0
  40. package/moj/components/banner/_banner.scss.map +1 -0
  41. package/moj/components/button-menu/README.md +10 -6
  42. package/moj/components/button-menu/_button-menu.scss +4 -1
  43. package/moj/components/button-menu/_button-menu.scss.map +1 -0
  44. package/moj/components/button-menu/button-menu.bundle.js +299 -0
  45. package/moj/components/button-menu/button-menu.bundle.js.map +1 -0
  46. package/moj/components/button-menu/{button-menu.js → button-menu.bundle.mjs} +74 -121
  47. package/moj/components/button-menu/button-menu.bundle.mjs.map +1 -0
  48. package/moj/components/button-menu/button-menu.mjs +246 -285
  49. package/moj/components/button-menu/button-menu.mjs.map +1 -1
  50. package/moj/components/cookie-banner/_cookie-banner.scss +2 -0
  51. package/moj/components/cookie-banner/_cookie-banner.scss.map +1 -0
  52. package/moj/components/currency-input/_currency-input.scss +2 -0
  53. package/moj/components/currency-input/_currency-input.scss.map +1 -0
  54. package/moj/components/date-picker/_date-picker.scss +2 -0
  55. package/moj/components/date-picker/_date-picker.scss.map +1 -0
  56. package/moj/components/date-picker/date-picker.bundle.js +784 -0
  57. package/moj/components/date-picker/date-picker.bundle.js.map +1 -0
  58. package/moj/components/date-picker/{date-picker.js → date-picker.bundle.mjs} +245 -439
  59. package/moj/components/date-picker/date-picker.bundle.mjs.map +1 -0
  60. package/moj/components/date-picker/date-picker.mjs +654 -840
  61. package/moj/components/date-picker/date-picker.mjs.map +1 -1
  62. package/moj/components/filter/_filter.scss +2 -0
  63. package/moj/components/filter/_filter.scss.map +1 -0
  64. package/moj/components/filter-toggle-button/filter-toggle-button.bundle.js +96 -0
  65. package/moj/components/filter-toggle-button/filter-toggle-button.bundle.js.map +1 -0
  66. package/moj/components/filter-toggle-button/filter-toggle-button.bundle.mjs +88 -0
  67. package/moj/components/filter-toggle-button/filter-toggle-button.bundle.mjs.map +1 -0
  68. package/moj/components/filter-toggle-button/filter-toggle-button.mjs +78 -84
  69. package/moj/components/filter-toggle-button/filter-toggle-button.mjs.map +1 -1
  70. package/moj/components/form-validator/form-validator.bundle.js +198 -0
  71. package/moj/components/form-validator/form-validator.bundle.js.map +1 -0
  72. package/moj/components/form-validator/form-validator.bundle.mjs +190 -0
  73. package/moj/components/form-validator/form-validator.bundle.mjs.map +1 -0
  74. package/moj/components/form-validator/form-validator.mjs +149 -152
  75. package/moj/components/form-validator/form-validator.mjs.map +1 -1
  76. package/moj/components/header/_header.scss +2 -0
  77. package/moj/components/header/_header.scss.map +1 -0
  78. package/moj/components/identity-bar/_identity-bar.scss +2 -0
  79. package/moj/components/identity-bar/_identity-bar.scss.map +1 -0
  80. package/moj/components/interruption-card/_interruption-card.scss +2 -0
  81. package/moj/components/interruption-card/_interruption-card.scss.map +1 -0
  82. package/moj/components/messages/_messages.scss +2 -0
  83. package/moj/components/messages/_messages.scss.map +1 -0
  84. package/moj/components/multi-file-upload/_multi-file-upload.scss +2 -0
  85. package/moj/components/multi-file-upload/_multi-file-upload.scss.map +1 -0
  86. package/moj/components/multi-file-upload/multi-file-upload.bundle.js +223 -0
  87. package/moj/components/multi-file-upload/multi-file-upload.bundle.js.map +1 -0
  88. package/moj/components/multi-file-upload/multi-file-upload.bundle.mjs +215 -0
  89. package/moj/components/multi-file-upload/multi-file-upload.bundle.mjs.map +1 -0
  90. package/moj/components/multi-file-upload/multi-file-upload.mjs +193 -209
  91. package/moj/components/multi-file-upload/multi-file-upload.mjs.map +1 -1
  92. package/moj/components/multi-select/_multi-select.scss +2 -0
  93. package/moj/components/multi-select/_multi-select.scss.map +1 -0
  94. package/moj/components/multi-select/multi-select.bundle.js +78 -0
  95. package/moj/components/multi-select/multi-select.bundle.js.map +1 -0
  96. package/moj/components/multi-select/multi-select.bundle.mjs +70 -0
  97. package/moj/components/multi-select/multi-select.bundle.mjs.map +1 -0
  98. package/moj/components/multi-select/multi-select.mjs +59 -67
  99. package/moj/components/multi-select/multi-select.mjs.map +1 -1
  100. package/moj/components/notification-badge/_notification-badge.scss +2 -0
  101. package/moj/components/notification-badge/_notification-badge.scss.map +1 -0
  102. package/moj/components/organisation-switcher/_organisation-switcher.scss +2 -0
  103. package/moj/components/organisation-switcher/_organisation-switcher.scss.map +1 -0
  104. package/moj/components/page-header-actions/_page-header-actions.scss +2 -0
  105. package/moj/components/page-header-actions/_page-header-actions.scss.map +1 -0
  106. package/moj/components/pagination/_pagination.scss +2 -2
  107. package/moj/components/pagination/_pagination.scss.map +1 -0
  108. package/moj/components/password-reveal/_password-reveal.scss +2 -0
  109. package/moj/components/password-reveal/_password-reveal.scss.map +1 -0
  110. package/moj/components/password-reveal/password-reveal.bundle.js +49 -0
  111. package/moj/components/password-reveal/password-reveal.bundle.js.map +1 -0
  112. package/moj/components/password-reveal/password-reveal.bundle.mjs +41 -0
  113. package/moj/components/password-reveal/password-reveal.bundle.mjs.map +1 -0
  114. package/moj/components/password-reveal/password-reveal.mjs +36 -31
  115. package/moj/components/password-reveal/password-reveal.mjs.map +1 -1
  116. package/moj/components/primary-navigation/_primary-navigation.scss +2 -0
  117. package/moj/components/primary-navigation/_primary-navigation.scss.map +1 -0
  118. package/moj/components/progress-bar/_progress-bar.scss +2 -0
  119. package/moj/components/progress-bar/_progress-bar.scss.map +1 -0
  120. package/moj/components/rich-text-editor/README.md +15 -9
  121. package/moj/components/rich-text-editor/_rich-text-editor.scss +2 -0
  122. package/moj/components/rich-text-editor/_rich-text-editor.scss.map +1 -0
  123. package/moj/components/rich-text-editor/rich-text-editor.bundle.js +145 -0
  124. package/moj/components/rich-text-editor/rich-text-editor.bundle.js.map +1 -0
  125. package/moj/components/rich-text-editor/rich-text-editor.bundle.mjs +137 -0
  126. package/moj/components/rich-text-editor/rich-text-editor.bundle.mjs.map +1 -0
  127. package/moj/components/rich-text-editor/rich-text-editor.mjs +124 -145
  128. package/moj/components/rich-text-editor/rich-text-editor.mjs.map +1 -1
  129. package/moj/components/search/_search.scss +2 -0
  130. package/moj/components/search/_search.scss.map +1 -0
  131. package/moj/components/search-toggle/{search-toggle.scss → _search-toggle.scss} +2 -0
  132. package/moj/components/search-toggle/_search-toggle.scss.map +1 -0
  133. package/moj/components/search-toggle/search-toggle.bundle.js +54 -0
  134. package/moj/components/search-toggle/search-toggle.bundle.js.map +1 -0
  135. package/moj/components/search-toggle/search-toggle.bundle.mjs +46 -0
  136. package/moj/components/search-toggle/search-toggle.bundle.mjs.map +1 -0
  137. package/moj/components/search-toggle/search-toggle.mjs +40 -49
  138. package/moj/components/search-toggle/search-toggle.mjs.map +1 -1
  139. package/moj/components/side-navigation/_side-navigation.scss +2 -0
  140. package/moj/components/side-navigation/_side-navigation.scss.map +1 -0
  141. package/moj/components/sortable-table/_sortable-table.scss +2 -2
  142. package/moj/components/sortable-table/_sortable-table.scss.map +1 -0
  143. package/moj/components/sortable-table/sortable-table.bundle.js +134 -0
  144. package/moj/components/sortable-table/sortable-table.bundle.js.map +1 -0
  145. package/moj/components/sortable-table/sortable-table.bundle.mjs +126 -0
  146. package/moj/components/sortable-table/sortable-table.bundle.mjs.map +1 -0
  147. package/moj/components/sortable-table/sortable-table.mjs +117 -130
  148. package/moj/components/sortable-table/sortable-table.mjs.map +1 -1
  149. package/moj/components/sub-navigation/_sub-navigation.scss +2 -0
  150. package/moj/components/sub-navigation/_sub-navigation.scss.map +1 -0
  151. package/moj/components/tag/_tag.scss +2 -0
  152. package/moj/components/tag/_tag.scss.map +1 -0
  153. package/moj/components/task-list/_task-list.scss +2 -0
  154. package/moj/components/task-list/_task-list.scss.map +1 -0
  155. package/moj/components/ticket-panel/_ticket-panel.scss +2 -0
  156. package/moj/components/ticket-panel/_ticket-panel.scss.map +1 -0
  157. package/moj/components/timeline/_timeline.scss +2 -0
  158. package/moj/components/timeline/_timeline.scss.map +1 -0
  159. package/moj/filters/all.js +44 -22
  160. package/moj/helpers/_all.scss +2 -0
  161. package/moj/helpers/_all.scss.map +1 -0
  162. package/moj/helpers/_hidden.scss +2 -0
  163. package/moj/helpers/_hidden.scss.map +1 -0
  164. package/moj/helpers/_links.scss +2 -0
  165. package/moj/helpers/_links.scss.map +1 -0
  166. package/moj/{helpers.js → helpers.bundle.js} +37 -42
  167. package/moj/helpers.bundle.js.map +1 -0
  168. package/moj/helpers.bundle.mjs +179 -0
  169. package/moj/helpers.bundle.mjs.map +1 -0
  170. package/moj/helpers.mjs +52 -28
  171. package/moj/helpers.mjs.map +1 -1
  172. package/moj/init.js +11 -2
  173. package/moj/moj-frontend.min.css +1 -1
  174. package/moj/moj-frontend.min.css.map +1 -1
  175. package/moj/moj-frontend.min.js +1 -1
  176. package/moj/moj-frontend.min.js.map +1 -1
  177. package/moj/objects/_all.scss +2 -0
  178. package/moj/objects/_all.scss.map +1 -0
  179. package/moj/objects/_button-group.scss +2 -0
  180. package/moj/objects/_button-group.scss.map +1 -0
  181. package/moj/objects/_filter-layout.scss +2 -0
  182. package/moj/objects/_filter-layout.scss.map +1 -0
  183. package/moj/objects/_scrollable-pane.scss +2 -0
  184. package/moj/objects/_scrollable-pane.scss.map +1 -0
  185. package/moj/objects/_width-container.scss +2 -0
  186. package/moj/objects/_width-container.scss.map +1 -0
  187. package/moj/settings/_all.scss +2 -0
  188. package/moj/settings/_all.scss.map +1 -0
  189. package/moj/settings/_assets.scss +2 -0
  190. package/moj/settings/_assets.scss.map +1 -0
  191. package/moj/settings/_colours.scss +2 -0
  192. package/moj/settings/_colours.scss.map +1 -0
  193. package/moj/settings/_measurements.scss +2 -0
  194. package/moj/settings/_measurements.scss.map +1 -0
  195. package/moj/settings/_typography.scss +2 -0
  196. package/moj/settings/_typography.scss.map +1 -0
  197. package/moj/template.njk +13 -0
  198. package/moj/utilities/_all.scss +2 -0
  199. package/moj/utilities/_all.scss.map +1 -0
  200. package/moj/utilities/_hidden.scss +2 -0
  201. package/moj/utilities/_hidden.scss.map +1 -0
  202. package/moj/utilities/_width-container.scss +2 -0
  203. package/moj/utilities/_width-container.scss.map +1 -0
  204. package/moj/vendor/govuk-frontend/_base.scss +2 -0
  205. package/moj/vendor/govuk-frontend/_base.scss.map +1 -0
  206. package/moj/vendor/govuk-frontend/_index.scss +2 -0
  207. package/moj/vendor/govuk-frontend/_index.scss.map +1 -0
  208. package/moj/{version.js → version.bundle.js} +1 -1
  209. package/moj/version.bundle.js.map +1 -0
  210. package/moj/version.bundle.mjs +4 -0
  211. package/moj/version.bundle.mjs.map +1 -0
  212. package/moj/version.mjs.map +1 -1
  213. package/package.json +5 -6
  214. package/moj/all.jquery.min.js +0 -1
  215. package/moj/all.jquery.min.js.map +0 -1
  216. package/moj/all.js +0 -2662
  217. package/moj/all.js.map +0 -1
  218. package/moj/components/add-another/add-another.js +0 -115
  219. package/moj/components/add-another/add-another.js.map +0 -1
  220. package/moj/components/alert/alert.js +0 -356
  221. package/moj/components/alert/alert.js.map +0 -1
  222. package/moj/components/alert/alert.spec.helper.js.map +0 -1
  223. package/moj/components/button-menu/button-menu.js.map +0 -1
  224. package/moj/components/date-picker/date-picker.js.map +0 -1
  225. package/moj/components/filter-toggle-button/filter-toggle-button.js +0 -102
  226. package/moj/components/filter-toggle-button/filter-toggle-button.js.map +0 -1
  227. package/moj/components/form-validator/form-validator.js +0 -205
  228. package/moj/components/form-validator/form-validator.js.map +0 -1
  229. package/moj/components/multi-file-upload/multi-file-upload.js +0 -241
  230. package/moj/components/multi-file-upload/multi-file-upload.js.map +0 -1
  231. package/moj/components/multi-select/multi-select.js +0 -86
  232. package/moj/components/multi-select/multi-select.js.map +0 -1
  233. package/moj/components/password-reveal/password-reveal.js +0 -44
  234. package/moj/components/password-reveal/password-reveal.js.map +0 -1
  235. package/moj/components/rich-text-editor/rich-text-editor.js +0 -166
  236. package/moj/components/rich-text-editor/rich-text-editor.js.map +0 -1
  237. package/moj/components/search-toggle/search-toggle.js +0 -63
  238. package/moj/components/search-toggle/search-toggle.js.map +0 -1
  239. package/moj/components/sortable-table/sortable-table.js +0 -147
  240. package/moj/components/sortable-table/sortable-table.js.map +0 -1
  241. package/moj/helpers.js.map +0 -1
  242. package/moj/vendor/html5shiv.js +0 -326
  243. package/moj/vendor/jquery.js +0 -9300
  244. package/moj/version.js.map +0 -1
@@ -0,0 +1,2523 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.MOJFrontend = global.MOJFrontend || {}));
5
+ })(this, (function (exports) { 'use strict';
6
+
7
+ class AddAnother {
8
+ constructor(container) {
9
+ this.container = container;
10
+ if (this.container.hasAttribute('data-moj-add-another-init')) {
11
+ return this;
12
+ }
13
+ this.container.setAttribute('data-moj-add-another-init', '');
14
+ this.container.addEventListener('click', this.onRemoveButtonClick.bind(this));
15
+ this.container.addEventListener('click', this.onAddButtonClick.bind(this));
16
+ const buttons = this.container.querySelectorAll('.moj-add-another__add-button, moj-add-another__remove-button');
17
+ buttons.forEach(button => {
18
+ if (!(button instanceof HTMLButtonElement)) {
19
+ return;
20
+ }
21
+ button.type = 'button';
22
+ });
23
+ }
24
+ onAddButtonClick(event) {
25
+ const button = event.target;
26
+ if (!button || !(button instanceof HTMLButtonElement) || !button.classList.contains('moj-add-another__add-button')) {
27
+ return;
28
+ }
29
+ const items = this.getItems();
30
+ const item = this.getNewItem();
31
+ if (!item || !(item instanceof HTMLElement)) {
32
+ return;
33
+ }
34
+ this.updateAttributes(item, items.length);
35
+ this.resetItem(item);
36
+ const firstItem = items[0];
37
+ if (!this.hasRemoveButton(firstItem)) {
38
+ this.createRemoveButton(firstItem);
39
+ }
40
+ items[items.length - 1].after(item);
41
+ const input = item.querySelector('input, textarea, select');
42
+ if (input && input instanceof HTMLInputElement) {
43
+ input.focus();
44
+ }
45
+ }
46
+ hasRemoveButton(item) {
47
+ return item.querySelectorAll('.moj-add-another__remove-button').length;
48
+ }
49
+ getItems() {
50
+ if (!this.container) {
51
+ return [];
52
+ }
53
+ const items = Array.from(this.container.querySelectorAll('.moj-add-another__item'));
54
+ return items.filter(item => item instanceof HTMLElement);
55
+ }
56
+ getNewItem() {
57
+ const items = this.getItems();
58
+ const item = items[0].cloneNode(true);
59
+ if (!item || !(item instanceof HTMLElement)) {
60
+ return;
61
+ }
62
+ if (!this.hasRemoveButton(item)) {
63
+ this.createRemoveButton(item);
64
+ }
65
+ return item;
66
+ }
67
+ updateAttributes(item, index) {
68
+ item.querySelectorAll('[data-name]').forEach(el => {
69
+ if (!(el instanceof HTMLInputElement)) {
70
+ return;
71
+ }
72
+ const name = el.getAttribute('data-name') || '';
73
+ const id = el.getAttribute('data-id') || '';
74
+ const originalId = el.id;
75
+ el.name = name.replace(/%index%/, `${index}`);
76
+ el.id = id.replace(/%index%/, `${index}`);
77
+ const label = el.parentElement.querySelector('label') || el.closest('label') || item.querySelector(`[for="${originalId}"]`);
78
+ if (label && label instanceof HTMLLabelElement) {
79
+ label.htmlFor = el.id;
80
+ }
81
+ });
82
+ }
83
+ createRemoveButton(item) {
84
+ const button = document.createElement('button');
85
+ button.type = 'button';
86
+ button.classList.add('govuk-button', 'govuk-button--secondary', 'moj-add-another__remove-button');
87
+ button.textContent = 'Remove';
88
+ item.append(button);
89
+ }
90
+ resetItem(item) {
91
+ item.querySelectorAll('[data-name], [data-id]').forEach(el => {
92
+ if (!(el instanceof HTMLInputElement)) {
93
+ return;
94
+ }
95
+ if (el.type === 'checkbox' || el.type === 'radio') {
96
+ el.checked = false;
97
+ } else {
98
+ el.value = '';
99
+ }
100
+ });
101
+ }
102
+ onRemoveButtonClick(event) {
103
+ const button = event.target;
104
+ if (!button || !(button instanceof HTMLButtonElement) || !button.classList.contains('moj-add-another__remove-button')) {
105
+ return;
106
+ }
107
+ button.closest('.moj-add-another__item').remove();
108
+ const items = this.getItems();
109
+ if (items.length === 1) {
110
+ items[0].querySelector('.moj-add-another__remove-button').remove();
111
+ }
112
+ items.forEach((el, index) => {
113
+ this.updateAttributes(el, index);
114
+ });
115
+ this.focusHeading();
116
+ }
117
+ focusHeading() {
118
+ const heading = this.container.querySelector('.moj-add-another__heading');
119
+ if (heading && heading instanceof HTMLElement) {
120
+ heading.focus();
121
+ }
122
+ }
123
+ }
124
+
125
+ function removeAttributeValue(el, attr, value) {
126
+ let re, m;
127
+ if (el.getAttribute(attr)) {
128
+ if (el.getAttribute(attr) === value) {
129
+ el.removeAttribute(attr);
130
+ } else {
131
+ re = new RegExp(`(^|\\s)${value}(\\s|$)`);
132
+ m = el.getAttribute(attr).match(re);
133
+ if (m && m.length === 3) {
134
+ el.setAttribute(attr, el.getAttribute(attr).replace(re, m[1] && m[2] ? ' ' : ''));
135
+ }
136
+ }
137
+ }
138
+ }
139
+ function addAttributeValue(el, attr, value) {
140
+ let re;
141
+ if (!el.getAttribute(attr)) {
142
+ el.setAttribute(attr, value);
143
+ } else {
144
+ re = new RegExp(`(^|\\s)${value}(\\s|$)`);
145
+ if (!re.test(el.getAttribute(attr))) {
146
+ el.setAttribute(attr, `${el.getAttribute(attr)} ${value}`);
147
+ }
148
+ }
149
+ }
150
+ function dragAndDropSupported() {
151
+ const div = document.createElement('div');
152
+ return typeof div.ondrop !== 'undefined';
153
+ }
154
+ function formDataSupported() {
155
+ return typeof FormData === 'function';
156
+ }
157
+ function fileApiSupported() {
158
+ const input = document.createElement('input');
159
+ input.type = 'file';
160
+ return typeof input.files !== 'undefined';
161
+ }
162
+
163
+ /**
164
+ * Find an elements preceding sibling
165
+ *
166
+ * Utility function to find an elements previous sibling matching the provided
167
+ * selector.
168
+ *
169
+ * @param {Element | null} $element - Element to find siblings for
170
+ * @param {string} [selector] - selector for required sibling
171
+ */
172
+ function getPreviousSibling($element, selector) {
173
+ if (!$element || !($element instanceof HTMLElement)) {
174
+ return;
175
+ }
176
+
177
+ // Get the previous sibling element
178
+ let $sibling = $element.previousElementSibling;
179
+
180
+ // If the sibling matches our selector, use it
181
+ // If not, jump to the next sibling and continue the loop
182
+ while ($sibling) {
183
+ if ($sibling.matches(selector)) return $sibling;
184
+ $sibling = $sibling.previousElementSibling;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * @param {Element | null} $element
190
+ * @param {string} [selector]
191
+ */
192
+ function findNearestMatchingElement($element, selector) {
193
+ // If no element or selector is provided, return
194
+ if (!$element || !($element instanceof HTMLElement) || false) {
195
+ return;
196
+ }
197
+
198
+ // Start with the current element
199
+ let $currentElement = $element;
200
+ while ($currentElement) {
201
+ // First check the current element
202
+ if ($currentElement.matches(selector)) {
203
+ return $currentElement;
204
+ }
205
+
206
+ // Check all previous siblings
207
+ let $sibling = $currentElement.previousElementSibling;
208
+ while ($sibling) {
209
+ // Check if the sibling itself is a heading
210
+ if ($sibling.matches(selector)) {
211
+ return $sibling;
212
+ }
213
+ $sibling = $sibling.previousElementSibling;
214
+ }
215
+
216
+ // If no match found in siblings, move up to parent
217
+ $currentElement = $currentElement.parentElement;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Move focus to element
223
+ *
224
+ * Sets tabindex to -1 to make the element programmatically focusable,
225
+ * but removes it on blur as the element doesn't need to be focused again.
226
+ *
227
+ * @param {HTMLElement} $element - HTML element
228
+ * @param {object} [options] - Handler options
229
+ * @param {function(this: HTMLElement): void} [options.onBeforeFocus] - Callback before focus
230
+ * @param {function(this: HTMLElement): void} [options.onBlur] - Callback on blur
231
+ */
232
+ function setFocus($element, options = {}) {
233
+ const isFocusable = $element.getAttribute('tabindex');
234
+ if (!isFocusable) {
235
+ $element.setAttribute('tabindex', '-1');
236
+ }
237
+
238
+ /**
239
+ * Handle element focus
240
+ */
241
+ function onFocus() {
242
+ $element.addEventListener('blur', onBlur, {
243
+ once: true
244
+ });
245
+ }
246
+
247
+ /**
248
+ * Handle element blur
249
+ */
250
+ function onBlur() {
251
+ if (options.onBlur) {
252
+ options.onBlur.call($element);
253
+ }
254
+ if (!isFocusable) {
255
+ $element.removeAttribute('tabindex');
256
+ }
257
+ }
258
+
259
+ // Add listener to reset element on blur, after focus
260
+ $element.addEventListener('focus', onFocus, {
261
+ once: true
262
+ });
263
+
264
+ // Focus element
265
+ if (options.onBeforeFocus) {
266
+ options.onBeforeFocus.call($element);
267
+ }
268
+ $element.focus();
269
+ }
270
+
271
+ class Alert {
272
+ /**
273
+ * @param {Element | null} $module - HTML element to use for alert
274
+ * @param {AlertConfig} [config] - Alert config
275
+ */
276
+ constructor($module, config = {}) {
277
+ if (!$module || !($module instanceof HTMLElement)) {
278
+ return this;
279
+ }
280
+ const schema = Object.freeze({
281
+ properties: {
282
+ dismissible: {
283
+ type: 'boolean'
284
+ },
285
+ dismissText: {
286
+ type: 'string'
287
+ },
288
+ disableAutoFocus: {
289
+ type: 'boolean'
290
+ },
291
+ focusOnDismissSelector: {
292
+ type: 'string'
293
+ }
294
+ }
295
+ });
296
+ const defaults = {
297
+ dismissible: false,
298
+ dismissText: 'Dismiss',
299
+ disableAutoFocus: false
300
+ };
301
+
302
+ // data attributes override JS config, which overrides defaults
303
+ this.config = this.mergeConfigs(defaults, config, this.parseDataset(schema, $module.dataset));
304
+ this.$module = $module;
305
+
306
+ /**
307
+ * Focus the alert
308
+ *
309
+ * If `role="alert"` is set, focus the element to help some assistive
310
+ * technologies prioritise announcing it.
311
+ *
312
+ * You can turn off the auto-focus functionality by setting
313
+ * `data-disable-auto-focus="true"` in the component HTML. You might wish to
314
+ * do this based on user research findings, or to avoid a clash with another
315
+ * element which should be focused when the page loads.
316
+ */
317
+ if (this.$module.getAttribute('role') === 'alert' && !this.config.disableAutoFocus) {
318
+ setFocus(this.$module);
319
+ }
320
+ this.$dismissButton = this.$module.querySelector('.moj-alert__dismiss');
321
+ if (this.config.dismissible && this.$dismissButton) {
322
+ this.$dismissButton.innerHTML = this.config.dismissText;
323
+ this.$dismissButton.removeAttribute('hidden');
324
+ this.$module.addEventListener('click', event => {
325
+ if (event.target instanceof Node && this.$dismissButton.contains(event.target)) {
326
+ this.dimiss();
327
+ }
328
+ });
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Handle dismissing the alert
334
+ */
335
+ dimiss() {
336
+ let $elementToRecieveFocus;
337
+
338
+ // If a selector has been provided, attempt to find that element
339
+ if (this.config.focusOnDismissSelector) {
340
+ $elementToRecieveFocus = document.querySelector(this.config.focusOnDismissSelector);
341
+ }
342
+
343
+ // Is the next sibling another alert
344
+ if (!$elementToRecieveFocus) {
345
+ const $nextSibling = this.$module.nextElementSibling;
346
+ if ($nextSibling && $nextSibling.matches('.moj-alert')) {
347
+ $elementToRecieveFocus = $nextSibling;
348
+ }
349
+ }
350
+
351
+ // Else try to find any preceding sibling alert or heading
352
+ if (!$elementToRecieveFocus) {
353
+ $elementToRecieveFocus = getPreviousSibling(this.$module, '.moj-alert, h1, h2, h3, h4, h5, h6');
354
+ }
355
+
356
+ // Else find the closest ancestor heading, or fallback to main, or last resort
357
+ // use the body element
358
+ if (!$elementToRecieveFocus) {
359
+ $elementToRecieveFocus = findNearestMatchingElement(this.$module, 'h1, h2, h3, h4, h5, h6, main, body');
360
+ }
361
+
362
+ // If we have an element, place focus on it
363
+ if ($elementToRecieveFocus instanceof HTMLElement) {
364
+ setFocus($elementToRecieveFocus);
365
+ }
366
+
367
+ // Remove the alert
368
+ this.$module.remove();
369
+ }
370
+
371
+ /**
372
+ * Normalise string
373
+ *
374
+ * 'If it looks like a duck, and it quacks like a duck…' 🦆
375
+ *
376
+ * If the passed value looks like a boolean or a number, convert it to a boolean
377
+ * or number.
378
+ *
379
+ * Designed to be used to convert config passed via data attributes (which are
380
+ * always strings) into something sensible.
381
+ *
382
+ * @internal
383
+ * @param {DOMStringMap[string]} value - The value to normalise
384
+ * @param {SchemaProperty} [property] - Component schema property
385
+ * @returns {string | boolean | number | undefined} Normalised data
386
+ */
387
+ normaliseString(value, property) {
388
+ const trimmedValue = value ? value.trim() : '';
389
+ let output;
390
+ let outputType;
391
+ if (property && property.type) {
392
+ outputType = property.type;
393
+ }
394
+
395
+ // No schema type set? Determine automatically
396
+ if (!outputType) {
397
+ if (['true', 'false'].includes(trimmedValue)) {
398
+ outputType = 'boolean';
399
+ }
400
+
401
+ // Empty / whitespace-only strings are considered finite so we need to check
402
+ // the length of the trimmed string as well
403
+ if (trimmedValue.length > 0 && Number.isFinite(Number(trimmedValue))) {
404
+ outputType = 'number';
405
+ }
406
+ }
407
+ switch (outputType) {
408
+ case 'boolean':
409
+ output = trimmedValue === 'true';
410
+ break;
411
+ case 'number':
412
+ output = Number(trimmedValue);
413
+ break;
414
+ default:
415
+ output = value;
416
+ }
417
+ return output;
418
+ }
419
+
420
+ /**
421
+ * Parse dataset
422
+ *
423
+ * Loop over an object and normalise each value using {@link normaliseString},
424
+ * optionally expanding nested `i18n.field`
425
+ *
426
+ * @param {Schema} schema - component schema
427
+ * @param {DOMStringMap} dataset - HTML element dataset
428
+ * @returns {object} Normalised dataset
429
+ */
430
+ parseDataset(schema, dataset) {
431
+ const parsed = {};
432
+ for (const [field, property] of Object.entries(schema.properties)) {
433
+ if (field in dataset) {
434
+ if (dataset[field]) {
435
+ parsed[field] = this.normaliseString(dataset[field], property);
436
+ }
437
+ }
438
+ }
439
+ return parsed;
440
+ }
441
+
442
+ /**
443
+ * Config merging function
444
+ *
445
+ * Takes any number of objects and combines them together, with
446
+ * greatest priority on the LAST item passed in.
447
+ *
448
+ * @param {...{ [key: string]: unknown }} configObjects - Config objects to merge
449
+ * @returns {{ [key: string]: unknown }} A merged config object
450
+ */
451
+ mergeConfigs(...configObjects) {
452
+ const formattedConfigObject = {};
453
+
454
+ // Loop through each of the passed objects
455
+ for (const configObject of configObjects) {
456
+ for (const key of Object.keys(configObject)) {
457
+ const option = formattedConfigObject[key];
458
+ const override = configObject[key];
459
+
460
+ // Push their keys one-by-one into formattedConfigObject. Any duplicate
461
+ // keys with object values will be merged, otherwise the new value will
462
+ // override the existing value.
463
+ if (typeof option === 'object' && typeof override === 'object') {
464
+ // @ts-expect-error Index signature for type 'string' is missing
465
+ formattedConfigObject[key] = this.mergeConfigs(option, override);
466
+ } else {
467
+ formattedConfigObject[key] = override;
468
+ }
469
+ }
470
+ }
471
+ return formattedConfigObject;
472
+ }
473
+ }
474
+
475
+ /**
476
+ * @typedef {object} AlertConfig
477
+ * @property {boolean} [dismissible=false] - Can the alert be dismissed by the user
478
+ * @property {string} [dismissText=Dismiss] - the label text for the dismiss button
479
+ * @property {boolean} [disableAutoFocus=false] - whether the alert will be autofocused
480
+ * @property {string} [focusOnDismissSelector] - CSS Selector for element to be focused on dismiss
481
+ */
482
+
483
+ class ButtonMenu {
484
+ /**
485
+ * @param {Element | null} $module - HTML element to use for button menu
486
+ * @param {ButtonMenuConfig} [config] - Button menu config
487
+ */
488
+ constructor($module, config = {}) {
489
+ if (!$module || !($module instanceof HTMLElement)) {
490
+ return this;
491
+ }
492
+ const schema = Object.freeze({
493
+ properties: {
494
+ buttonText: {
495
+ type: 'string'
496
+ },
497
+ buttonClasses: {
498
+ type: 'string'
499
+ },
500
+ alignMenu: {
501
+ type: 'string'
502
+ }
503
+ }
504
+ });
505
+ const defaults = {
506
+ buttonText: 'Actions',
507
+ alignMenu: 'left',
508
+ buttonClasses: ''
509
+ };
510
+ // data attributes override JS config, which overrides defaults
511
+ this.config = this.mergeConfigs(defaults, config, this.parseDataset(schema, $module.dataset));
512
+ this.$module = $module;
513
+
514
+ // If only one button is provided, don't initiate a menu and toggle button
515
+ // if classes have been provided for the toggleButton, apply them to the single item
516
+ if (this.$module.children.length === 1) {
517
+ const button = this.$module.children[0];
518
+ button.classList.forEach(className => {
519
+ if (className.startsWith('govuk-button-')) {
520
+ button.classList.remove(className);
521
+ }
522
+ button.classList.remove('moj-button-menu__item');
523
+ button.classList.add('moj-button-menu__single-button');
524
+ });
525
+ if (this.config.buttonClasses) {
526
+ button.classList.add(...this.config.buttonClasses.split(' '));
527
+ }
528
+ }
529
+ // Otherwise intialise a button menu
530
+ if (this.$module.children.length > 1) {
531
+ this.initMenu();
532
+ }
533
+ }
534
+ initMenu() {
535
+ this.$menu = this.createMenu();
536
+ this.$module.insertAdjacentHTML('afterbegin', this.toggleTemplate());
537
+ this.setupMenuItems();
538
+ this.$menuToggle = this.$module.querySelector(':scope > button');
539
+ this.items = this.$menu.querySelectorAll('a, button');
540
+ this.$menuToggle.addEventListener('click', event => {
541
+ this.toggleMenu(event);
542
+ });
543
+ this.$module.addEventListener('keydown', event => {
544
+ this.handleKeyDown(event);
545
+ });
546
+ document.addEventListener('click', event => {
547
+ if (!this.$module.contains(event.target)) {
548
+ this.closeMenu(false);
549
+ }
550
+ });
551
+ }
552
+ createMenu() {
553
+ const $menu = document.createElement('ul');
554
+ $menu.setAttribute('role', 'list');
555
+ $menu.hidden = true;
556
+ $menu.classList.add('moj-button-menu__wrapper');
557
+ if (this.config.alignMenu === 'right') {
558
+ $menu.classList.add('moj-button-menu__wrapper--right');
559
+ }
560
+ this.$module.appendChild($menu);
561
+ while (this.$module.firstChild !== $menu) {
562
+ $menu.appendChild(this.$module.firstChild);
563
+ }
564
+ return $menu;
565
+ }
566
+ setupMenuItems() {
567
+ Array.from(this.$menu.children).forEach(item => {
568
+ // wrap item in li tag
569
+ const listItem = document.createElement('li');
570
+ this.$menu.insertBefore(listItem, item);
571
+ listItem.appendChild(item);
572
+ item.setAttribute('tabindex', '-1');
573
+ if (item.tagName === 'BUTTON') {
574
+ item.setAttribute('type', 'button');
575
+ }
576
+ item.classList.forEach(className => {
577
+ if (className.startsWith('govuk-button')) {
578
+ item.classList.remove(className);
579
+ }
580
+ });
581
+
582
+ // add a slight delay after click before closing the menu, makes it *feel* better
583
+ item.addEventListener('click', () => {
584
+ setTimeout(() => {
585
+ this.closeMenu(false);
586
+ }, 50);
587
+ });
588
+ });
589
+ }
590
+ toggleTemplate() {
591
+ return `
592
+ <button type="button" class="govuk-button moj-button-menu__toggle-button ${this.config.buttonClasses || ''}" aria-haspopup="true" aria-expanded="false">
593
+ <span>
594
+ ${this.config.buttonText}
595
+ <svg width="11" height="5" viewBox="0 0 11 5" xmlns="http://www.w3.org/2000/svg">
596
+ <path d="M5.5 0L11 5L0 5L5.5 0Z" fill="currentColor"/>
597
+ </svg>
598
+ </span>
599
+ </button>`;
600
+ }
601
+
602
+ /**
603
+ * @returns {boolean}
604
+ */
605
+ isOpen() {
606
+ return this.$menuToggle.getAttribute('aria-expanded') === 'true';
607
+ }
608
+ toggleMenu(event) {
609
+ event.preventDefault();
610
+
611
+ // If menu is triggered with mouse don't move focus to first item
612
+ const keyboardEvent = event.detail === 0;
613
+ const focusIndex = keyboardEvent ? 0 : -1;
614
+ if (this.isOpen()) {
615
+ this.closeMenu();
616
+ } else {
617
+ this.openMenu(focusIndex);
618
+ }
619
+ }
620
+
621
+ /**
622
+ * Opens the menu and optionally sets the focus to the item with given index
623
+ *
624
+ * @param {number} focusIndex - The index of the item to focus
625
+ */
626
+ openMenu(focusIndex = 0) {
627
+ this.$menu.hidden = false;
628
+ this.$menuToggle.setAttribute('aria-expanded', 'true');
629
+ if (focusIndex !== -1) {
630
+ this.focusItem(focusIndex);
631
+ }
632
+ }
633
+
634
+ /**
635
+ * Closes the menu and optionally returns focus back to menuToggle
636
+ *
637
+ * @param {boolean} moveFocus - whether to return focus to the toggle button
638
+ */
639
+ closeMenu(moveFocus = true) {
640
+ this.$menu.hidden = true;
641
+ this.$menuToggle.setAttribute('aria-expanded', 'false');
642
+ if (moveFocus) {
643
+ this.$menuToggle.focus();
644
+ }
645
+ }
646
+
647
+ /**
648
+ * Focuses the menu item at the specified index
649
+ *
650
+ * @param {number} index - the index of the item to focus
651
+ */
652
+ focusItem(index) {
653
+ if (index >= this.items.length) index = 0;
654
+ if (index < 0) index = this.items.length - 1;
655
+ const menuItem = this.items.item(index);
656
+ if (menuItem) {
657
+ menuItem.focus();
658
+ }
659
+ }
660
+ currentFocusIndex() {
661
+ const activeElement = document.activeElement;
662
+ const menuItems = Array.from(this.items);
663
+ return menuItems.indexOf(activeElement);
664
+ }
665
+ handleKeyDown(event) {
666
+ if (event.target === this.$menuToggle) {
667
+ switch (event.key) {
668
+ case 'ArrowDown':
669
+ event.preventDefault();
670
+ this.openMenu();
671
+ break;
672
+ case 'ArrowUp':
673
+ event.preventDefault();
674
+ this.openMenu(this.items.length - 1);
675
+ break;
676
+ }
677
+ }
678
+ if (this.$menu.contains(event.target) && this.isOpen()) {
679
+ switch (event.key) {
680
+ case 'ArrowDown':
681
+ event.preventDefault();
682
+ if (this.currentFocusIndex() !== -1) {
683
+ this.focusItem(this.currentFocusIndex() + 1);
684
+ }
685
+ break;
686
+ case 'ArrowUp':
687
+ event.preventDefault();
688
+ if (this.currentFocusIndex() !== -1) {
689
+ this.focusItem(this.currentFocusIndex() - 1);
690
+ }
691
+ break;
692
+ case 'Home':
693
+ event.preventDefault();
694
+ this.focusItem(0);
695
+ break;
696
+ case 'End':
697
+ event.preventDefault();
698
+ this.focusItem(this.items.length - 1);
699
+ break;
700
+ }
701
+ }
702
+ if (event.key === 'Escape' && this.isOpen()) {
703
+ this.closeMenu();
704
+ }
705
+ if (event.key === 'Tab' && this.isOpen()) {
706
+ this.closeMenu(false);
707
+ }
708
+ }
709
+
710
+ /**
711
+ * Parse dataset
712
+ *
713
+ * Loop over an object and normalise each value using {@link normaliseString},
714
+ * optionally expanding nested `i18n.field`
715
+ *
716
+ * @param {Schema} schema - component schema
717
+ * @param {DOMStringMap} dataset - HTML element dataset
718
+ * @returns {object} Normalised dataset
719
+ */
720
+ parseDataset(schema, dataset) {
721
+ const parsed = {};
722
+ for (const [field,,] of Object.entries(schema.properties)) {
723
+ if (field in dataset) {
724
+ if (dataset[field]) {
725
+ parsed[field] = dataset[field];
726
+ }
727
+ }
728
+ }
729
+ return parsed;
730
+ }
731
+
732
+ /**
733
+ * Config merging function
734
+ *
735
+ * Takes any number of objects and combines them together, with
736
+ * greatest priority on the LAST item passed in.
737
+ *
738
+ * @param {...{ [key: string]: unknown }} configObjects - Config objects to merge
739
+ * @returns {{ [key: string]: unknown }} A merged config object
740
+ */
741
+ mergeConfigs(...configObjects) {
742
+ const formattedConfigObject = {};
743
+
744
+ // Loop through each of the passed objects
745
+ for (const configObject of configObjects) {
746
+ for (const key of Object.keys(configObject)) {
747
+ const option = formattedConfigObject[key];
748
+ const override = configObject[key];
749
+
750
+ // Push their keys one-by-one into formattedConfigObject. Any duplicate
751
+ // keys with object values will be merged, otherwise the new value will
752
+ // override the existing value.
753
+ if (typeof option === 'object' && typeof override === 'object') {
754
+ // @ts-expect-error Index signature for type 'string' is missing
755
+ formattedConfigObject[key] = this.mergeConfigs(option, override);
756
+ } else {
757
+ formattedConfigObject[key] = override;
758
+ }
759
+ }
760
+ }
761
+ return formattedConfigObject;
762
+ }
763
+ }
764
+
765
+ /**
766
+ * @typedef {object} ButtonMenuConfig
767
+ * @property {string} [buttonText='Actions'] - Label for the toggle button
768
+ * @property {"left" | "right"} [alignMenu='left'] - the alignment of the menu
769
+ * @property {string} [buttonClasses='govuk-button--secondary'] - css classes applied to the toggle button
770
+ */
771
+
772
+ class DatePicker {
773
+ /**
774
+ * @param {Element | null} $module - HTML element to use for date picker
775
+ * @param {DatePickerConfig} [config] - Date picker config
776
+ */
777
+ constructor($module, config = {}) {
778
+ if (!$module || !($module instanceof HTMLElement)) {
779
+ return this;
780
+ }
781
+ const $input = $module.querySelector('.moj-js-datepicker-input');
782
+
783
+ // Check that required elements are present
784
+ if (!$input || !($input instanceof HTMLInputElement)) {
785
+ return this;
786
+ }
787
+ this.$module = $module;
788
+ this.$input = $input;
789
+ const schema = Object.freeze({
790
+ properties: {
791
+ excludedDates: {
792
+ type: 'string'
793
+ },
794
+ excludedDays: {
795
+ type: 'string'
796
+ },
797
+ leadingZeros: {
798
+ type: 'string'
799
+ },
800
+ maxDate: {
801
+ type: 'string'
802
+ },
803
+ minDate: {
804
+ type: 'string'
805
+ },
806
+ weekStartDay: {
807
+ type: 'string'
808
+ }
809
+ }
810
+ });
811
+ const defaults = {
812
+ leadingZeros: false,
813
+ weekStartDay: 'monday'
814
+ };
815
+
816
+ // data attributes override JS config, which overrides defaults
817
+ this.config = this.mergeConfigs(defaults, config, this.parseDataset(schema, $module.dataset));
818
+ this.dayLabels = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
819
+ this.monthLabels = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
820
+ this.currentDate = new Date();
821
+ this.currentDate.setHours(0, 0, 0, 0);
822
+ this.calendarDays = [];
823
+ this.excludedDates = [];
824
+ this.excludedDays = [];
825
+ this.buttonClass = 'moj-datepicker__button';
826
+ this.selectedDayButtonClass = 'moj-datepicker__button--selected';
827
+ this.currentDayButtonClass = 'moj-datepicker__button--current';
828
+ this.todayButtonClass = 'moj-datepicker__button--today';
829
+ if (this.$module.dataset.initialized) {
830
+ return this;
831
+ }
832
+ this.setOptions();
833
+ this.initControls();
834
+ this.$module.setAttribute('data-initialized', 'true');
835
+ }
836
+ initControls() {
837
+ this.id = `datepicker-${this.$input.id}`;
838
+ this.$dialog = this.createDialog();
839
+ this.createCalendarHeaders();
840
+ const $componentWrapper = document.createElement('div');
841
+ const $inputWrapper = document.createElement('div');
842
+ $componentWrapper.classList.add('moj-datepicker__wrapper');
843
+ $inputWrapper.classList.add('govuk-input__wrapper');
844
+ this.$input.parentElement.insertBefore($componentWrapper, this.$input);
845
+ $componentWrapper.appendChild($inputWrapper);
846
+ $inputWrapper.appendChild(this.$input);
847
+ $inputWrapper.insertAdjacentHTML('beforeend', this.toggleTemplate());
848
+ $componentWrapper.insertAdjacentElement('beforeend', this.$dialog);
849
+ this.$calendarButton = this.$module.querySelector('.moj-js-datepicker-toggle');
850
+ this.$dialogTitle = this.$dialog.querySelector('.moj-js-datepicker-month-year');
851
+ this.createCalendar();
852
+ this.$prevMonthButton = this.$dialog.querySelector('.moj-js-datepicker-prev-month');
853
+ this.$prevYearButton = this.$dialog.querySelector('.moj-js-datepicker-prev-year');
854
+ this.$nextMonthButton = this.$dialog.querySelector('.moj-js-datepicker-next-month');
855
+ this.$nextYearButton = this.$dialog.querySelector('.moj-js-datepicker-next-year');
856
+ this.$cancelButton = this.$dialog.querySelector('.moj-js-datepicker-cancel');
857
+ this.$okButton = this.$dialog.querySelector('.moj-js-datepicker-ok');
858
+
859
+ // add event listeners
860
+ this.$prevMonthButton.addEventListener('click', event => this.focusPreviousMonth(event, false));
861
+ this.$prevYearButton.addEventListener('click', event => this.focusPreviousYear(event, false));
862
+ this.$nextMonthButton.addEventListener('click', event => this.focusNextMonth(event, false));
863
+ this.$nextYearButton.addEventListener('click', event => this.focusNextYear(event, false));
864
+ this.$cancelButton.addEventListener('click', event => {
865
+ event.preventDefault();
866
+ this.closeDialog();
867
+ });
868
+ this.$okButton.addEventListener('click', () => {
869
+ this.selectDate(this.currentDate);
870
+ });
871
+ const dialogButtons = this.$dialog.querySelectorAll('button:not([disabled="true"])');
872
+ this.$firstButtonInDialog = dialogButtons[0];
873
+ this.$lastButtonInDialog = dialogButtons[dialogButtons.length - 1];
874
+ this.$firstButtonInDialog.addEventListener('keydown', event => this.firstButtonKeydown(event));
875
+ this.$lastButtonInDialog.addEventListener('keydown', event => this.lastButtonKeydown(event));
876
+ this.$calendarButton.addEventListener('click', event => this.toggleDialog(event));
877
+ this.$dialog.addEventListener('keydown', event => {
878
+ if (event.key === 'Escape') {
879
+ this.closeDialog();
880
+ event.preventDefault();
881
+ event.stopPropagation();
882
+ }
883
+ });
884
+ document.body.addEventListener('mouseup', event => this.backgroundClick(event));
885
+
886
+ // populates calendar with initial dates, avoids Wave errors about null buttons
887
+ this.updateCalendar();
888
+ }
889
+ createDialog() {
890
+ const titleId = `datepicker-title-${this.$input.id}`;
891
+ const $dialog = document.createElement('div');
892
+ $dialog.id = this.id;
893
+ $dialog.setAttribute('class', 'moj-datepicker__dialog');
894
+ $dialog.setAttribute('role', 'dialog');
895
+ $dialog.setAttribute('aria-modal', 'true');
896
+ $dialog.setAttribute('aria-labelledby', titleId);
897
+ $dialog.innerHTML = this.dialogTemplate(titleId);
898
+ $dialog.hidden = true;
899
+ return $dialog;
900
+ }
901
+ createCalendar() {
902
+ const $tbody = this.$dialog.querySelector('tbody');
903
+ let dayCount = 0;
904
+ for (let i = 0; i < 6; i++) {
905
+ // create row
906
+ const $row = $tbody.insertRow(i);
907
+ for (let j = 0; j < 7; j++) {
908
+ // create cell (day)
909
+ const $cell = document.createElement('td');
910
+ const $dateButton = document.createElement('button');
911
+ $cell.appendChild($dateButton);
912
+ $row.appendChild($cell);
913
+ const calendarDay = new DSCalendarDay($dateButton, dayCount, i, j, this);
914
+ this.calendarDays.push(calendarDay);
915
+ dayCount++;
916
+ }
917
+ }
918
+ }
919
+ toggleTemplate() {
920
+ return `<button class="moj-datepicker__toggle moj-js-datepicker-toggle" type="button" aria-haspopup="dialog" aria-controls="${this.id}" aria-expanded="false">
921
+ <span class="govuk-visually-hidden">Choose date</span>
922
+ <svg width="32" height="24" focusable="false" class="moj-datepicker-icon" aria-hidden="true" role="img" viewBox="0 0 22 22">
923
+ <path
924
+ fill="currentColor"
925
+ fill-rule="evenodd"
926
+ clip-rule="evenodd"
927
+ d="M16.1333 2.93333H5.86668V4.4C5.86668 5.21002 5.21003 5.86667 4.40002 5.86667C3.59 5.86667 2.93335 5.21002 2.93335 4.4V2.93333H2C0.895431 2.93333 0 3.82877 0 4.93334V19.2667C0 20.3712 0.89543 21.2667 2 21.2667H20C21.1046 21.2667 22 20.3712 22 19.2667V4.93333C22 3.82876 21.1046 2.93333 20 2.93333H19.0667V4.4C19.0667 5.21002 18.41 5.86667 17.6 5.86667C16.79 5.86667 16.1333 5.21002 16.1333 4.4V2.93333ZM20.5333 8.06667H1.46665V18.8C1.46665 19.3523 1.91436 19.8 2.46665 19.8H19.5333C20.0856 19.8 20.5333 19.3523 20.5333 18.8V8.06667Z"
928
+ ></path>
929
+ <rect x="3.66669" width="1.46667" height="5.13333" rx="0.733333" fill="currentColor"></rect>
930
+ <rect x="16.8667" width="1.46667" height="5.13333" rx="0.733333" fill="currentColor"></rect>
931
+ </svg>
932
+ </button>`;
933
+ }
934
+
935
+ /**
936
+ * HTML template for calendar dialog
937
+ *
938
+ * @param {string} [titleId] - Id attribute for dialog title
939
+ * @returns {string}
940
+ */
941
+ dialogTemplate(titleId) {
942
+ return `<div class="moj-datepicker__dialog-header">
943
+ <div class="moj-datepicker__dialog-navbuttons">
944
+ <button class="moj-datepicker__button moj-js-datepicker-prev-year">
945
+ <span class="govuk-visually-hidden">Previous year</span>
946
+ <svg width="44" height="40" viewBox="0 0 44 40" fill="none" fill="none" focusable="false" aria-hidden="true" role="img">
947
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M23.1643 20L28.9572 14.2071L27.5429 12.7929L20.3358 20L27.5429 27.2071L28.9572 25.7929L23.1643 20Z" fill="currentColor"/>
948
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M17.1643 20L22.9572 14.2071L21.5429 12.7929L14.3358 20L21.5429 27.2071L22.9572 25.7929L17.1643 20Z" fill="currentColor"/>
949
+ </svg>
950
+ </button>
951
+
952
+ <button class="moj-datepicker__button moj-js-datepicker-prev-month">
953
+ <span class="govuk-visually-hidden">Previous month</span>
954
+ <svg width="44" height="40" viewBox="0 0 44 40" fill="none" focusable="false" aria-hidden="true" role="img">
955
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M20.5729 20L25.7865 14.2071L24.5137 12.7929L18.0273 20L24.5137 27.2071L25.7865 25.7929L20.5729 20Z" fill="currentColor"/>
956
+ </svg>
957
+ </button>
958
+ </div>
959
+
960
+ <h2 id="${titleId}" class="moj-datepicker__dialog-title moj-js-datepicker-month-year" aria-live="polite">June 2020</h2>
961
+
962
+ <div class="moj-datepicker__dialog-navbuttons">
963
+ <button class="moj-datepicker__button moj-js-datepicker-next-month">
964
+ <span class="govuk-visually-hidden">Next month</span>
965
+ <svg width="44" height="40" viewBox="0 0 44 40" fill="none" focusable="false" aria-hidden="true" role="img">
966
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M23.4271 20L18.2135 14.2071L19.4863 12.7929L25.9727 20L19.4863 27.2071L18.2135 25.7929L23.4271 20Z" fill="currentColor"/>
967
+ </svg>
968
+ </button>
969
+
970
+ <button class="moj-datepicker__button moj-js-datepicker-next-year">
971
+ <span class="govuk-visually-hidden">Next year</span>
972
+ <svg width="44" height="40" viewBox="0 0 44 40" fill="none" fill="none" focusable="false" aria-hidden="true" role="img">
973
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M20.8357 20L15.0428 14.2071L16.4571 12.7929L23.6642 20L16.4571 27.2071L15.0428 25.7929L20.8357 20Z" fill="currentColor"/>
974
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M26.8357 20L21.0428 14.2071L22.4571 12.7929L29.6642 20L22.4571 27.2071L21.0428 25.7929L26.8357 20Z" fill="currentColor"/>
975
+ </svg>
976
+ </button>
977
+ </div>
978
+ </div>
979
+
980
+ <table class="moj-datepicker__calendar moj-js-datepicker-grid" role="grid" aria-labelledby="${titleId}">
981
+ <thead>
982
+ <tr></tr>
983
+ </thead>
984
+
985
+ <tbody></tbody>
986
+ </table>
987
+
988
+ <div class="govuk-button-group">
989
+ <button type="button" class="govuk-button moj-js-datepicker-ok">Select</button>
990
+ <button type="button" class="govuk-button govuk-button--secondary moj-js-datepicker-cancel">Close</button>
991
+ </div>`;
992
+ }
993
+ createCalendarHeaders() {
994
+ this.dayLabels.forEach(day => {
995
+ const html = `<th scope="col"><span aria-hidden="true">${day.substring(0, 3)}</span><span class="govuk-visually-hidden">${day}</span></th>`;
996
+ const $headerRow = this.$dialog.querySelector('thead > tr');
997
+ $headerRow.insertAdjacentHTML('beforeend', html);
998
+ });
999
+ }
1000
+
1001
+ /**
1002
+ * Pads given number with leading zeros
1003
+ *
1004
+ * @param {number} value - The value to be padded
1005
+ * @param {number} length - The length in characters of the output
1006
+ * @returns {string}
1007
+ */
1008
+ leadingZeros(value, length = 2) {
1009
+ let ret = value.toString();
1010
+ while (ret.length < length) {
1011
+ ret = `0${ret}`;
1012
+ }
1013
+ return ret;
1014
+ }
1015
+ setOptions() {
1016
+ this.setMinAndMaxDatesOnCalendar();
1017
+ this.setExcludedDates();
1018
+ this.setExcludedDays();
1019
+ this.setLeadingZeros();
1020
+ this.setWeekStartDay();
1021
+ }
1022
+ setMinAndMaxDatesOnCalendar() {
1023
+ if (this.config.minDate) {
1024
+ this.minDate = this.formattedDateFromString(this.config.minDate, null);
1025
+ if (this.minDate && this.currentDate < this.minDate) {
1026
+ this.currentDate = this.minDate;
1027
+ }
1028
+ }
1029
+ if (this.config.maxDate) {
1030
+ this.maxDate = this.formattedDateFromString(this.config.maxDate, null);
1031
+ if (this.maxDate && this.currentDate > this.maxDate) {
1032
+ this.currentDate = this.maxDate;
1033
+ }
1034
+ }
1035
+ }
1036
+ setExcludedDates() {
1037
+ if (this.config.excludedDates) {
1038
+ this.excludedDates = this.config.excludedDates.replace(/\s+/, ' ').split(' ').map(item => {
1039
+ return item.includes('-') ? this.parseDateRangeString(item) : [this.formattedDateFromString(item)];
1040
+ }).reduce((dates, items) => dates.concat(items)).filter(date => date);
1041
+ }
1042
+ }
1043
+
1044
+ /*
1045
+ * Parses a daterange string into an array of dates
1046
+ * @param {String} datestring - A daterange string in the format "dd/mm/yyyy-dd/mm/yyyy"
1047
+ * @returns {Date[]}
1048
+ */
1049
+ parseDateRangeString(datestring) {
1050
+ const dates = [];
1051
+ const [startDate, endDate] = datestring.split('-').map(d => this.formattedDateFromString(d, null));
1052
+ if (startDate && endDate) {
1053
+ const date = new Date(startDate.getTime());
1054
+ /* eslint-disable no-unmodified-loop-condition */
1055
+ while (date <= endDate) {
1056
+ dates.push(new Date(date));
1057
+ date.setDate(date.getDate() + 1);
1058
+ }
1059
+ /* eslint-enable no-unmodified-loop-condition */
1060
+ }
1061
+ return dates;
1062
+ }
1063
+ setExcludedDays() {
1064
+ if (this.config.excludedDays) {
1065
+ // lowercase and arrange dayLabels to put indexOf sunday == 0 for comparison
1066
+ // with getDay() function
1067
+ const weekDays = this.dayLabels.map(item => item.toLowerCase());
1068
+ if (this.config.weekStartDay === 'monday') {
1069
+ weekDays.unshift(weekDays.pop());
1070
+ }
1071
+ this.excludedDays = this.config.excludedDays.replace(/\s+/, ' ').toLowerCase().split(' ').map(item => weekDays.indexOf(item)).filter(item => item !== -1);
1072
+ }
1073
+ }
1074
+ setLeadingZeros() {
1075
+ if (typeof this.config.leadingZeros !== 'boolean') {
1076
+ if (this.config.leadingZeros.toLowerCase() === 'true') {
1077
+ this.config.leadingZeros = true;
1078
+ return;
1079
+ }
1080
+ if (this.config.leadingZeros.toLowerCase() === 'false') {
1081
+ this.config.leadingZeros = false;
1082
+ }
1083
+ }
1084
+ }
1085
+ setWeekStartDay() {
1086
+ const weekStartDayParam = this.config.weekStartDay;
1087
+ if (weekStartDayParam && weekStartDayParam.toLowerCase() === 'sunday') {
1088
+ this.config.weekStartDay = 'sunday';
1089
+ // Rotate dayLabels array to put Sunday as the first item
1090
+ this.dayLabels.unshift(this.dayLabels.pop());
1091
+ } else {
1092
+ this.config.weekStartDay = 'monday';
1093
+ }
1094
+ }
1095
+
1096
+ /**
1097
+ * Determine if a date is selecteable
1098
+ *
1099
+ * @param {Date} date - the date to check
1100
+ * @returns {boolean}
1101
+ */
1102
+ isExcludedDate(date) {
1103
+ // This comparison does not work correctly - it will exclude the mindate itself
1104
+ // see: https://github.com/ministryofjustice/moj-frontend/issues/923
1105
+ if (this.minDate && this.minDate > date) {
1106
+ return true;
1107
+ }
1108
+
1109
+ // This comparison works as expected - the maxdate will not be excluded
1110
+ if (this.maxDate && this.maxDate < date) {
1111
+ return true;
1112
+ }
1113
+ for (const excludedDate of this.excludedDates) {
1114
+ if (date.toDateString() === excludedDate.toDateString()) {
1115
+ return true;
1116
+ }
1117
+ }
1118
+ if (this.excludedDays.includes(date.getDay())) {
1119
+ return true;
1120
+ }
1121
+ return false;
1122
+ }
1123
+
1124
+ /**
1125
+ * Get a Date object from a string
1126
+ *
1127
+ * @param {string} dateString - string in the format d/m/yyyy dd/mm/yyyy
1128
+ * @param {Date} fallback - date object to return if formatting fails
1129
+ * @returns {Date}
1130
+ */
1131
+ formattedDateFromString(dateString, fallback = new Date()) {
1132
+ let formattedDate = null;
1133
+ // Accepts d/m/yyyy and dd/mm/yyyy
1134
+ const dateFormatPattern = /(\d{1,2})([-/,. ])(\d{1,2})\2(\d{4})/;
1135
+ if (!dateFormatPattern.test(dateString)) return fallback;
1136
+ const match = dateFormatPattern.exec(dateString);
1137
+ const day = match[1];
1138
+ const month = match[3];
1139
+ const year = match[4];
1140
+ formattedDate = new Date(`${year}-${month}-${day}`);
1141
+ if (formattedDate instanceof Date && Number.isFinite(formattedDate.getTime())) {
1142
+ return formattedDate;
1143
+ }
1144
+ return fallback;
1145
+ }
1146
+
1147
+ /**
1148
+ * Get a formatted date string from a Date object
1149
+ *
1150
+ * @param {Date} date - date to format to a string
1151
+ * @returns {string}
1152
+ */
1153
+ formattedDateFromDate(date) {
1154
+ if (this.config.leadingZeros) {
1155
+ return `${this.leadingZeros(date.getDate())}/${this.leadingZeros(date.getMonth() + 1)}/${date.getFullYear()}`;
1156
+ }
1157
+ return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`;
1158
+ }
1159
+
1160
+ /**
1161
+ * Get a human readable date in the format Monday 2 March 2024
1162
+ *
1163
+ * @param {Date} date - date to format
1164
+ * @returns {string}
1165
+ */
1166
+ formattedDateHuman(date) {
1167
+ return `${this.dayLabels[(date.getDay() + 6) % 7]} ${date.getDate()} ${this.monthLabels[date.getMonth()]} ${date.getFullYear()}`;
1168
+ }
1169
+ backgroundClick(event) {
1170
+ if (this.isOpen() && event.target instanceof Node && !this.$dialog.contains(event.target) && !this.$input.contains(event.target) && !this.$calendarButton.contains(event.target)) {
1171
+ event.preventDefault();
1172
+ this.closeDialog();
1173
+ }
1174
+ }
1175
+ firstButtonKeydown(event) {
1176
+ if (event.key === 'Tab' && event.shiftKey) {
1177
+ this.$lastButtonInDialog.focus();
1178
+ event.preventDefault();
1179
+ }
1180
+ }
1181
+ lastButtonKeydown(event) {
1182
+ if (event.key === 'Tab' && !event.shiftKey) {
1183
+ this.$firstButtonInDialog.focus();
1184
+ event.preventDefault();
1185
+ }
1186
+ }
1187
+
1188
+ // render calendar
1189
+ updateCalendar() {
1190
+ this.$dialogTitle.innerHTML = `${this.monthLabels[this.currentDate.getMonth()]} ${this.currentDate.getFullYear()}`;
1191
+ const day = this.currentDate;
1192
+ const firstOfMonth = new Date(day.getFullYear(), day.getMonth(), 1);
1193
+ let dayOfWeek;
1194
+ if (this.config.weekStartDay === 'monday') {
1195
+ dayOfWeek = firstOfMonth.getDay() === 0 ? 6 : firstOfMonth.getDay() - 1; // Change logic to make Monday first day of week, i.e. 0
1196
+ } else {
1197
+ dayOfWeek = firstOfMonth.getDay();
1198
+ }
1199
+ firstOfMonth.setDate(firstOfMonth.getDate() - dayOfWeek);
1200
+ const thisDay = new Date(firstOfMonth);
1201
+
1202
+ // loop through our days
1203
+ for (const calendarDay of this.calendarDays) {
1204
+ const hidden = thisDay.getMonth() !== day.getMonth();
1205
+ const disabled = this.isExcludedDate(thisDay);
1206
+ calendarDay.update(thisDay, hidden, disabled);
1207
+ thisDay.setDate(thisDay.getDate() + 1);
1208
+ }
1209
+ }
1210
+ setCurrentDate(focus = true) {
1211
+ const {
1212
+ currentDate
1213
+ } = this;
1214
+ this.calendarDays.forEach(calendarDay => {
1215
+ calendarDay.button.classList.add('moj-datepicker__button');
1216
+ calendarDay.button.classList.add('moj-datepicker__calendar-day');
1217
+ calendarDay.button.setAttribute('tabindex', '-1');
1218
+ calendarDay.button.classList.remove(this.selectedDayButtonClass);
1219
+ const calendarDayDate = calendarDay.date;
1220
+ calendarDayDate.setHours(0, 0, 0, 0);
1221
+ const today = new Date();
1222
+ today.setHours(0, 0, 0, 0);
1223
+ if (calendarDayDate.getTime() === currentDate.getTime() /* && !calendarDay.button.disabled */) {
1224
+ if (focus) {
1225
+ calendarDay.button.setAttribute('tabindex', '0');
1226
+ calendarDay.button.focus();
1227
+ calendarDay.button.classList.add(this.selectedDayButtonClass);
1228
+ }
1229
+ }
1230
+ if (this.inputDate && calendarDayDate.getTime() === this.inputDate.getTime()) {
1231
+ calendarDay.button.classList.add(this.currentDayButtonClass);
1232
+ calendarDay.button.setAttribute('aria-current', 'date');
1233
+ } else {
1234
+ calendarDay.button.classList.remove(this.currentDayButtonClass);
1235
+ calendarDay.button.removeAttribute('aria-current');
1236
+ }
1237
+ if (calendarDayDate.getTime() === today.getTime()) {
1238
+ calendarDay.button.classList.add(this.todayButtonClass);
1239
+ } else {
1240
+ calendarDay.button.classList.remove(this.todayButtonClass);
1241
+ }
1242
+ });
1243
+
1244
+ // if no date is tab-able, make the first non-disabled date tab-able
1245
+ if (!focus) {
1246
+ const enabledDays = this.calendarDays.filter(calendarDay => {
1247
+ return window.getComputedStyle(calendarDay.button).display === 'block' && !calendarDay.button.disabled;
1248
+ });
1249
+ enabledDays[0].button.setAttribute('tabindex', '0');
1250
+ this.currentDate = enabledDays[0].date;
1251
+ }
1252
+ }
1253
+ selectDate(date) {
1254
+ if (this.isExcludedDate(date)) {
1255
+ return;
1256
+ }
1257
+ this.$calendarButton.querySelector('span').innerText = `Choose date. Selected date is ${this.formattedDateHuman(date)}`;
1258
+ this.$input.value = this.formattedDateFromDate(date);
1259
+ const changeEvent = new Event('change', {
1260
+ bubbles: true,
1261
+ cancelable: true
1262
+ });
1263
+ this.$input.dispatchEvent(changeEvent);
1264
+ this.closeDialog();
1265
+ }
1266
+ isOpen() {
1267
+ return this.$dialog.classList.contains('moj-datepicker__dialog--open');
1268
+ }
1269
+ toggleDialog(event) {
1270
+ event.preventDefault();
1271
+ if (this.isOpen()) {
1272
+ this.closeDialog();
1273
+ } else {
1274
+ this.setMinAndMaxDatesOnCalendar();
1275
+ this.openDialog();
1276
+ }
1277
+ }
1278
+ openDialog() {
1279
+ this.$dialog.hidden = false;
1280
+ this.$dialog.classList.add('moj-datepicker__dialog--open');
1281
+ this.$calendarButton.setAttribute('aria-expanded', 'true');
1282
+
1283
+ // position the dialog
1284
+ // if input is wider than dialog pin it to the right
1285
+ if (this.$input.offsetWidth > this.$dialog.offsetWidth) {
1286
+ this.$dialog.style.right = `0px`;
1287
+ }
1288
+ this.$dialog.style.top = `${this.$input.offsetHeight + 3}px`;
1289
+
1290
+ // get the date from the input element
1291
+ this.inputDate = this.formattedDateFromString(this.$input.value);
1292
+ this.currentDate = this.inputDate;
1293
+ this.currentDate.setHours(0, 0, 0, 0);
1294
+ this.updateCalendar();
1295
+ this.setCurrentDate();
1296
+ }
1297
+ closeDialog() {
1298
+ this.$dialog.hidden = true;
1299
+ this.$dialog.classList.remove('moj-datepicker__dialog--open');
1300
+ this.$calendarButton.setAttribute('aria-expanded', 'false');
1301
+ this.$calendarButton.focus();
1302
+ }
1303
+ goToDate(date, focus) {
1304
+ const current = this.currentDate;
1305
+ this.currentDate = date;
1306
+ if (current.getMonth() !== this.currentDate.getMonth() || current.getFullYear() !== this.currentDate.getFullYear()) {
1307
+ this.updateCalendar();
1308
+ }
1309
+ this.setCurrentDate(focus);
1310
+ }
1311
+
1312
+ // day navigation
1313
+ focusNextDay() {
1314
+ const date = new Date(this.currentDate);
1315
+ date.setDate(date.getDate() + 1);
1316
+ this.goToDate(date);
1317
+ }
1318
+ focusPreviousDay() {
1319
+ const date = new Date(this.currentDate);
1320
+ date.setDate(date.getDate() - 1);
1321
+ this.goToDate(date);
1322
+ }
1323
+
1324
+ // week navigation
1325
+ focusNextWeek() {
1326
+ const date = new Date(this.currentDate);
1327
+ date.setDate(date.getDate() + 7);
1328
+ this.goToDate(date);
1329
+ }
1330
+ focusPreviousWeek() {
1331
+ const date = new Date(this.currentDate);
1332
+ date.setDate(date.getDate() - 7);
1333
+ this.goToDate(date);
1334
+ }
1335
+ focusFirstDayOfWeek() {
1336
+ const date = new Date(this.currentDate);
1337
+ const firstDayOfWeekIndex = this.config.weekStartDay === 'sunday' ? 0 : 1;
1338
+ const dayOfWeek = date.getDay();
1339
+ const diff = dayOfWeek >= firstDayOfWeekIndex ? dayOfWeek - firstDayOfWeekIndex : 6 - dayOfWeek;
1340
+ date.setDate(date.getDate() - diff);
1341
+ date.setHours(0, 0, 0, 0);
1342
+ this.goToDate(date);
1343
+ }
1344
+ focusLastDayOfWeek() {
1345
+ const date = new Date(this.currentDate);
1346
+ const lastDayOfWeekIndex = this.config.weekStartDay === 'sunday' ? 6 : 0;
1347
+ const dayOfWeek = date.getDay();
1348
+ const diff = dayOfWeek <= lastDayOfWeekIndex ? lastDayOfWeekIndex - dayOfWeek : 7 - dayOfWeek;
1349
+ date.setDate(date.getDate() + diff);
1350
+ date.setHours(0, 0, 0, 0);
1351
+ this.goToDate(date);
1352
+ }
1353
+
1354
+ // month navigation
1355
+ focusNextMonth(event, focus = true) {
1356
+ event.preventDefault();
1357
+ const date = new Date(this.currentDate);
1358
+ date.setMonth(date.getMonth() + 1, 1);
1359
+ this.goToDate(date, focus);
1360
+ }
1361
+ focusPreviousMonth(event, focus = true) {
1362
+ event.preventDefault();
1363
+ const date = new Date(this.currentDate);
1364
+ date.setMonth(date.getMonth() - 1, 1);
1365
+ this.goToDate(date, focus);
1366
+ }
1367
+
1368
+ // year navigation
1369
+ focusNextYear(event, focus = true) {
1370
+ event.preventDefault();
1371
+ const date = new Date(this.currentDate);
1372
+ date.setFullYear(date.getFullYear() + 1, date.getMonth(), 1);
1373
+ this.goToDate(date, focus);
1374
+ }
1375
+ focusPreviousYear(event, focus = true) {
1376
+ event.preventDefault();
1377
+ const date = new Date(this.currentDate);
1378
+ date.setFullYear(date.getFullYear() - 1, date.getMonth(), 1);
1379
+ this.goToDate(date, focus);
1380
+ }
1381
+
1382
+ /**
1383
+ * Parse dataset
1384
+ *
1385
+ * @param {Schema} schema - Component class
1386
+ * @param {DOMStringMap} dataset - HTML element dataset
1387
+ * @returns {object} Normalised dataset
1388
+ */
1389
+ parseDataset(schema, dataset) {
1390
+ const parsed = {};
1391
+ for (const [field,,] of Object.entries(schema.properties)) {
1392
+ if (field in dataset) {
1393
+ parsed[field] = dataset[field];
1394
+ }
1395
+ }
1396
+ return parsed;
1397
+ }
1398
+
1399
+ /**
1400
+ * Config merging function
1401
+ *
1402
+ * Takes any number of objects and combines them together, with
1403
+ * greatest priority on the LAST item passed in.
1404
+ *
1405
+ * @param {...{ [key: string]: unknown }} configObjects - Config objects to merge
1406
+ * @returns {{ [key: string]: unknown }} A merged config object
1407
+ */
1408
+ mergeConfigs(...configObjects) {
1409
+ const formattedConfigObject = {};
1410
+
1411
+ // Loop through each of the passed objects
1412
+ for (const configObject of configObjects) {
1413
+ for (const key of Object.keys(configObject)) {
1414
+ const option = formattedConfigObject[key];
1415
+ const override = configObject[key];
1416
+
1417
+ // Push their keys one-by-one into formattedConfigObject. Any duplicate
1418
+ // keys with object values will be merged, otherwise the new value will
1419
+ // override the existing value.
1420
+ if (typeof option === 'object' && typeof override === 'object') {
1421
+ // @ts-expect-error Index signature for type 'string' is missing
1422
+ formattedConfigObject[key] = this.mergeConfigs(option, override);
1423
+ } else {
1424
+ formattedConfigObject[key] = override;
1425
+ }
1426
+ }
1427
+ }
1428
+ return formattedConfigObject;
1429
+ }
1430
+ }
1431
+ class DSCalendarDay {
1432
+ /**
1433
+ *
1434
+ * @param {HTMLElement} button
1435
+ * @param {number} index
1436
+ * @param {number} row
1437
+ * @param {number} column
1438
+ * @param {DatePicker} picker
1439
+ */
1440
+ constructor(button, index, row, column, picker) {
1441
+ this.index = index;
1442
+ this.row = row;
1443
+ this.column = column;
1444
+ this.button = button;
1445
+ this.picker = picker;
1446
+ this.date = new Date();
1447
+ this.button.addEventListener('keydown', this.keyPress.bind(this));
1448
+ this.button.addEventListener('click', this.click.bind(this));
1449
+ }
1450
+
1451
+ /**
1452
+ * @param {Date} day - the Date for the calendar day
1453
+ * @param {boolean} hidden - visibility of the day
1454
+ * @param {boolean} disabled - is the day selectable or excluded
1455
+ */
1456
+ update(day, hidden, disabled) {
1457
+ const label = day.getDate();
1458
+ let accessibleLabel = this.picker.formattedDateHuman(day);
1459
+ if (disabled) {
1460
+ this.button.setAttribute('aria-disabled', 'true');
1461
+ accessibleLabel = `Excluded date, ${accessibleLabel}`;
1462
+ } else {
1463
+ this.button.removeAttribute('aria-disabled');
1464
+ }
1465
+ if (hidden) {
1466
+ this.button.style.display = 'none';
1467
+ } else {
1468
+ this.button.style.display = 'block';
1469
+ }
1470
+ this.button.setAttribute('data-testid', this.picker.formattedDateFromDate(day));
1471
+ this.button.innerHTML = `<span class="govuk-visually-hidden">${accessibleLabel}</span><span aria-hidden="true">${label}</span>`;
1472
+ this.date = new Date(day);
1473
+ }
1474
+ click(event) {
1475
+ this.picker.goToDate(this.date);
1476
+ this.picker.selectDate(this.date);
1477
+ event.stopPropagation();
1478
+ event.preventDefault();
1479
+ }
1480
+ keyPress(event) {
1481
+ let calendarNavKey = true;
1482
+ switch (event.key) {
1483
+ case 'ArrowLeft':
1484
+ this.picker.focusPreviousDay();
1485
+ break;
1486
+ case 'ArrowRight':
1487
+ this.picker.focusNextDay();
1488
+ break;
1489
+ case 'ArrowUp':
1490
+ this.picker.focusPreviousWeek();
1491
+ break;
1492
+ case 'ArrowDown':
1493
+ this.picker.focusNextWeek();
1494
+ break;
1495
+ case 'Home':
1496
+ this.picker.focusFirstDayOfWeek();
1497
+ break;
1498
+ case 'End':
1499
+ this.picker.focusLastDayOfWeek();
1500
+ break;
1501
+ case 'PageUp':
1502
+ {
1503
+ if (event.shiftKey) {
1504
+ this.picker.focusPreviousYear(event);
1505
+ } else {
1506
+ this.picker.focusPreviousMonth(event);
1507
+ }
1508
+ break;
1509
+ }
1510
+ case 'PageDown':
1511
+ {
1512
+ if (event.shiftKey) {
1513
+ this.picker.focusNextYear(event);
1514
+ } else {
1515
+ this.picker.focusNextMonth(event);
1516
+ }
1517
+ break;
1518
+ }
1519
+ default:
1520
+ calendarNavKey = false;
1521
+ break;
1522
+ }
1523
+ if (calendarNavKey) {
1524
+ event.preventDefault();
1525
+ event.stopPropagation();
1526
+ }
1527
+ }
1528
+ }
1529
+
1530
+ /**
1531
+ * Date picker config
1532
+ *
1533
+ * @typedef {object} DatePickerConfig
1534
+ * @property {string} [excludedDates] - Dates that cannot be selected
1535
+ * @property {string} [excludedDays] - Days that cannot be selected
1536
+ * @property {boolean} [leadingZeroes] - Whether to add leading zeroes when populating the field
1537
+ * @property {string} [minDate] - The earliest available date
1538
+ * @property {string} [maxDate] - The latest available date
1539
+ * @property {string} [weekStartDay] - First day of the week in calendar view
1540
+ */
1541
+
1542
+ /**
1543
+ * @import { Schema } from '../../all.mjs'
1544
+ */
1545
+
1546
+ class FilterToggleButton {
1547
+ constructor(options) {
1548
+ this.options = options;
1549
+ this.container = this.options.toggleButton.container;
1550
+ this.filterContainer = this.options.filter.container;
1551
+ this.createToggleButton();
1552
+ this.setupResponsiveChecks();
1553
+ this.filterContainer.setAttribute('tabindex', '-1');
1554
+ if (this.options.startHidden) {
1555
+ this.hideMenu();
1556
+ }
1557
+ }
1558
+ setupResponsiveChecks() {
1559
+ this.mq = window.matchMedia(this.options.bigModeMediaQuery);
1560
+ this.mq.addListener(this.checkMode.bind(this));
1561
+ this.checkMode(this.mq);
1562
+ }
1563
+ createToggleButton() {
1564
+ this.menuButton = document.createElement('button');
1565
+ this.menuButton.setAttribute('type', 'button');
1566
+ this.menuButton.setAttribute('aria-haspopup', 'true');
1567
+ this.menuButton.setAttribute('aria-expanded', 'false');
1568
+ this.menuButton.className = `govuk-button ${this.options.toggleButton.classes}`;
1569
+ this.menuButton.textContent = this.options.toggleButton.showText;
1570
+ this.menuButton.addEventListener('click', this.onMenuButtonClick.bind(this));
1571
+ this.container.append(this.menuButton);
1572
+ }
1573
+ checkMode(mq) {
1574
+ if (mq.matches) {
1575
+ this.enableBigMode();
1576
+ } else {
1577
+ this.enableSmallMode();
1578
+ }
1579
+ }
1580
+ enableBigMode() {
1581
+ this.showMenu();
1582
+ this.removeCloseButton();
1583
+ }
1584
+ enableSmallMode() {
1585
+ this.hideMenu();
1586
+ this.addCloseButton();
1587
+ }
1588
+ addCloseButton() {
1589
+ if (!this.options.closeButton) {
1590
+ return;
1591
+ }
1592
+ this.closeButton = document.createElement('button');
1593
+ this.closeButton.setAttribute('type', 'button');
1594
+ this.closeButton.className = 'moj-filter__close';
1595
+ this.closeButton.textContent = this.options.closeButton.text;
1596
+ this.closeButton.addEventListener('click', this.onCloseClick.bind(this));
1597
+ this.options.closeButton.container.append(this.closeButton);
1598
+ }
1599
+ onCloseClick() {
1600
+ this.hideMenu();
1601
+ this.menuButton.focus();
1602
+ }
1603
+ removeCloseButton() {
1604
+ if (this.closeButton) {
1605
+ this.closeButton.remove();
1606
+ this.closeButton = null;
1607
+ }
1608
+ }
1609
+ hideMenu() {
1610
+ this.menuButton.setAttribute('aria-expanded', 'false');
1611
+ this.filterContainer.classList.add('moj-js-hidden');
1612
+ this.menuButton.textContent = this.options.toggleButton.showText;
1613
+ }
1614
+ showMenu() {
1615
+ this.menuButton.setAttribute('aria-expanded', 'true');
1616
+ this.filterContainer.classList.remove('moj-js-hidden');
1617
+ this.menuButton.textContent = this.options.toggleButton.hideText;
1618
+ }
1619
+ onMenuButtonClick() {
1620
+ this.toggle();
1621
+ }
1622
+ toggle() {
1623
+ if (this.menuButton.getAttribute('aria-expanded') === 'false') {
1624
+ this.showMenu();
1625
+ this.filterContainer.focus();
1626
+ } else {
1627
+ this.hideMenu();
1628
+ }
1629
+ }
1630
+ }
1631
+
1632
+ class FormValidator {
1633
+ /**
1634
+ * @param {Element | null} form - HTML element to use for form validator
1635
+ * @param {FormValidatorConfig} [config] - Button menu config
1636
+ */
1637
+ constructor(form, config = {}) {
1638
+ if (!form || !(form instanceof HTMLFormElement)) {
1639
+ return this;
1640
+ }
1641
+ this.form = form;
1642
+ this.errors = [];
1643
+ this.validators = [];
1644
+ this.form.addEventListener('submit', this.onSubmit.bind(this));
1645
+ this.summary = config.summary || document.querySelector('.govuk-error-summary');
1646
+ this.originalTitle = document.title;
1647
+ }
1648
+ escapeHtml(string) {
1649
+ return String(string).replace(/[&<>"'`=/]/g, function fromEntityMap(s) {
1650
+ return FormValidator.entityMap[s];
1651
+ });
1652
+ }
1653
+ resetTitle() {
1654
+ document.title = this.originalTitle;
1655
+ }
1656
+ updateTitle() {
1657
+ document.title = `${this.errors.length} errors - ${document.title}`;
1658
+ }
1659
+ showSummary() {
1660
+ this.summary.innerHTML = this.getSummaryHtml();
1661
+ this.summary.classList.remove('moj-hidden');
1662
+ this.summary.setAttribute('aria-labelledby', 'errorSummary-heading');
1663
+ this.summary.focus();
1664
+ }
1665
+ getSummaryHtml() {
1666
+ let html = '<h2 id="error-summary-title" class="govuk-error-summary__title">There is a problem</h2>';
1667
+ html += '<div class="govuk-error-summary__body">';
1668
+ html += '<ul class="govuk-list govuk-error-summary__list">';
1669
+ for (const error of this.errors) {
1670
+ html += '<li>';
1671
+ html += `<a href="#${this.escapeHtml(error.fieldName)}">`;
1672
+ html += this.escapeHtml(error.message);
1673
+ html += '</a>';
1674
+ html += '</li>';
1675
+ }
1676
+ html += '</ul>';
1677
+ html += '</div>';
1678
+ return html;
1679
+ }
1680
+ hideSummary() {
1681
+ this.summary.classList.add('moj-hidden');
1682
+ this.summary.removeAttribute('aria-labelledby');
1683
+ }
1684
+ onSubmit(event) {
1685
+ this.removeInlineErrors();
1686
+ this.hideSummary();
1687
+ this.resetTitle();
1688
+ if (!this.validate()) {
1689
+ event.preventDefault();
1690
+ this.updateTitle();
1691
+ this.showSummary();
1692
+ this.showInlineErrors();
1693
+ }
1694
+ }
1695
+ showInlineErrors() {
1696
+ for (const error of this.errors) {
1697
+ this.showInlineError(error);
1698
+ }
1699
+ }
1700
+ showInlineError(error) {
1701
+ const errorSpan = document.createElement('span');
1702
+ errorSpan.id = `${error.fieldName}-error`;
1703
+ errorSpan.classList.add('govuk-error-message');
1704
+ errorSpan.innerHTML = this.escapeHtml(error.message);
1705
+ const control = document.querySelector(`#${error.fieldName}`);
1706
+ const fieldset = control.closest('.govuk-fieldset');
1707
+ const fieldContainer = (fieldset || control).closest('.govuk-form-group');
1708
+ const label = fieldContainer.querySelector('label');
1709
+ const legend = fieldContainer.querySelector('legend');
1710
+ fieldContainer.classList.add('govuk-form-group--error');
1711
+ if (fieldset && legend) {
1712
+ legend.after(errorSpan);
1713
+ fieldContainer.setAttribute('aria-invalid', 'true');
1714
+ addAttributeValue(fieldset, 'aria-describedby', errorSpan.id);
1715
+ } else if (label && control) {
1716
+ label.after(errorSpan);
1717
+ control.setAttribute('aria-invalid', 'true');
1718
+ addAttributeValue(control, 'aria-describedby', errorSpan.id);
1719
+ }
1720
+ }
1721
+ removeInlineErrors() {
1722
+ for (const error of this.errors) {
1723
+ this.removeInlineError(error);
1724
+ }
1725
+ }
1726
+ removeInlineError(error) {
1727
+ const errorSpan = document.querySelector(`#${error.fieldName}-error`);
1728
+ const control = document.querySelector(`#${error.fieldName}`);
1729
+ const fieldset = control.closest('.govuk-fieldset');
1730
+ const fieldContainer = (fieldset || control).closest('.govuk-form-group');
1731
+ const label = fieldContainer.querySelector('label');
1732
+ const legend = fieldContainer.querySelector('legend');
1733
+ errorSpan.remove();
1734
+ fieldContainer.classList.remove('govuk-form-group--error');
1735
+ if (fieldset && legend) {
1736
+ fieldContainer.removeAttribute('aria-invalid');
1737
+ removeAttributeValue(fieldset, 'aria-describedby', errorSpan.id);
1738
+ } else if (label && control) {
1739
+ control.removeAttribute('aria-invalid');
1740
+ removeAttributeValue(control, 'aria-describedby', errorSpan.id);
1741
+ }
1742
+ }
1743
+ addValidator(fieldName, rules) {
1744
+ this.validators.push({
1745
+ fieldName,
1746
+ rules,
1747
+ field: this.form.elements[fieldName]
1748
+ });
1749
+ }
1750
+ validate() {
1751
+ this.errors = [];
1752
+ let validator = null;
1753
+ let validatorReturnValue = true;
1754
+ let i;
1755
+ let j;
1756
+ for (i = 0; i < this.validators.length; i++) {
1757
+ validator = this.validators[i];
1758
+ for (j = 0; j < validator.rules.length; j++) {
1759
+ validatorReturnValue = validator.rules[j].method(validator.field, validator.rules[j].params);
1760
+ if (typeof validatorReturnValue === 'boolean' && !validatorReturnValue) {
1761
+ this.errors.push({
1762
+ fieldName: validator.fieldName,
1763
+ message: validator.rules[j].message
1764
+ });
1765
+ break;
1766
+ } else if (typeof validatorReturnValue === 'string') {
1767
+ this.errors.push({
1768
+ fieldName: validatorReturnValue,
1769
+ message: validator.rules[j].message
1770
+ });
1771
+ break;
1772
+ }
1773
+ }
1774
+ }
1775
+ return this.errors.length === 0;
1776
+ }
1777
+ }
1778
+
1779
+ /**
1780
+ * @typedef {object} FormValidatorConfig
1781
+ * @property {HTMLElement} [summary] - HTML element to use for error summary
1782
+ */
1783
+ FormValidator.entityMap = {
1784
+ '&': '&amp;',
1785
+ '<': '&lt;',
1786
+ '>': '&gt;',
1787
+ '"': '&quot;',
1788
+ "'": '&#39;',
1789
+ '/': '&#x2F;',
1790
+ '`': '&#x60;',
1791
+ '=': '&#x3D;'
1792
+ };
1793
+
1794
+ /* eslint-disable @typescript-eslint/no-empty-function */
1795
+
1796
+ class MultiFileUpload {
1797
+ /**
1798
+ * @param {MultiFileUploadConfig} [params] - Multi file upload config
1799
+ */
1800
+ constructor(params = {}) {
1801
+ const {
1802
+ container
1803
+ } = params;
1804
+ if (!container || !(container instanceof HTMLElement) || !(dragAndDropSupported() && formDataSupported() && fileApiSupported())) {
1805
+ return this;
1806
+ }
1807
+ this.container = container;
1808
+ this.container.classList.add('moj-multi-file-upload--enhanced');
1809
+ this.defaultParams = {
1810
+ uploadFileEntryHook: () => {},
1811
+ uploadFileExitHook: () => {},
1812
+ uploadFileErrorHook: () => {},
1813
+ fileDeleteHook: () => {},
1814
+ uploadStatusText: 'Uploading files, please wait',
1815
+ dropzoneHintText: 'Drag and drop files here or',
1816
+ dropzoneButtonText: 'Choose files'
1817
+ };
1818
+ this.params = Object.assign({}, this.defaultParams, params);
1819
+ this.feedbackContainer = /** @type {HTMLDivElement} */
1820
+ this.container.querySelector('.moj-multi-file__uploaded-files');
1821
+ this.setupFileInput();
1822
+ this.setupDropzone();
1823
+ this.setupLabel();
1824
+ this.setupStatusBox();
1825
+ this.container.addEventListener('click', this.onFileDeleteClick.bind(this));
1826
+ }
1827
+ setupDropzone() {
1828
+ this.dropzone = document.createElement('div');
1829
+ this.dropzone.classList.add('moj-multi-file-upload__dropzone');
1830
+ this.dropzone.addEventListener('dragover', this.onDragOver.bind(this));
1831
+ this.dropzone.addEventListener('dragleave', this.onDragLeave.bind(this));
1832
+ this.dropzone.addEventListener('drop', this.onDrop.bind(this));
1833
+ this.fileInput.replaceWith(this.dropzone);
1834
+ this.dropzone.appendChild(this.fileInput);
1835
+ }
1836
+ setupLabel() {
1837
+ const label = document.createElement('label');
1838
+ label.setAttribute('for', this.fileInput.id);
1839
+ label.classList.add('govuk-button', 'govuk-button--secondary');
1840
+ label.textContent = this.params.dropzoneButtonText;
1841
+ const hint = document.createElement('p');
1842
+ hint.classList.add('govuk-body');
1843
+ hint.textContent = this.params.dropzoneHintText;
1844
+ this.label = label;
1845
+ this.dropzone.append(hint);
1846
+ this.dropzone.append(label);
1847
+ }
1848
+ setupFileInput() {
1849
+ this.fileInput = /** @type {HTMLInputElement} */
1850
+ this.container.querySelector('.moj-multi-file-upload__input');
1851
+ this.fileInput.addEventListener('change', this.onFileChange.bind(this));
1852
+ this.fileInput.addEventListener('focus', this.onFileFocus.bind(this));
1853
+ this.fileInput.addEventListener('blur', this.onFileBlur.bind(this));
1854
+ }
1855
+ setupStatusBox() {
1856
+ this.status = document.createElement('div');
1857
+ this.status.classList.add('govuk-visually-hidden');
1858
+ this.status.setAttribute('aria-live', 'polite');
1859
+ this.status.setAttribute('role', 'status');
1860
+ this.dropzone.append(this.status);
1861
+ }
1862
+ onDragOver(event) {
1863
+ event.preventDefault();
1864
+ this.dropzone.classList.add('moj-multi-file-upload--dragover');
1865
+ }
1866
+ onDragLeave() {
1867
+ this.dropzone.classList.remove('moj-multi-file-upload--dragover');
1868
+ }
1869
+ onDrop(event) {
1870
+ event.preventDefault();
1871
+ this.dropzone.classList.remove('moj-multi-file-upload--dragover');
1872
+ this.feedbackContainer.classList.remove('moj-hidden');
1873
+ this.status.textContent = this.params.uploadStatusText;
1874
+ this.uploadFiles(event.dataTransfer.files);
1875
+ }
1876
+ uploadFiles(files) {
1877
+ for (const file of Array.from(files)) {
1878
+ this.uploadFile(file);
1879
+ }
1880
+ }
1881
+ onFileChange() {
1882
+ this.feedbackContainer.classList.remove('moj-hidden');
1883
+ this.status.textContent = this.params.uploadStatusText;
1884
+ this.uploadFiles(this.fileInput.files);
1885
+ const fileInput = this.fileInput.cloneNode(true);
1886
+ if (!fileInput || !(fileInput instanceof HTMLInputElement)) {
1887
+ return;
1888
+ }
1889
+ fileInput.value = '';
1890
+ this.fileInput.replaceWith(fileInput);
1891
+ this.setupFileInput();
1892
+ this.fileInput.focus();
1893
+ }
1894
+ onFileFocus() {
1895
+ this.label.classList.add('moj-multi-file-upload--focused');
1896
+ }
1897
+ onFileBlur() {
1898
+ this.label.classList.remove('moj-multi-file-upload--focused');
1899
+ }
1900
+ getSuccessHtml(success) {
1901
+ return `<span class="moj-multi-file-upload__success"> <svg class="moj-banner__icon" fill="currentColor" role="presentation" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 25" height="25" width="25"><path d="M25,6.2L8.7,23.2L0,14.1l4-4.2l4.7,4.9L21,2L25,6.2z"/></svg>${success.messageHtml}</span>`;
1902
+ }
1903
+ getErrorHtml(error) {
1904
+ return `<span class="moj-multi-file-upload__error"> <svg class="moj-banner__icon" fill="currentColor" role="presentation" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 25" height="25" width="25"><path d="M13.6,15.4h-2.3v-4.5h2.3V15.4z M13.6,19.8h-2.3v-2.2h2.3V19.8z M0,23.2h25L12.5,2L0,23.2z"/></svg>${error.message}</span>`;
1905
+ }
1906
+ getFileRow(file) {
1907
+ const row = document.createElement('div');
1908
+ row.classList.add('govuk-summary-list__row', 'moj-multi-file-upload__row');
1909
+ row.innerHTML = `
1910
+ <div class="govuk-summary-list__value moj-multi-file-upload__message">
1911
+ <span class="moj-multi-file-upload__filename">${file.name}</span>
1912
+ <span class="moj-multi-file-upload__progress">0%</span>
1913
+ </div>
1914
+ <div class="govuk-summary-list__actions moj-multi-file-upload__actions"></div>
1915
+ `;
1916
+ return row;
1917
+ }
1918
+ getDeleteButton(file) {
1919
+ const button = document.createElement('button');
1920
+ button.setAttribute('type', 'button');
1921
+ button.setAttribute('name', 'delete');
1922
+ button.setAttribute('value', file.filename);
1923
+ button.classList.add('moj-multi-file-upload__delete', 'govuk-button', 'govuk-button--secondary', 'govuk-!-margin-bottom-0');
1924
+ button.innerHTML = `Delete <span class="govuk-visually-hidden">${file.originalname}</span>`;
1925
+ return button;
1926
+ }
1927
+ uploadFile(file) {
1928
+ this.params.uploadFileEntryHook(this, file);
1929
+ const item = this.getFileRow(file);
1930
+ const message = item.querySelector('.moj-multi-file-upload__message');
1931
+ const actions = item.querySelector('.moj-multi-file-upload__actions');
1932
+ const progress = item.querySelector('.moj-multi-file-upload__progress');
1933
+ const formData = new FormData();
1934
+ formData.append('documents', file);
1935
+ this.feedbackContainer.querySelector('.moj-multi-file-upload__list').append(item);
1936
+ const xhr = new XMLHttpRequest();
1937
+ const onLoad = () => {
1938
+ if (xhr.status < 200 || xhr.status >= 300 || !('success' in xhr.response)) {
1939
+ return onError();
1940
+ }
1941
+ message.innerHTML = this.getSuccessHtml(xhr.response.success);
1942
+ this.status.textContent = xhr.response.success.messageText;
1943
+ actions.append(this.getDeleteButton(xhr.response.file));
1944
+ this.params.uploadFileExitHook(this, file, xhr, xhr.responseText);
1945
+ };
1946
+ const onError = () => {
1947
+ const error = new Error(xhr.response && 'error' in xhr.response ? xhr.response.error.message : xhr.statusText || 'Upload failed');
1948
+ message.innerHTML = this.getErrorHtml(error);
1949
+ this.status.textContent = error.message;
1950
+ this.params.uploadFileErrorHook(this, file, xhr, xhr.responseText, error);
1951
+ };
1952
+ xhr.addEventListener('load', onLoad);
1953
+ xhr.addEventListener('error', onError);
1954
+ xhr.upload.addEventListener('progress', event => {
1955
+ if (!event.lengthComputable) {
1956
+ return;
1957
+ }
1958
+ const percentComplete = Math.round(event.loaded / event.total * 100);
1959
+ progress.textContent = ` ${percentComplete}%`;
1960
+ });
1961
+ xhr.open('POST', this.params.uploadUrl);
1962
+ xhr.responseType = 'json';
1963
+ xhr.send(formData);
1964
+ }
1965
+ onFileDeleteClick(event) {
1966
+ const button = event.target;
1967
+ if (!button || !(button instanceof HTMLButtonElement) || !button.classList.contains('moj-multi-file-upload__delete')) {
1968
+ return;
1969
+ }
1970
+ event.preventDefault(); // if user refreshes page and then deletes
1971
+
1972
+ const xhr = new XMLHttpRequest();
1973
+ xhr.addEventListener('load', () => {
1974
+ if (xhr.status < 200 || xhr.status >= 300) {
1975
+ return;
1976
+ }
1977
+ const rows = Array.from(this.feedbackContainer.querySelectorAll('.moj-multi-file-upload__row'));
1978
+ if (rows.length === 1) {
1979
+ this.feedbackContainer.classList.add('moj-hidden');
1980
+ }
1981
+ const row = rows.find(row => row.contains(button));
1982
+ if (row) row.remove();
1983
+ this.params.fileDeleteHook(this, undefined, xhr, xhr.responseText);
1984
+ });
1985
+ xhr.open('POST', this.params.deleteUrl);
1986
+ xhr.setRequestHeader('Content-Type', 'application/json');
1987
+ xhr.responseType = 'json';
1988
+ xhr.send(JSON.stringify({
1989
+ [button.name]: button.value
1990
+ }));
1991
+ }
1992
+ }
1993
+
1994
+ class MultiSelect {
1995
+ constructor(options) {
1996
+ this.container = options.container;
1997
+ if (this.container.hasAttribute('data-moj-multi-select-init')) {
1998
+ return this;
1999
+ }
2000
+ this.container.setAttribute('data-moj-multi-select-init', '');
2001
+ const idPrefix = options.id_prefix;
2002
+ this.setupToggle(idPrefix);
2003
+ this.toggleButton = this.toggle.querySelector('input');
2004
+ this.toggleButton.addEventListener('click', this.onButtonClick.bind(this));
2005
+ this.container.append(this.toggle);
2006
+ this.checkboxes = Array.from(options.checkboxes);
2007
+ this.checkboxes.forEach(el => el.addEventListener('click', this.onCheckboxClick.bind(this)));
2008
+ this.checked = options.checked || false;
2009
+ }
2010
+ setupToggle(idPrefix = '') {
2011
+ const id = `${idPrefix}checkboxes-all`;
2012
+ const toggle = document.createElement('div');
2013
+ const label = document.createElement('label');
2014
+ const input = document.createElement('input');
2015
+ const span = document.createElement('span');
2016
+ toggle.classList.add('govuk-checkboxes__item', 'govuk-checkboxes--small', 'moj-multi-select__checkbox');
2017
+ input.id = id;
2018
+ input.type = 'checkbox';
2019
+ input.classList.add('govuk-checkboxes__input');
2020
+ label.setAttribute('for', id);
2021
+ label.classList.add('govuk-label', 'govuk-checkboxes__label', 'moj-multi-select__toggle-label');
2022
+ span.classList.add('govuk-visually-hidden');
2023
+ span.textContent = 'Select all';
2024
+ label.append(span);
2025
+ toggle.append(input, label);
2026
+ this.toggle = toggle;
2027
+ }
2028
+ onButtonClick() {
2029
+ if (this.checked) {
2030
+ this.uncheckAll();
2031
+ this.toggleButton.checked = false;
2032
+ } else {
2033
+ this.checkAll();
2034
+ this.toggleButton.checked = true;
2035
+ }
2036
+ }
2037
+ checkAll() {
2038
+ this.checkboxes.forEach(el => {
2039
+ el.checked = true;
2040
+ });
2041
+ this.checked = true;
2042
+ }
2043
+ uncheckAll() {
2044
+ this.checkboxes.forEach(el => {
2045
+ el.checked = false;
2046
+ });
2047
+ this.checked = false;
2048
+ }
2049
+ onCheckboxClick(event) {
2050
+ if (!event.target.checked) {
2051
+ this.toggleButton.checked = false;
2052
+ this.checked = false;
2053
+ } else {
2054
+ if (this.checkboxes.filter(el => el.checked).length === this.checkboxes.length) {
2055
+ this.toggleButton.checked = true;
2056
+ this.checked = true;
2057
+ }
2058
+ }
2059
+ }
2060
+ }
2061
+
2062
+ class PasswordReveal {
2063
+ /**
2064
+ * @param {Element | null} element - HTML element to use for password reveal
2065
+ */
2066
+ constructor(element) {
2067
+ if (!element || !(element instanceof HTMLInputElement)) {
2068
+ return this;
2069
+ }
2070
+ this.el = element;
2071
+ this.container = element.parentElement;
2072
+ if (this.container.hasAttribute('data-moj-password-reveal-init')) {
2073
+ return this;
2074
+ }
2075
+ this.container.setAttribute('data-moj-password-reveal-init', '');
2076
+ this.el.setAttribute('spellcheck', 'false');
2077
+ this.createButton();
2078
+ }
2079
+ createButton() {
2080
+ this.group = document.createElement('div');
2081
+ this.button = document.createElement('button');
2082
+ this.button.setAttribute('type', 'button');
2083
+ this.group.className = 'moj-password-reveal';
2084
+ this.button.className = 'govuk-button govuk-button--secondary moj-password-reveal__button';
2085
+ this.button.innerHTML = 'Show <span class="govuk-visually-hidden">password</span>';
2086
+ this.button.addEventListener('click', this.onButtonClick.bind(this));
2087
+ this.group.append(this.el, this.button);
2088
+ this.container.append(this.group);
2089
+ }
2090
+ onButtonClick() {
2091
+ if (this.el.type === 'password') {
2092
+ this.el.type = 'text';
2093
+ this.button.innerHTML = 'Hide <span class="govuk-visually-hidden">password</span>';
2094
+ } else {
2095
+ this.el.type = 'password';
2096
+ this.button.innerHTML = 'Show <span class="govuk-visually-hidden">password</span>';
2097
+ }
2098
+ }
2099
+ }
2100
+
2101
+ class RichTextEditor {
2102
+ /**
2103
+ * @param {RichTextEditorConfig} options
2104
+ */
2105
+ constructor(options = {}) {
2106
+ const {
2107
+ textarea
2108
+ } = options;
2109
+ if (!textarea || !textarea.parentElement || !(textarea instanceof HTMLTextAreaElement) || !('contentEditable' in document.documentElement)) {
2110
+ return this;
2111
+ }
2112
+ options.toolbar = options.toolbar || {
2113
+ bold: false,
2114
+ italic: false,
2115
+ underline: false,
2116
+ bullets: true,
2117
+ numbers: true
2118
+ };
2119
+ this.textarea = textarea;
2120
+ this.container = this.textarea.parentElement;
2121
+ this.options = options;
2122
+ if (this.container.hasAttribute('data-rich-text-editor-init')) {
2123
+ return this;
2124
+ }
2125
+ this.container.setAttribute('data-rich-text-editor-init', '');
2126
+ this.createToolbar();
2127
+ this.hideDefault();
2128
+ this.configureToolbar();
2129
+ this.keys = {
2130
+ left: 37,
2131
+ right: 39,
2132
+ up: 38,
2133
+ down: 40
2134
+ };
2135
+ this.content.addEventListener('input', this.onEditorInput.bind(this));
2136
+ this.container.querySelector('label').addEventListener('click', this.onLabelClick.bind(this));
2137
+ this.toolbar.addEventListener('keydown', this.onToolbarKeydown.bind(this));
2138
+ }
2139
+ onToolbarKeydown(event) {
2140
+ let focusableButton;
2141
+ switch (event.keyCode) {
2142
+ case this.keys.right:
2143
+ case this.keys.down:
2144
+ {
2145
+ focusableButton = this.buttons.find(button => button.getAttribute('tabindex') === '0');
2146
+ const nextButton = focusableButton.nextElementSibling;
2147
+ if (nextButton instanceof HTMLButtonElement) {
2148
+ nextButton.focus();
2149
+ focusableButton.setAttribute('tabindex', '-1');
2150
+ nextButton.setAttribute('tabindex', '0');
2151
+ }
2152
+ break;
2153
+ }
2154
+ case this.keys.left:
2155
+ case this.keys.up:
2156
+ {
2157
+ focusableButton = this.buttons.find(button => button.getAttribute('tabindex') === '0');
2158
+ const previousButton = focusableButton.previousElementSibling;
2159
+ if (previousButton instanceof HTMLButtonElement) {
2160
+ previousButton.focus();
2161
+ focusableButton.setAttribute('tabindex', '-1');
2162
+ previousButton.setAttribute('tabindex', '0');
2163
+ }
2164
+ break;
2165
+ }
2166
+ }
2167
+ }
2168
+ getToolbarHtml() {
2169
+ let html = '';
2170
+ html += '<div class="moj-rich-text-editor__toolbar" role="toolbar">';
2171
+ if (this.options.toolbar.bold) {
2172
+ html += '<button class="moj-rich-text-editor__toolbar-button moj-rich-text-editor__toolbar-button--bold" type="button" data-command="bold"><span class="govuk-visually-hidden">Bold</span></button>';
2173
+ }
2174
+ if (this.options.toolbar.italic) {
2175
+ html += '<button class="moj-rich-text-editor__toolbar-button moj-rich-text-editor__toolbar-button--italic" type="button" data-command="italic"><span class="govuk-visually-hidden">Italic</span></button>';
2176
+ }
2177
+ if (this.options.toolbar.underline) {
2178
+ html += '<button class="moj-rich-text-editor__toolbar-button moj-rich-text-editor__toolbar-button--underline" type="button" data-command="underline"><span class="govuk-visually-hidden">Underline</span></button>';
2179
+ }
2180
+ if (this.options.toolbar.bullets) {
2181
+ html += '<button class="moj-rich-text-editor__toolbar-button moj-rich-text-editor__toolbar-button--unordered-list" type="button" data-command="insertUnorderedList"><span class="govuk-visually-hidden">Unordered list</span></button>';
2182
+ }
2183
+ if (this.options.toolbar.numbers) {
2184
+ html += '<button class="moj-rich-text-editor__toolbar-button moj-rich-text-editor__toolbar-button--ordered-list" type="button" data-command="insertOrderedList"><span class="govuk-visually-hidden">Ordered list</span></button>';
2185
+ }
2186
+ html += '</div>';
2187
+ return html;
2188
+ }
2189
+ getEnhancedHtml() {
2190
+ return `${this.getToolbarHtml()}<div class="govuk-textarea moj-rich-text-editor__content" contenteditable="true" spellcheck="false"></div>`;
2191
+ }
2192
+ hideDefault() {
2193
+ this.textarea.classList.add('govuk-visually-hidden');
2194
+ this.textarea.setAttribute('aria-hidden', 'true');
2195
+ this.textarea.setAttribute('tabindex', '-1');
2196
+ }
2197
+ createToolbar() {
2198
+ this.toolbar = document.createElement('div');
2199
+ this.toolbar.className = 'moj-rich-text-editor';
2200
+ this.toolbar.innerHTML = this.getEnhancedHtml();
2201
+ this.container.append(this.toolbar);
2202
+ this.content = /** @type {HTMLDivElement} */
2203
+ this.container.querySelector('.moj-rich-text-editor__content');
2204
+ this.content.innerHTML = this.$textarea.value;
2205
+ }
2206
+ configureToolbar() {
2207
+ this.buttons = Array.from(/** @type {NodeListOf<HTMLButtonElement>} */
2208
+ this.container.querySelectorAll('.moj-rich-text-editor__toolbar-button'));
2209
+ this.buttons.forEach((button, index) => {
2210
+ button.setAttribute('tabindex', !index ? '0' : '-1');
2211
+ button.addEventListener('click', this.onButtonClick.bind(this));
2212
+ });
2213
+ }
2214
+ onButtonClick(event) {
2215
+ if (!(event.currentTarget instanceof HTMLElement)) {
2216
+ return;
2217
+ }
2218
+ document.execCommand(event.currentTarget.getAttribute('data-command'), false, undefined);
2219
+ }
2220
+ getContent() {
2221
+ return this.content.innerHTML;
2222
+ }
2223
+ onEditorInput() {
2224
+ this.updateTextarea();
2225
+ }
2226
+ updateTextarea() {
2227
+ document.execCommand('defaultParagraphSeparator', false, 'p');
2228
+ this.textarea.value = this.getContent();
2229
+ }
2230
+ onLabelClick(event) {
2231
+ event.preventDefault();
2232
+ this.content.focus();
2233
+ }
2234
+ }
2235
+
2236
+ class SearchToggle {
2237
+ constructor(options) {
2238
+ this.options = options;
2239
+ this.container = this.options.search.container;
2240
+ this.toggleButtonContainer = this.options.toggleButton.container;
2241
+ if (this.container.hasAttribute('data-moj-search-toggle-init')) {
2242
+ return this;
2243
+ }
2244
+ this.container.setAttribute('data-moj-search-toggle-init', '');
2245
+ const svg = '<svg viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="moj-search-toggle__button__icon"><path d="M7.433,12.5790048 C6.06762625,12.5808611 4.75763941,12.0392925 3.79217348,11.0738265 C2.82670755,10.1083606 2.28513891,8.79837375 2.28699522,7.433 C2.28513891,6.06762625 2.82670755,4.75763941 3.79217348,3.79217348 C4.75763941,2.82670755 6.06762625,2.28513891 7.433,2.28699522 C8.79837375,2.28513891 10.1083606,2.82670755 11.0738265,3.79217348 C12.0392925,4.75763941 12.5808611,6.06762625 12.5790048,7.433 C12.5808611,8.79837375 12.0392925,10.1083606 11.0738265,11.0738265 C10.1083606,12.0392925 8.79837375,12.5808611 7.433,12.5790048 L7.433,12.5790048 Z M14.293,12.579 L13.391,12.579 L13.071,12.269 C14.2300759,10.9245158 14.8671539,9.20813198 14.866,7.433 C14.866,3.32786745 11.5381325,-1.65045755e-15 7.433,-1.65045755e-15 C3.32786745,-1.65045755e-15 -1.65045755e-15,3.32786745 -1.65045755e-15,7.433 C-1.65045755e-15,11.5381325 3.32786745,14.866 7.433,14.866 C9.208604,14.8671159 10.9253982,14.2296624 12.27,13.07 L12.579,13.39 L12.579,14.294 L18.296,20 L20,18.296 L14.294,12.579 L14.293,12.579 Z"></path></svg>';
2246
+ this.toggleButton = document.createElement('button');
2247
+ this.toggleButton.setAttribute('class', 'moj-search-toggle__button');
2248
+ this.toggleButton.setAttribute('type', 'button');
2249
+ this.toggleButton.setAttribute('aria-haspopup', 'true');
2250
+ this.toggleButton.setAttribute('aria-expanded', 'false');
2251
+ this.toggleButton.innerHTML = `${this.options.toggleButton.text} ${svg}`;
2252
+ this.toggleButton.addEventListener('click', this.onToggleButtonClick.bind(this));
2253
+ this.toggleButtonContainer.append(this.toggleButton);
2254
+ document.addEventListener('click', this.onDocumentClick.bind(this));
2255
+ document.addEventListener('focusin', this.onDocumentClick.bind(this));
2256
+ }
2257
+ showMenu() {
2258
+ this.toggleButton.setAttribute('aria-expanded', 'true');
2259
+ this.container.classList.remove('moj-js-hidden');
2260
+ this.container.querySelector('input').focus();
2261
+ }
2262
+ hideMenu() {
2263
+ this.container.classList.add('moj-js-hidden');
2264
+ this.toggleButton.setAttribute('aria-expanded', 'false');
2265
+ }
2266
+ onToggleButtonClick() {
2267
+ if (this.toggleButton.getAttribute('aria-expanded') === 'false') {
2268
+ this.showMenu();
2269
+ } else {
2270
+ this.hideMenu();
2271
+ }
2272
+ }
2273
+ onDocumentClick(event) {
2274
+ if (!this.toggleButtonContainer.contains(event.target) && !this.container.contains(event.target)) {
2275
+ this.hideMenu();
2276
+ }
2277
+ }
2278
+ }
2279
+
2280
+ class SortableTable {
2281
+ constructor(params) {
2282
+ const table = params.table;
2283
+ const head = table == null ? void 0 : table.querySelector('thead');
2284
+ const body = table == null ? void 0 : table.querySelector('tbody');
2285
+ if (!table || !(table instanceof HTMLElement) || !head || !body) {
2286
+ return this;
2287
+ }
2288
+ this.table = table;
2289
+ this.head = head;
2290
+ this.body = body;
2291
+ if (this.table.hasAttribute('data-moj-sortable-table-init')) {
2292
+ return this;
2293
+ }
2294
+ this.table.setAttribute('data-moj-sortable-table-init', '');
2295
+ this.headings = this.head ? Array.from(this.head.querySelectorAll('th')) : [];
2296
+ this.setupOptions(params);
2297
+ this.createHeadingButtons();
2298
+ this.createStatusBox();
2299
+ this.initialiseSortedColumn();
2300
+ this.head.addEventListener('click', this.onSortButtonClick.bind(this));
2301
+ }
2302
+ setupOptions(params) {
2303
+ params = params || {};
2304
+ this.statusMessage = params.statusMessage || 'Sort by %heading% (%direction%)';
2305
+ this.ascendingText = params.ascendingText || 'ascending';
2306
+ this.descendingText = params.descendingText || 'descending';
2307
+ }
2308
+ createHeadingButtons() {
2309
+ for (const heading of this.headings) {
2310
+ if (heading.hasAttribute('aria-sort')) {
2311
+ this.createHeadingButton(heading);
2312
+ }
2313
+ }
2314
+ }
2315
+ createHeadingButton(heading) {
2316
+ const index = this.headings.indexOf(heading);
2317
+ const button = document.createElement('button');
2318
+ button.setAttribute('type', 'button');
2319
+ button.setAttribute('data-index', `${index}`);
2320
+ button.textContent = heading.textContent;
2321
+ heading.textContent = '';
2322
+ heading.appendChild(button);
2323
+ }
2324
+ createStatusBox() {
2325
+ this.status = document.createElement('div');
2326
+ this.status.setAttribute('aria-atomic', 'true');
2327
+ this.status.setAttribute('aria-live', 'polite');
2328
+ this.status.setAttribute('class', 'govuk-visually-hidden');
2329
+ this.status.setAttribute('role', 'status');
2330
+ this.table.insertAdjacentElement('afterend', this.status);
2331
+ }
2332
+ initialiseSortedColumn() {
2333
+ var _sortButton$getAttrib;
2334
+ const rows = this.getTableRowsArray();
2335
+ const heading = this.table.querySelector('th[aria-sort]');
2336
+ const sortButton = heading == null ? void 0 : heading.querySelector('button');
2337
+ const sortDirection = heading == null ? void 0 : heading.getAttribute('aria-sort');
2338
+ const columnNumber = Number.parseInt((_sortButton$getAttrib = sortButton == null ? void 0 : sortButton.getAttribute('data-index')) != null ? _sortButton$getAttrib : '0', 10);
2339
+ if (!heading || !sortButton || !(sortDirection === 'ascending' || sortDirection === 'descending')) {
2340
+ return;
2341
+ }
2342
+ const sortedRows = this.sort(rows, columnNumber, sortDirection);
2343
+ this.addRows(sortedRows);
2344
+ }
2345
+ onSortButtonClick(event) {
2346
+ var _button$getAttribute;
2347
+ const button = event.target;
2348
+ if (!button || !(button instanceof HTMLButtonElement) || !button.parentElement) {
2349
+ return;
2350
+ }
2351
+ const heading = button.parentElement;
2352
+ const sortDirection = heading.getAttribute('aria-sort');
2353
+ const columnNumber = Number.parseInt((_button$getAttribute = button == null ? void 0 : button.getAttribute('data-index')) != null ? _button$getAttribute : '0', 10);
2354
+ const newSortDirection = sortDirection === 'none' || sortDirection === 'descending' ? 'ascending' : 'descending';
2355
+ const rows = this.getTableRowsArray();
2356
+ const sortedRows = this.sort(rows, columnNumber, newSortDirection);
2357
+ this.addRows(sortedRows);
2358
+ this.removeButtonStates();
2359
+ this.updateButtonState(button, newSortDirection);
2360
+ }
2361
+ updateButtonState(button, direction) {
2362
+ if (!(direction === 'ascending' || direction === 'descending')) {
2363
+ return;
2364
+ }
2365
+ button.parentElement.setAttribute('aria-sort', direction);
2366
+ let message = this.statusMessage;
2367
+ message = message.replace(/%heading%/, button.textContent);
2368
+ message = message.replace(/%direction%/, this[`${direction}Text`]);
2369
+ this.status.textContent = message;
2370
+ }
2371
+ removeButtonStates() {
2372
+ for (const heading of this.headings) {
2373
+ heading.setAttribute('aria-sort', 'none');
2374
+ }
2375
+ }
2376
+ addRows(rows) {
2377
+ for (const row of rows) {
2378
+ this.body.append(row);
2379
+ }
2380
+ }
2381
+ getTableRowsArray() {
2382
+ return Array.from(this.body.querySelectorAll('tr'));
2383
+ }
2384
+ sort(rows, columnNumber, sortDirection) {
2385
+ return rows.sort((rowA, rowB) => {
2386
+ const tdA = rowA.querySelectorAll('td, th')[columnNumber];
2387
+ const tdB = rowB.querySelectorAll('td, th')[columnNumber];
2388
+ if (!tdA || !tdB || !(tdA instanceof HTMLElement) || !(tdB instanceof HTMLElement)) {
2389
+ return 0;
2390
+ }
2391
+ const valueA = sortDirection === 'ascending' ? this.getCellValue(tdA) : this.getCellValue(tdB);
2392
+ const valueB = sortDirection === 'ascending' ? this.getCellValue(tdB) : this.getCellValue(tdA);
2393
+ return !(typeof valueA === 'number' && typeof valueB === 'number') ? valueA.toString().localeCompare(valueB.toString()) : valueA - valueB;
2394
+ });
2395
+ }
2396
+ getCellValue(cell) {
2397
+ const val = cell.getAttribute('data-sort-value') || cell.innerHTML;
2398
+ const valAsNumber = Number(val);
2399
+ return Number.isFinite(valAsNumber) ? valAsNumber // Exclude invalid numbers, infinity etc
2400
+ : val;
2401
+ }
2402
+ }
2403
+
2404
+ const version = '0.0.0-development';
2405
+
2406
+ /* eslint-disable no-new */
2407
+
2408
+
2409
+ /**
2410
+ * @param {Config} [config]
2411
+ */
2412
+ function initAll(config) {
2413
+ // Set the config to an empty object by default if no config is passed.
2414
+ config = typeof config !== 'undefined' ? config : {};
2415
+
2416
+ // Allow the user to initialise MOJ Frontend in only certain sections of the page
2417
+ // Defaults to the entire document if nothing is set.
2418
+ const scope = typeof config.scope !== 'undefined' ? config.scope : document;
2419
+ const $addAnothers = scope.querySelectorAll('[data-module="moj-add-another"]');
2420
+ $addAnothers.forEach($addAnother => {
2421
+ new AddAnother($addAnother);
2422
+ });
2423
+ const $multiSelects = scope.querySelectorAll('[data-module="moj-multi-select"]');
2424
+ $multiSelects.forEach($multiSelect => {
2425
+ const containerSelector = $multiSelect.getAttribute('data-multi-select-checkbox');
2426
+ if (!($multiSelect instanceof HTMLElement) || !containerSelector) {
2427
+ return;
2428
+ }
2429
+ new MultiSelect({
2430
+ container: $multiSelect.querySelector(containerSelector),
2431
+ checkboxes: $multiSelect.querySelectorAll('tbody .govuk-checkboxes__input'),
2432
+ id_prefix: $multiSelect.getAttribute('data-multi-select-idprefix')
2433
+ });
2434
+ });
2435
+ const $passwordReveals = scope.querySelectorAll('[data-module="moj-password-reveal"]');
2436
+ $passwordReveals.forEach($passwordReveal => {
2437
+ new PasswordReveal($passwordReveal);
2438
+ });
2439
+ const $richTextEditors = scope.querySelectorAll('[data-module="moj-rich-text-editor"]');
2440
+ $richTextEditors.forEach($richTextEditor => {
2441
+ const options = {
2442
+ textarea: $richTextEditor
2443
+ };
2444
+ const toolbarAttr = $richTextEditor.getAttribute('data-moj-rich-text-editor-toolbar');
2445
+ if (toolbarAttr) {
2446
+ const toolbar = toolbarAttr.split(',');
2447
+ options.toolbar = {};
2448
+ for (const option of toolbar) {
2449
+ if (option === 'bold' || option === 'italic' || option === 'underline' || option === 'bullets' || option === 'numbers') {
2450
+ options.toolbar[option] = true;
2451
+ }
2452
+ }
2453
+ }
2454
+ new RichTextEditor(options);
2455
+ });
2456
+ const $searchToggles = scope.querySelectorAll('[data-module="moj-search-toggle"]');
2457
+ $searchToggles.forEach($searchToggle => {
2458
+ new SearchToggle({
2459
+ toggleButton: {
2460
+ container: $searchToggle.querySelector('.moj-search-toggle__toggle'),
2461
+ text: $searchToggle.getAttribute('data-moj-search-toggle-text')
2462
+ },
2463
+ search: {
2464
+ container: $searchToggle.querySelector('.moj-search')
2465
+ }
2466
+ });
2467
+ });
2468
+ const $sortableTables = scope.querySelectorAll('[data-module="moj-sortable-table"]');
2469
+ $sortableTables.forEach($table => {
2470
+ new SortableTable({
2471
+ table: $table
2472
+ });
2473
+ });
2474
+ const $datePickers = scope.querySelectorAll('[data-module="moj-date-picker"]');
2475
+ $datePickers.forEach($datePicker => {
2476
+ new DatePicker($datePicker);
2477
+ });
2478
+ const $buttonMenus = scope.querySelectorAll('[data-module="moj-button-menu"]');
2479
+ $buttonMenus.forEach($buttonmenu => {
2480
+ new ButtonMenu($buttonmenu);
2481
+ });
2482
+ const $alerts = scope.querySelectorAll('[data-module="moj-alert"]');
2483
+ $alerts.forEach($alert => {
2484
+ new Alert($alert);
2485
+ });
2486
+ }
2487
+
2488
+ /**
2489
+ * @typedef {object} Config
2490
+ * @property {Element} [scope=document] - Scope to query for components
2491
+ */
2492
+
2493
+ /**
2494
+ * Schema for component config
2495
+ *
2496
+ * @typedef {object} Schema
2497
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
2498
+ */
2499
+
2500
+ /**
2501
+ * Schema property for component config
2502
+ *
2503
+ * @typedef {object} SchemaProperty
2504
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
2505
+ */
2506
+
2507
+ exports.AddAnother = AddAnother;
2508
+ exports.Alert = Alert;
2509
+ exports.ButtonMenu = ButtonMenu;
2510
+ exports.DatePicker = DatePicker;
2511
+ exports.FilterToggleButton = FilterToggleButton;
2512
+ exports.FormValidator = FormValidator;
2513
+ exports.MultiFileUpload = MultiFileUpload;
2514
+ exports.MultiSelect = MultiSelect;
2515
+ exports.PasswordReveal = PasswordReveal;
2516
+ exports.RichTextEditor = RichTextEditor;
2517
+ exports.SearchToggle = SearchToggle;
2518
+ exports.SortableTable = SortableTable;
2519
+ exports.initAll = initAll;
2520
+ exports.version = version;
2521
+
2522
+ }));
2523
+ //# sourceMappingURL=all.bundle.js.map