@ministryofjustice/frontend 4.0.1 → 5.0.0

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