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