@ministryofjustice/frontend 4.0.0 → 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 (245) 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 +10 -3
  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/page-header-actions/template.njk +1 -1
  107. package/moj/components/pagination/_pagination.scss +2 -2
  108. package/moj/components/pagination/_pagination.scss.map +1 -0
  109. package/moj/components/password-reveal/_password-reveal.scss +2 -0
  110. package/moj/components/password-reveal/_password-reveal.scss.map +1 -0
  111. package/moj/components/password-reveal/password-reveal.bundle.js +49 -0
  112. package/moj/components/password-reveal/password-reveal.bundle.js.map +1 -0
  113. package/moj/components/password-reveal/password-reveal.bundle.mjs +41 -0
  114. package/moj/components/password-reveal/password-reveal.bundle.mjs.map +1 -0
  115. package/moj/components/password-reveal/password-reveal.mjs +36 -31
  116. package/moj/components/password-reveal/password-reveal.mjs.map +1 -1
  117. package/moj/components/primary-navigation/_primary-navigation.scss +2 -0
  118. package/moj/components/primary-navigation/_primary-navigation.scss.map +1 -0
  119. package/moj/components/progress-bar/_progress-bar.scss +2 -0
  120. package/moj/components/progress-bar/_progress-bar.scss.map +1 -0
  121. package/moj/components/rich-text-editor/README.md +15 -9
  122. package/moj/components/rich-text-editor/_rich-text-editor.scss +2 -0
  123. package/moj/components/rich-text-editor/_rich-text-editor.scss.map +1 -0
  124. package/moj/components/rich-text-editor/rich-text-editor.bundle.js +145 -0
  125. package/moj/components/rich-text-editor/rich-text-editor.bundle.js.map +1 -0
  126. package/moj/components/rich-text-editor/rich-text-editor.bundle.mjs +137 -0
  127. package/moj/components/rich-text-editor/rich-text-editor.bundle.mjs.map +1 -0
  128. package/moj/components/rich-text-editor/rich-text-editor.mjs +124 -145
  129. package/moj/components/rich-text-editor/rich-text-editor.mjs.map +1 -1
  130. package/moj/components/search/_search.scss +2 -0
  131. package/moj/components/search/_search.scss.map +1 -0
  132. package/moj/components/search-toggle/{search-toggle.scss → _search-toggle.scss} +2 -0
  133. package/moj/components/search-toggle/_search-toggle.scss.map +1 -0
  134. package/moj/components/search-toggle/search-toggle.bundle.js +54 -0
  135. package/moj/components/search-toggle/search-toggle.bundle.js.map +1 -0
  136. package/moj/components/search-toggle/search-toggle.bundle.mjs +46 -0
  137. package/moj/components/search-toggle/search-toggle.bundle.mjs.map +1 -0
  138. package/moj/components/search-toggle/search-toggle.mjs +40 -49
  139. package/moj/components/search-toggle/search-toggle.mjs.map +1 -1
  140. package/moj/components/side-navigation/_side-navigation.scss +2 -0
  141. package/moj/components/side-navigation/_side-navigation.scss.map +1 -0
  142. package/moj/components/sortable-table/_sortable-table.scss +2 -2
  143. package/moj/components/sortable-table/_sortable-table.scss.map +1 -0
  144. package/moj/components/sortable-table/sortable-table.bundle.js +134 -0
  145. package/moj/components/sortable-table/sortable-table.bundle.js.map +1 -0
  146. package/moj/components/sortable-table/sortable-table.bundle.mjs +126 -0
  147. package/moj/components/sortable-table/sortable-table.bundle.mjs.map +1 -0
  148. package/moj/components/sortable-table/sortable-table.mjs +117 -130
  149. package/moj/components/sortable-table/sortable-table.mjs.map +1 -1
  150. package/moj/components/sub-navigation/_sub-navigation.scss +2 -0
  151. package/moj/components/sub-navigation/_sub-navigation.scss.map +1 -0
  152. package/moj/components/tag/_tag.scss +2 -0
  153. package/moj/components/tag/_tag.scss.map +1 -0
  154. package/moj/components/task-list/_task-list.scss +2 -0
  155. package/moj/components/task-list/_task-list.scss.map +1 -0
  156. package/moj/components/ticket-panel/_ticket-panel.scss +2 -0
  157. package/moj/components/ticket-panel/_ticket-panel.scss.map +1 -0
  158. package/moj/components/timeline/_timeline.scss +2 -0
  159. package/moj/components/timeline/_timeline.scss.map +1 -0
  160. package/moj/filters/all.js +44 -22
  161. package/moj/helpers/_all.scss +2 -0
  162. package/moj/helpers/_all.scss.map +1 -0
  163. package/moj/helpers/_hidden.scss +2 -0
  164. package/moj/helpers/_hidden.scss.map +1 -0
  165. package/moj/helpers/_links.scss +2 -0
  166. package/moj/helpers/_links.scss.map +1 -0
  167. package/moj/{helpers.js → helpers.bundle.js} +37 -42
  168. package/moj/helpers.bundle.js.map +1 -0
  169. package/moj/helpers.bundle.mjs +179 -0
  170. package/moj/helpers.bundle.mjs.map +1 -0
  171. package/moj/helpers.mjs +52 -28
  172. package/moj/helpers.mjs.map +1 -1
  173. package/moj/init.js +11 -2
  174. package/moj/moj-frontend.min.css +1 -1
  175. package/moj/moj-frontend.min.css.map +1 -1
  176. package/moj/moj-frontend.min.js +1 -1
  177. package/moj/moj-frontend.min.js.map +1 -1
  178. package/moj/objects/_all.scss +2 -0
  179. package/moj/objects/_all.scss.map +1 -0
  180. package/moj/objects/_button-group.scss +17 -1
  181. package/moj/objects/_button-group.scss.map +1 -0
  182. package/moj/objects/_filter-layout.scss +2 -0
  183. package/moj/objects/_filter-layout.scss.map +1 -0
  184. package/moj/objects/_scrollable-pane.scss +2 -0
  185. package/moj/objects/_scrollable-pane.scss.map +1 -0
  186. package/moj/objects/_width-container.scss +2 -0
  187. package/moj/objects/_width-container.scss.map +1 -0
  188. package/moj/settings/_all.scss +2 -0
  189. package/moj/settings/_all.scss.map +1 -0
  190. package/moj/settings/_assets.scss +2 -0
  191. package/moj/settings/_assets.scss.map +1 -0
  192. package/moj/settings/_colours.scss +2 -0
  193. package/moj/settings/_colours.scss.map +1 -0
  194. package/moj/settings/_measurements.scss +2 -0
  195. package/moj/settings/_measurements.scss.map +1 -0
  196. package/moj/settings/_typography.scss +2 -0
  197. package/moj/settings/_typography.scss.map +1 -0
  198. package/moj/template.njk +13 -0
  199. package/moj/utilities/_all.scss +2 -0
  200. package/moj/utilities/_all.scss.map +1 -0
  201. package/moj/utilities/_hidden.scss +2 -0
  202. package/moj/utilities/_hidden.scss.map +1 -0
  203. package/moj/utilities/_width-container.scss +2 -0
  204. package/moj/utilities/_width-container.scss.map +1 -0
  205. package/moj/vendor/govuk-frontend/_base.scss +2 -0
  206. package/moj/vendor/govuk-frontend/_base.scss.map +1 -0
  207. package/moj/vendor/govuk-frontend/_index.scss +2 -0
  208. package/moj/vendor/govuk-frontend/_index.scss.map +1 -0
  209. package/moj/{version.js → version.bundle.js} +1 -1
  210. package/moj/version.bundle.js.map +1 -0
  211. package/moj/version.bundle.mjs +4 -0
  212. package/moj/version.bundle.mjs.map +1 -0
  213. package/moj/version.mjs.map +1 -1
  214. package/package.json +5 -6
  215. package/moj/all.jquery.min.js +0 -1
  216. package/moj/all.jquery.min.js.map +0 -1
  217. package/moj/all.js +0 -2662
  218. package/moj/all.js.map +0 -1
  219. package/moj/components/add-another/add-another.js +0 -115
  220. package/moj/components/add-another/add-another.js.map +0 -1
  221. package/moj/components/alert/alert.js +0 -356
  222. package/moj/components/alert/alert.js.map +0 -1
  223. package/moj/components/alert/alert.spec.helper.js.map +0 -1
  224. package/moj/components/button-menu/button-menu.js.map +0 -1
  225. package/moj/components/date-picker/date-picker.js.map +0 -1
  226. package/moj/components/filter-toggle-button/filter-toggle-button.js +0 -102
  227. package/moj/components/filter-toggle-button/filter-toggle-button.js.map +0 -1
  228. package/moj/components/form-validator/form-validator.js +0 -205
  229. package/moj/components/form-validator/form-validator.js.map +0 -1
  230. package/moj/components/multi-file-upload/multi-file-upload.js +0 -241
  231. package/moj/components/multi-file-upload/multi-file-upload.js.map +0 -1
  232. package/moj/components/multi-select/multi-select.js +0 -86
  233. package/moj/components/multi-select/multi-select.js.map +0 -1
  234. package/moj/components/password-reveal/password-reveal.js +0 -44
  235. package/moj/components/password-reveal/password-reveal.js.map +0 -1
  236. package/moj/components/rich-text-editor/rich-text-editor.js +0 -166
  237. package/moj/components/rich-text-editor/rich-text-editor.js.map +0 -1
  238. package/moj/components/search-toggle/search-toggle.js +0 -63
  239. package/moj/components/search-toggle/search-toggle.js.map +0 -1
  240. package/moj/components/sortable-table/sortable-table.js +0 -147
  241. package/moj/components/sortable-table/sortable-table.js.map +0 -1
  242. package/moj/helpers.js.map +0 -1
  243. package/moj/vendor/html5shiv.js +0 -326
  244. package/moj/vendor/jquery.js +0 -9300
  245. package/moj/version.js.map +0 -1
@@ -1,236 +1,152 @@
1
- /**
2
- * Date picker config
3
- *
4
- * @typedef {object} DatePickerConfig
5
- * @property {string} [excludedDates] - Dates that cannot be selected
6
- * @property {string} [excludedDays] - Days that cannot be selected
7
- * @property {boolean} [leadingZeroes] - Whether to add leading zeroes when populating the field
8
- * @property {string} [minDate] - The earliest available date
9
- * @property {string} [maxDate] - The latest available date
10
- * @property {string} [weekStartDay] - First day of the week in calendar view
11
- */
12
-
13
- /**
14
- * @param {HTMLElement} $module - HTML element
15
- * @param {DatePickerConfig} config - config object
16
- * @class
17
- */
18
- function DatePicker($module, config = {}) {
19
- if (!$module) {
20
- return this
21
- }
22
-
23
- const schema = Object.freeze({
24
- properties: {
25
- excludedDates: { type: 'string' },
26
- excludedDays: { type: 'string' },
27
- leadingZeros: { type: 'string' },
28
- maxDate: { type: 'string' },
29
- minDate: { type: 'string' },
30
- weekStartDay: { type: 'string' }
31
- }
32
- });
33
-
34
- const defaults = {
35
- leadingZeros: false,
36
- weekStartDay: 'monday'
37
- };
38
-
39
- // data attributes override JS config, which overrides defaults
40
- this.config = this.mergeConfigs(
41
- defaults,
42
- config,
43
- this.parseDataset(schema, $module.dataset)
44
- );
45
-
46
- this.dayLabels = [
47
- 'Monday',
48
- 'Tuesday',
49
- 'Wednesday',
50
- 'Thursday',
51
- 'Friday',
52
- 'Saturday',
53
- 'Sunday'
54
- ];
55
-
56
- this.monthLabels = [
57
- 'January',
58
- 'February',
59
- 'March',
60
- 'April',
61
- 'May',
62
- 'June',
63
- 'July',
64
- 'August',
65
- 'September',
66
- 'October',
67
- 'November',
68
- 'December'
69
- ];
70
-
71
- this.currentDate = new Date();
72
- this.currentDate.setHours(0, 0, 0, 0);
73
- this.calendarDays = [];
74
- this.excludedDates = [];
75
- this.excludedDays = [];
76
-
77
- this.buttonClass = 'moj-datepicker__button';
78
- this.selectedDayButtonClass = 'moj-datepicker__button--selected';
79
- this.currentDayButtonClass = 'moj-datepicker__button--current';
80
- this.todayButtonClass = 'moj-datepicker__button--today';
81
-
82
- this.$module = $module;
83
- this.$input = $module.querySelector('.moj-js-datepicker-input');
84
- }
85
-
86
- DatePicker.prototype.init = function () {
87
- // Check that required elements are present
88
- if (!this.$input) {
89
- return
90
- }
91
- if (this.$module.dataset.initialized) {
92
- return
93
- }
94
-
95
- this.setOptions();
96
- this.initControls();
97
- this.$module.setAttribute('data-initialized', 'true');
98
- };
99
-
100
- DatePicker.prototype.initControls = function () {
101
- this.id = `datepicker-${this.$input.id}`;
102
-
103
- this.$dialog = this.createDialog();
104
- this.createCalendarHeaders();
105
-
106
- const $componentWrapper = document.createElement('div');
107
- const $inputWrapper = document.createElement('div');
108
- $componentWrapper.classList.add('moj-datepicker__wrapper');
109
- $inputWrapper.classList.add('govuk-input__wrapper');
110
-
111
- this.$input.parentNode.insertBefore($componentWrapper, this.$input);
112
- $componentWrapper.appendChild($inputWrapper);
113
- $inputWrapper.appendChild(this.$input);
114
-
115
- $inputWrapper.insertAdjacentHTML('beforeend', this.toggleTemplate());
116
- $componentWrapper.insertAdjacentElement('beforeend', this.$dialog);
117
-
118
- this.$calendarButton = this.$module.querySelector('.moj-js-datepicker-toggle');
119
- this.$dialogTitle = this.$dialog.querySelector(
120
- '.moj-js-datepicker-month-year'
121
- );
122
-
123
- this.createCalendar();
124
-
125
- this.$prevMonthButton = this.$dialog.querySelector(
126
- '.moj-js-datepicker-prev-month'
127
- );
128
- this.$prevYearButton = this.$dialog.querySelector(
129
- '.moj-js-datepicker-prev-year'
130
- );
131
- this.$nextMonthButton = this.$dialog.querySelector(
132
- '.moj-js-datepicker-next-month'
133
- );
134
- this.$nextYearButton = this.$dialog.querySelector(
135
- '.moj-js-datepicker-next-year'
136
- );
137
- this.$cancelButton = this.$dialog.querySelector('.moj-js-datepicker-cancel');
138
- this.$okButton = this.$dialog.querySelector('.moj-js-datepicker-ok');
139
-
140
- // add event listeners
141
- this.$prevMonthButton.addEventListener('click', (event) =>
142
- this.focusPreviousMonth(event, false)
143
- );
144
- this.$prevYearButton.addEventListener('click', (event) =>
145
- this.focusPreviousYear(event, false)
146
- );
147
- this.$nextMonthButton.addEventListener('click', (event) =>
148
- this.focusNextMonth(event, false)
149
- );
150
- this.$nextYearButton.addEventListener('click', (event) =>
151
- this.focusNextYear(event, false)
152
- );
153
- this.$cancelButton.addEventListener('click', (event) => {
154
- event.preventDefault();
155
- this.closeDialog(event);
156
- });
157
- this.$okButton.addEventListener('click', () => {
158
- this.selectDate(this.currentDate);
159
- });
160
-
161
- const dialogButtons = this.$dialog.querySelectorAll(
162
- 'button:not([disabled="true"])'
163
- );
164
- // eslint-disable-next-line prefer-destructuring
165
- this.$firstButtonInDialog = dialogButtons[0];
166
- this.$lastButtonInDialog = dialogButtons[dialogButtons.length - 1];
167
- this.$firstButtonInDialog.addEventListener('keydown', (event) =>
168
- this.firstButtonKeydown(event)
169
- );
170
- this.$lastButtonInDialog.addEventListener('keydown', (event) =>
171
- this.lastButtonKeydown(event)
172
- );
173
-
174
- this.$calendarButton.addEventListener('click', (event) =>
175
- this.toggleDialog(event)
176
- );
177
-
178
- this.$dialog.addEventListener('keydown', (event) => {
179
- if (event.key === 'Escape') {
180
- this.closeDialog();
181
- event.preventDefault();
182
- event.stopPropagation();
1
+ class DatePicker {
2
+ /**
3
+ * @param {Element | null} $module - HTML element to use for date picker
4
+ * @param {DatePickerConfig} [config] - Date picker config
5
+ */
6
+ constructor($module, config = {}) {
7
+ if (!$module || !($module instanceof HTMLElement)) {
8
+ return this;
183
9
  }
184
- });
185
-
186
- document.body.addEventListener('mouseup', (event) =>
187
- this.backgroundClick(event)
188
- );
189
-
190
- // populates calendar with initial dates, avoids Wave errors about null buttons
191
- this.updateCalendar();
192
- };
193
-
194
- DatePicker.prototype.createDialog = function () {
195
- const titleId = `datepicker-title-${this.$input.id}`;
196
- const $dialog = document.createElement('div');
197
-
198
- $dialog.id = this.id;
199
- $dialog.setAttribute('class', 'moj-datepicker__dialog');
200
- $dialog.setAttribute('role', 'dialog');
201
- $dialog.setAttribute('aria-modal', 'true');
202
- $dialog.setAttribute('aria-labelledby', titleId);
203
- $dialog.innerHTML = this.dialogTemplate(titleId);
204
- $dialog.hidden = true;
205
-
206
- return $dialog
207
- };
208
-
209
- DatePicker.prototype.createCalendar = function () {
210
- const $tbody = this.$dialog.querySelector('tbody');
211
- let dayCount = 0;
212
- for (let i = 0; i < 6; i++) {
213
- // create row
214
- const $row = $tbody.insertRow(i);
10
+ const $input = $module.querySelector('.moj-js-datepicker-input');
215
11
 
216
- for (let j = 0; j < 7; j++) {
217
- // create cell (day)
218
- const $cell = document.createElement('td');
219
- const $dateButton = document.createElement('button');
220
-
221
- $cell.appendChild($dateButton);
222
- $row.appendChild($cell);
12
+ // Check that required elements are present
13
+ if (!$input || !($input instanceof HTMLInputElement)) {
14
+ return this;
15
+ }
16
+ this.$module = $module;
17
+ this.$input = $input;
18
+ const schema = Object.freeze({
19
+ properties: {
20
+ excludedDates: {
21
+ type: 'string'
22
+ },
23
+ excludedDays: {
24
+ type: 'string'
25
+ },
26
+ leadingZeros: {
27
+ type: 'string'
28
+ },
29
+ maxDate: {
30
+ type: 'string'
31
+ },
32
+ minDate: {
33
+ type: 'string'
34
+ },
35
+ weekStartDay: {
36
+ type: 'string'
37
+ }
38
+ }
39
+ });
40
+ const defaults = {
41
+ leadingZeros: false,
42
+ weekStartDay: 'monday'
43
+ };
44
+
45
+ // data attributes override JS config, which overrides defaults
46
+ this.config = this.mergeConfigs(defaults, config, this.parseDataset(schema, $module.dataset));
47
+ this.dayLabels = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
48
+ this.monthLabels = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
49
+ this.currentDate = new Date();
50
+ this.currentDate.setHours(0, 0, 0, 0);
51
+ this.calendarDays = [];
52
+ this.excludedDates = [];
53
+ this.excludedDays = [];
54
+ this.buttonClass = 'moj-datepicker__button';
55
+ this.selectedDayButtonClass = 'moj-datepicker__button--selected';
56
+ this.currentDayButtonClass = 'moj-datepicker__button--current';
57
+ this.todayButtonClass = 'moj-datepicker__button--today';
58
+ if (this.$module.dataset.initialized) {
59
+ return this;
60
+ }
61
+ this.setOptions();
62
+ this.initControls();
63
+ this.$module.setAttribute('data-initialized', 'true');
64
+ }
65
+ initControls() {
66
+ this.id = `datepicker-${this.$input.id}`;
67
+ this.$dialog = this.createDialog();
68
+ this.createCalendarHeaders();
69
+ const $componentWrapper = document.createElement('div');
70
+ const $inputWrapper = document.createElement('div');
71
+ $componentWrapper.classList.add('moj-datepicker__wrapper');
72
+ $inputWrapper.classList.add('govuk-input__wrapper');
73
+ this.$input.parentElement.insertBefore($componentWrapper, this.$input);
74
+ $componentWrapper.appendChild($inputWrapper);
75
+ $inputWrapper.appendChild(this.$input);
76
+ $inputWrapper.insertAdjacentHTML('beforeend', this.toggleTemplate());
77
+ $componentWrapper.insertAdjacentElement('beforeend', this.$dialog);
78
+ this.$calendarButton = this.$module.querySelector('.moj-js-datepicker-toggle');
79
+ this.$dialogTitle = this.$dialog.querySelector('.moj-js-datepicker-month-year');
80
+ this.createCalendar();
81
+ this.$prevMonthButton = this.$dialog.querySelector('.moj-js-datepicker-prev-month');
82
+ this.$prevYearButton = this.$dialog.querySelector('.moj-js-datepicker-prev-year');
83
+ this.$nextMonthButton = this.$dialog.querySelector('.moj-js-datepicker-next-month');
84
+ this.$nextYearButton = this.$dialog.querySelector('.moj-js-datepicker-next-year');
85
+ this.$cancelButton = this.$dialog.querySelector('.moj-js-datepicker-cancel');
86
+ this.$okButton = this.$dialog.querySelector('.moj-js-datepicker-ok');
87
+
88
+ // add event listeners
89
+ this.$prevMonthButton.addEventListener('click', event => this.focusPreviousMonth(event, false));
90
+ this.$prevYearButton.addEventListener('click', event => this.focusPreviousYear(event, false));
91
+ this.$nextMonthButton.addEventListener('click', event => this.focusNextMonth(event, false));
92
+ this.$nextYearButton.addEventListener('click', event => this.focusNextYear(event, false));
93
+ this.$cancelButton.addEventListener('click', event => {
94
+ event.preventDefault();
95
+ this.closeDialog();
96
+ });
97
+ this.$okButton.addEventListener('click', () => {
98
+ this.selectDate(this.currentDate);
99
+ });
100
+ const dialogButtons = this.$dialog.querySelectorAll('button:not([disabled="true"])');
101
+ this.$firstButtonInDialog = dialogButtons[0];
102
+ this.$lastButtonInDialog = dialogButtons[dialogButtons.length - 1];
103
+ this.$firstButtonInDialog.addEventListener('keydown', event => this.firstButtonKeydown(event));
104
+ this.$lastButtonInDialog.addEventListener('keydown', event => this.lastButtonKeydown(event));
105
+ this.$calendarButton.addEventListener('click', event => this.toggleDialog(event));
106
+ this.$dialog.addEventListener('keydown', event => {
107
+ if (event.key === 'Escape') {
108
+ this.closeDialog();
109
+ event.preventDefault();
110
+ event.stopPropagation();
111
+ }
112
+ });
113
+ document.body.addEventListener('mouseup', event => this.backgroundClick(event));
223
114
 
224
- const calendarDay = new DSCalendarDay($dateButton, dayCount, i, j, this);
225
- calendarDay.init();
226
- this.calendarDays.push(calendarDay);
227
- dayCount++;
115
+ // populates calendar with initial dates, avoids Wave errors about null buttons
116
+ this.updateCalendar();
117
+ }
118
+ createDialog() {
119
+ const titleId = `datepicker-title-${this.$input.id}`;
120
+ const $dialog = document.createElement('div');
121
+ $dialog.id = this.id;
122
+ $dialog.setAttribute('class', 'moj-datepicker__dialog');
123
+ $dialog.setAttribute('role', 'dialog');
124
+ $dialog.setAttribute('aria-modal', 'true');
125
+ $dialog.setAttribute('aria-labelledby', titleId);
126
+ $dialog.innerHTML = this.dialogTemplate(titleId);
127
+ $dialog.hidden = true;
128
+ return $dialog;
129
+ }
130
+ createCalendar() {
131
+ const $tbody = this.$dialog.querySelector('tbody');
132
+ let dayCount = 0;
133
+ for (let i = 0; i < 6; i++) {
134
+ // create row
135
+ const $row = $tbody.insertRow(i);
136
+ for (let j = 0; j < 7; j++) {
137
+ // create cell (day)
138
+ const $cell = document.createElement('td');
139
+ const $dateButton = document.createElement('button');
140
+ $cell.appendChild($dateButton);
141
+ $row.appendChild($cell);
142
+ const calendarDay = new DSCalendarDay($dateButton, dayCount, i, j, this);
143
+ this.calendarDays.push(calendarDay);
144
+ dayCount++;
145
+ }
228
146
  }
229
147
  }
230
- };
231
-
232
- DatePicker.prototype.toggleTemplate = function () {
233
- return `<button class="moj-datepicker__toggle moj-js-datepicker-toggle" type="button" aria-haspopup="dialog" aria-controls="${this.id}" aria-expanded="false">
148
+ toggleTemplate() {
149
+ return `<button class="moj-datepicker__toggle moj-js-datepicker-toggle" type="button" aria-haspopup="dialog" aria-controls="${this.id}" aria-expanded="false">
234
150
  <span class="govuk-visually-hidden">Choose date</span>
235
151
  <svg width="32" height="24" focusable="false" class="moj-datepicker-icon" aria-hidden="true" role="img" viewBox="0 0 22 22">
236
152
  <path
@@ -242,17 +158,17 @@ DatePicker.prototype.toggleTemplate = function () {
242
158
  <rect x="3.66669" width="1.46667" height="5.13333" rx="0.733333" fill="currentColor"></rect>
243
159
  <rect x="16.8667" width="1.46667" height="5.13333" rx="0.733333" fill="currentColor"></rect>
244
160
  </svg>
245
- </button>`
246
- };
161
+ </button>`;
162
+ }
247
163
 
248
- /**
249
- * HTML template for calendar dialog
250
- *
251
- * @param {string} [titleId] - Id attribute for dialog title
252
- * @returns {string}
253
- */
254
- DatePicker.prototype.dialogTemplate = function (titleId) {
255
- return `<div class="moj-datepicker__dialog-header">
164
+ /**
165
+ * HTML template for calendar dialog
166
+ *
167
+ * @param {string} [titleId] - Id attribute for dialog title
168
+ * @returns {string}
169
+ */
170
+ dialogTemplate(titleId) {
171
+ return `<div class="moj-datepicker__dialog-header">
256
172
  <div class="moj-datepicker__dialog-navbuttons">
257
173
  <button class="moj-datepicker__button moj-js-datepicker-prev-year">
258
174
  <span class="govuk-visually-hidden">Previous year</span>
@@ -301,661 +217,559 @@ DatePicker.prototype.dialogTemplate = function (titleId) {
301
217
  <div class="govuk-button-group">
302
218
  <button type="button" class="govuk-button moj-js-datepicker-ok">Select</button>
303
219
  <button type="button" class="govuk-button govuk-button--secondary moj-js-datepicker-cancel">Close</button>
304
- </div>`
305
- };
306
-
307
- DatePicker.prototype.createCalendarHeaders = function () {
308
- this.dayLabels.forEach((day) => {
309
- const html = `<th scope="col"><span aria-hidden="true">${day.substring(0, 3)}</span><span class="govuk-visually-hidden">${day}</span></th>`;
310
- const $headerRow = this.$dialog.querySelector('thead > tr');
311
- $headerRow.insertAdjacentHTML('beforeend', html);
312
- });
313
- };
314
-
315
- /**
316
- * Pads given number with leading zeros
317
- *
318
- * @param {number} value - The value to be padded
319
- * @param {number} length - The length in characters of the output
320
- * @returns {string}
321
- */
322
- DatePicker.prototype.leadingZeros = function (value, length = 2) {
323
- let ret = value.toString();
324
-
325
- while (ret.length < length) {
326
- ret = `0${ret}`;
220
+ </div>`;
221
+ }
222
+ createCalendarHeaders() {
223
+ this.dayLabels.forEach(day => {
224
+ const html = `<th scope="col"><span aria-hidden="true">${day.substring(0, 3)}</span><span class="govuk-visually-hidden">${day}</span></th>`;
225
+ const $headerRow = this.$dialog.querySelector('thead > tr');
226
+ $headerRow.insertAdjacentHTML('beforeend', html);
227
+ });
327
228
  }
328
229
 
329
- return ret
330
- };
331
-
332
- DatePicker.prototype.setOptions = function () {
333
- this.setMinAndMaxDatesOnCalendar();
334
- this.setExcludedDates();
335
- this.setExcludedDays();
336
- this.setLeadingZeros();
337
- this.setWeekStartDay();
338
- };
339
-
340
- DatePicker.prototype.setMinAndMaxDatesOnCalendar = function () {
341
- if (this.config.minDate) {
342
- this.minDate = this.formattedDateFromString(this.config.minDate, null);
343
- if (this.minDate && this.currentDate < this.minDate) {
344
- this.currentDate = this.minDate;
230
+ /**
231
+ * Pads given number with leading zeros
232
+ *
233
+ * @param {number} value - The value to be padded
234
+ * @param {number} length - The length in characters of the output
235
+ * @returns {string}
236
+ */
237
+ leadingZeros(value, length = 2) {
238
+ let ret = value.toString();
239
+ while (ret.length < length) {
240
+ ret = `0${ret}`;
345
241
  }
242
+ return ret;
346
243
  }
347
-
348
- if (this.config.maxDate) {
349
- this.maxDate = this.formattedDateFromString(this.config.maxDate, null);
350
- if (this.maxDate && this.currentDate > this.maxDate) {
351
- this.currentDate = this.maxDate;
244
+ setOptions() {
245
+ this.setMinAndMaxDatesOnCalendar();
246
+ this.setExcludedDates();
247
+ this.setExcludedDays();
248
+ this.setLeadingZeros();
249
+ this.setWeekStartDay();
250
+ }
251
+ setMinAndMaxDatesOnCalendar() {
252
+ if (this.config.minDate) {
253
+ this.minDate = this.formattedDateFromString(this.config.minDate, null);
254
+ if (this.minDate && this.currentDate < this.minDate) {
255
+ this.currentDate = this.minDate;
256
+ }
257
+ }
258
+ if (this.config.maxDate) {
259
+ this.maxDate = this.formattedDateFromString(this.config.maxDate, null);
260
+ if (this.maxDate && this.currentDate > this.maxDate) {
261
+ this.currentDate = this.maxDate;
262
+ }
352
263
  }
353
264
  }
354
- };
355
-
356
- DatePicker.prototype.setExcludedDates = function () {
357
- if (this.config.excludedDates) {
358
- this.excludedDates = this.config.excludedDates
359
- .replace(/\s+/, ' ')
360
- .split(' ')
361
- .map((item) => {
362
- return item.includes('-')
363
- ? this.parseDateRangeString(item)
364
- : this.formattedDateFromString(item)
365
- })
366
- .flat()
367
- .filter((item) => item);
265
+ setExcludedDates() {
266
+ if (this.config.excludedDates) {
267
+ this.excludedDates = this.config.excludedDates.replace(/\s+/, ' ').split(' ').map(item => {
268
+ return item.includes('-') ? this.parseDateRangeString(item) : [this.formattedDateFromString(item)];
269
+ }).reduce((dates, items) => dates.concat(items)).filter(date => date);
270
+ }
368
271
  }
369
- };
370
272
 
371
- /*
372
- * Parses a daterange string into an array of dates
373
- * @param {String} datestring - A daterange string in the format "dd/mm/yyyy-dd/mm/yyyy"
374
- * @returns {Date[]}
375
- */
376
- DatePicker.prototype.parseDateRangeString = function (datestring) {
377
- const dates = [];
378
- const [startDate, endDate] = datestring
379
- .split('-')
380
- .map((d) => this.formattedDateFromString(d, null));
381
-
382
- if (startDate && endDate) {
383
- const date = new Date(startDate.getTime());
384
- /* eslint-disable no-unmodified-loop-condition */
385
- while (date <= endDate) {
386
- dates.push(new Date(date));
387
- date.setDate(date.getDate() + 1);
388
- }
389
- /* eslint-enable no-unmodified-loop-condition */
390
- }
391
- return dates
392
- };
393
-
394
- DatePicker.prototype.setExcludedDays = function () {
395
- if (this.config.excludedDays) {
396
- // lowercase and arrange dayLabels to put indexOf sunday == 0 for comparison
397
- // with getDay() function
398
- const weekDays = this.dayLabels.map((item) => item.toLowerCase());
399
- if (this.config.weekStartDay === 'monday') {
400
- weekDays.unshift(weekDays.pop());
273
+ /*
274
+ * Parses a daterange string into an array of dates
275
+ * @param {String} datestring - A daterange string in the format "dd/mm/yyyy-dd/mm/yyyy"
276
+ * @returns {Date[]}
277
+ */
278
+ parseDateRangeString(datestring) {
279
+ const dates = [];
280
+ const [startDate, endDate] = datestring.split('-').map(d => this.formattedDateFromString(d, null));
281
+ if (startDate && endDate) {
282
+ const date = new Date(startDate.getTime());
283
+ /* eslint-disable no-unmodified-loop-condition */
284
+ while (date <= endDate) {
285
+ dates.push(new Date(date));
286
+ date.setDate(date.getDate() + 1);
287
+ }
288
+ /* eslint-enable no-unmodified-loop-condition */
289
+ }
290
+ return dates;
291
+ }
292
+ setExcludedDays() {
293
+ if (this.config.excludedDays) {
294
+ // lowercase and arrange dayLabels to put indexOf sunday == 0 for comparison
295
+ // with getDay() function
296
+ const weekDays = this.dayLabels.map(item => item.toLowerCase());
297
+ if (this.config.weekStartDay === 'monday') {
298
+ weekDays.unshift(weekDays.pop());
299
+ }
300
+ this.excludedDays = this.config.excludedDays.replace(/\s+/, ' ').toLowerCase().split(' ').map(item => weekDays.indexOf(item)).filter(item => item !== -1);
401
301
  }
402
-
403
- this.excludedDays = this.config.excludedDays
404
- .replace(/\s+/, ' ')
405
- .toLowerCase()
406
- .split(' ')
407
- .map((item) => weekDays.indexOf(item))
408
- .filter((item) => item !== -1);
409
302
  }
410
- };
411
-
412
- DatePicker.prototype.setLeadingZeros = function () {
413
- if (typeof this.config.leadingZeros !== 'boolean') {
414
- if (this.config.leadingZeros.toLowerCase() === 'true') {
415
- this.config.leadingZeros = true;
416
- return
303
+ setLeadingZeros() {
304
+ if (typeof this.config.leadingZeros !== 'boolean') {
305
+ if (this.config.leadingZeros.toLowerCase() === 'true') {
306
+ this.config.leadingZeros = true;
307
+ return;
308
+ }
309
+ if (this.config.leadingZeros.toLowerCase() === 'false') {
310
+ this.config.leadingZeros = false;
311
+ }
417
312
  }
418
- if (this.config.leadingZeros.toLowerCase() === 'false') {
419
- this.config.leadingZeros = false;
313
+ }
314
+ setWeekStartDay() {
315
+ const weekStartDayParam = this.config.weekStartDay;
316
+ if (weekStartDayParam && weekStartDayParam.toLowerCase() === 'sunday') {
317
+ this.config.weekStartDay = 'sunday';
318
+ // Rotate dayLabels array to put Sunday as the first item
319
+ this.dayLabels.unshift(this.dayLabels.pop());
320
+ } else {
321
+ this.config.weekStartDay = 'monday';
420
322
  }
421
323
  }
422
- };
423
324
 
424
- DatePicker.prototype.setWeekStartDay = function () {
425
- const weekStartDayParam = this.config.weekStartDay;
426
- if (weekStartDayParam && weekStartDayParam.toLowerCase() === 'sunday') {
427
- this.config.weekStartDay = 'sunday';
428
- // Rotate dayLabels array to put Sunday as the first item
429
- this.dayLabels.unshift(this.dayLabels.pop());
430
- } else {
431
- this.config.weekStartDay = 'monday';
432
- }
433
- };
325
+ /**
326
+ * Determine if a date is selecteable
327
+ *
328
+ * @param {Date} date - the date to check
329
+ * @returns {boolean}
330
+ */
331
+ isExcludedDate(date) {
332
+ // This comparison does not work correctly - it will exclude the mindate itself
333
+ // see: https://github.com/ministryofjustice/moj-frontend/issues/923
334
+ if (this.minDate && this.minDate > date) {
335
+ return true;
336
+ }
434
337
 
435
- /**
436
- * Determine if a date is selecteable
437
- *
438
- * @param {Date} date - the date to check
439
- * @returns {boolean}
440
- */
441
- DatePicker.prototype.isExcludedDate = function (date) {
442
- // This comparison does not work correctly - it will exclude the mindate itself
443
- // see: https://github.com/ministryofjustice/moj-frontend/issues/923
444
- if (this.minDate && this.minDate > date) {
445
- return true
338
+ // This comparison works as expected - the maxdate will not be excluded
339
+ if (this.maxDate && this.maxDate < date) {
340
+ return true;
341
+ }
342
+ for (const excludedDate of this.excludedDates) {
343
+ if (date.toDateString() === excludedDate.toDateString()) {
344
+ return true;
345
+ }
346
+ }
347
+ if (this.excludedDays.includes(date.getDay())) {
348
+ return true;
349
+ }
350
+ return false;
351
+ }
352
+
353
+ /**
354
+ * Get a Date object from a string
355
+ *
356
+ * @param {string} dateString - string in the format d/m/yyyy dd/mm/yyyy
357
+ * @param {Date} fallback - date object to return if formatting fails
358
+ * @returns {Date}
359
+ */
360
+ formattedDateFromString(dateString, fallback = new Date()) {
361
+ let formattedDate = null;
362
+ // Accepts d/m/yyyy and dd/mm/yyyy
363
+ const dateFormatPattern = /(\d{1,2})([-/,. ])(\d{1,2})\2(\d{4})/;
364
+ if (!dateFormatPattern.test(dateString)) return fallback;
365
+ const match = dateFormatPattern.exec(dateString);
366
+ const day = match[1];
367
+ const month = match[3];
368
+ const year = match[4];
369
+ formattedDate = new Date(`${year}-${month}-${day}`);
370
+ if (formattedDate instanceof Date && Number.isFinite(formattedDate.getTime())) {
371
+ return formattedDate;
372
+ }
373
+ return fallback;
374
+ }
375
+
376
+ /**
377
+ * Get a formatted date string from a Date object
378
+ *
379
+ * @param {Date} date - date to format to a string
380
+ * @returns {string}
381
+ */
382
+ formattedDateFromDate(date) {
383
+ if (this.config.leadingZeros) {
384
+ return `${this.leadingZeros(date.getDate())}/${this.leadingZeros(date.getMonth() + 1)}/${date.getFullYear()}`;
385
+ }
386
+ return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`;
446
387
  }
447
388
 
448
- // This comparison works as expected - the maxdate will not be excluded
449
- if (this.maxDate && this.maxDate < date) {
450
- return true
389
+ /**
390
+ * Get a human readable date in the format Monday 2 March 2024
391
+ *
392
+ * @param {Date} date - date to format
393
+ * @returns {string}
394
+ */
395
+ formattedDateHuman(date) {
396
+ return `${this.dayLabels[(date.getDay() + 6) % 7]} ${date.getDate()} ${this.monthLabels[date.getMonth()]} ${date.getFullYear()}`;
451
397
  }
452
-
453
- for (const excludedDate of this.excludedDates) {
454
- if (date.toDateString() === excludedDate.toDateString()) {
455
- return true
398
+ backgroundClick(event) {
399
+ if (this.isOpen() && event.target instanceof Node && !this.$dialog.contains(event.target) && !this.$input.contains(event.target) && !this.$calendarButton.contains(event.target)) {
400
+ event.preventDefault();
401
+ this.closeDialog();
456
402
  }
457
403
  }
458
-
459
- if (this.excludedDays.includes(date.getDay())) {
460
- return true
404
+ firstButtonKeydown(event) {
405
+ if (event.key === 'Tab' && event.shiftKey) {
406
+ this.$lastButtonInDialog.focus();
407
+ event.preventDefault();
408
+ }
461
409
  }
462
-
463
- return false
464
- };
465
-
466
- /**
467
- * Get a Date object from a string
468
- *
469
- * @param {string} dateString - string in the format d/m/yyyy dd/mm/yyyy
470
- * @param {Date} fallback - date object to return if formatting fails
471
- * @returns {Date}
472
- */
473
- DatePicker.prototype.formattedDateFromString = function (
474
- dateString,
475
- fallback = new Date()
476
- ) {
477
- let formattedDate = null;
478
- // Accepts d/m/yyyy and dd/mm/yyyy
479
- const dateFormatPattern = /(\d{1,2})([-/,. ])(\d{1,2})\2(\d{4})/;
480
-
481
- if (!dateFormatPattern.test(dateString)) return fallback
482
-
483
- const match = dateString.match(dateFormatPattern);
484
- const day = match[1];
485
- const month = match[3];
486
- const year = match[4];
487
-
488
- formattedDate = new Date(`${year}-${month}-${day}`);
489
- if (formattedDate instanceof Date && !isNaN(formattedDate)) {
490
- return formattedDate
410
+ lastButtonKeydown(event) {
411
+ if (event.key === 'Tab' && !event.shiftKey) {
412
+ this.$firstButtonInDialog.focus();
413
+ event.preventDefault();
414
+ }
491
415
  }
492
- return fallback
493
- };
494
416
 
495
- /**
496
- * Get a formatted date string from a Date object
497
- *
498
- * @param {Date} date - date to format to a string
499
- * @returns {string}
500
- */
501
- DatePicker.prototype.formattedDateFromDate = function (date) {
502
- if (this.config.leadingZeros) {
503
- return `${this.leadingZeros(date.getDate())}/${this.leadingZeros(date.getMonth() + 1)}/${date.getFullYear()}`
417
+ // render calendar
418
+ updateCalendar() {
419
+ this.$dialogTitle.innerHTML = `${this.monthLabels[this.currentDate.getMonth()]} ${this.currentDate.getFullYear()}`;
420
+ const day = this.currentDate;
421
+ const firstOfMonth = new Date(day.getFullYear(), day.getMonth(), 1);
422
+ let dayOfWeek;
423
+ if (this.config.weekStartDay === 'monday') {
424
+ dayOfWeek = firstOfMonth.getDay() === 0 ? 6 : firstOfMonth.getDay() - 1; // Change logic to make Monday first day of week, i.e. 0
425
+ } else {
426
+ dayOfWeek = firstOfMonth.getDay();
427
+ }
428
+ firstOfMonth.setDate(firstOfMonth.getDate() - dayOfWeek);
429
+ const thisDay = new Date(firstOfMonth);
430
+
431
+ // loop through our days
432
+ for (const calendarDay of this.calendarDays) {
433
+ const hidden = thisDay.getMonth() !== day.getMonth();
434
+ const disabled = this.isExcludedDate(thisDay);
435
+ calendarDay.update(thisDay, hidden, disabled);
436
+ thisDay.setDate(thisDay.getDate() + 1);
437
+ }
504
438
  }
439
+ setCurrentDate(focus = true) {
440
+ const {
441
+ currentDate
442
+ } = this;
443
+ this.calendarDays.forEach(calendarDay => {
444
+ calendarDay.button.classList.add('moj-datepicker__button');
445
+ calendarDay.button.classList.add('moj-datepicker__calendar-day');
446
+ calendarDay.button.setAttribute('tabindex', '-1');
447
+ calendarDay.button.classList.remove(this.selectedDayButtonClass);
448
+ const calendarDayDate = calendarDay.date;
449
+ calendarDayDate.setHours(0, 0, 0, 0);
450
+ const today = new Date();
451
+ today.setHours(0, 0, 0, 0);
452
+ if (calendarDayDate.getTime() === currentDate.getTime() /* && !calendarDay.button.disabled */) {
453
+ if (focus) {
454
+ calendarDay.button.setAttribute('tabindex', '0');
455
+ calendarDay.button.focus();
456
+ calendarDay.button.classList.add(this.selectedDayButtonClass);
457
+ }
458
+ }
459
+ if (this.inputDate && calendarDayDate.getTime() === this.inputDate.getTime()) {
460
+ calendarDay.button.classList.add(this.currentDayButtonClass);
461
+ calendarDay.button.setAttribute('aria-current', 'date');
462
+ } else {
463
+ calendarDay.button.classList.remove(this.currentDayButtonClass);
464
+ calendarDay.button.removeAttribute('aria-current');
465
+ }
466
+ if (calendarDayDate.getTime() === today.getTime()) {
467
+ calendarDay.button.classList.add(this.todayButtonClass);
468
+ } else {
469
+ calendarDay.button.classList.remove(this.todayButtonClass);
470
+ }
471
+ });
505
472
 
506
- return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`
507
- };
508
-
509
- /**
510
- * Get a human readable date in the format Monday 2 March 2024
511
- *
512
- * @param {Date} date - date to format
513
- * @returns {string}
514
- */
515
- DatePicker.prototype.formattedDateHuman = function (date) {
516
- return `${this.dayLabels[(date.getDay() + 6) % 7]} ${date.getDate()} ${this.monthLabels[date.getMonth()]} ${date.getFullYear()}`
517
- };
518
-
519
- DatePicker.prototype.backgroundClick = function (event) {
520
- if (
521
- this.isOpen() &&
522
- !this.$dialog.contains(event.target) &&
523
- !this.$input.contains(event.target) &&
524
- !this.$calendarButton.contains(event.target)
525
- ) {
526
- event.preventDefault();
473
+ // if no date is tab-able, make the first non-disabled date tab-able
474
+ if (!focus) {
475
+ const enabledDays = this.calendarDays.filter(calendarDay => {
476
+ return window.getComputedStyle(calendarDay.button).display === 'block' && !calendarDay.button.disabled;
477
+ });
478
+ enabledDays[0].button.setAttribute('tabindex', '0');
479
+ this.currentDate = enabledDays[0].date;
480
+ }
481
+ }
482
+ selectDate(date) {
483
+ if (this.isExcludedDate(date)) {
484
+ return;
485
+ }
486
+ this.$calendarButton.querySelector('span').innerText = `Choose date. Selected date is ${this.formattedDateHuman(date)}`;
487
+ this.$input.value = this.formattedDateFromDate(date);
488
+ const changeEvent = new Event('change', {
489
+ bubbles: true,
490
+ cancelable: true
491
+ });
492
+ this.$input.dispatchEvent(changeEvent);
527
493
  this.closeDialog();
528
494
  }
529
- };
530
-
531
- DatePicker.prototype.firstButtonKeydown = function (event) {
532
- if (event.key === 'Tab' && event.shiftKey) {
533
- this.$lastButtonInDialog.focus();
534
- event.preventDefault();
495
+ isOpen() {
496
+ return this.$dialog.classList.contains('moj-datepicker__dialog--open');
535
497
  }
536
- };
537
-
538
- DatePicker.prototype.lastButtonKeydown = function (event) {
539
- if (event.key === 'Tab' && !event.shiftKey) {
540
- this.$firstButtonInDialog.focus();
498
+ toggleDialog(event) {
541
499
  event.preventDefault();
500
+ if (this.isOpen()) {
501
+ this.closeDialog();
502
+ } else {
503
+ this.setMinAndMaxDatesOnCalendar();
504
+ this.openDialog();
505
+ }
542
506
  }
543
- };
507
+ openDialog() {
508
+ this.$dialog.hidden = false;
509
+ this.$dialog.classList.add('moj-datepicker__dialog--open');
510
+ this.$calendarButton.setAttribute('aria-expanded', 'true');
544
511
 
545
- // render calendar
546
- DatePicker.prototype.updateCalendar = function () {
547
- this.$dialogTitle.innerHTML = `${this.monthLabels[this.currentDate.getMonth()]} ${this.currentDate.getFullYear()}`;
548
-
549
- const day = this.currentDate;
550
- const firstOfMonth = new Date(day.getFullYear(), day.getMonth(), 1);
551
- let dayOfWeek;
512
+ // position the dialog
513
+ // if input is wider than dialog pin it to the right
514
+ if (this.$input.offsetWidth > this.$dialog.offsetWidth) {
515
+ this.$dialog.style.right = `0px`;
516
+ }
517
+ this.$dialog.style.top = `${this.$input.offsetHeight + 3}px`;
552
518
 
553
- if (this.config.weekStartDay === 'monday') {
554
- dayOfWeek = firstOfMonth.getDay() === 0 ? 6 : firstOfMonth.getDay() - 1; // Change logic to make Monday first day of week, i.e. 0
555
- } else {
556
- dayOfWeek = firstOfMonth.getDay();
519
+ // get the date from the input element
520
+ this.inputDate = this.formattedDateFromString(this.$input.value);
521
+ this.currentDate = this.inputDate;
522
+ this.currentDate.setHours(0, 0, 0, 0);
523
+ this.updateCalendar();
524
+ this.setCurrentDate();
525
+ }
526
+ closeDialog() {
527
+ this.$dialog.hidden = true;
528
+ this.$dialog.classList.remove('moj-datepicker__dialog--open');
529
+ this.$calendarButton.setAttribute('aria-expanded', 'false');
530
+ this.$calendarButton.focus();
531
+ }
532
+ goToDate(date, focus) {
533
+ const current = this.currentDate;
534
+ this.currentDate = date;
535
+ if (current.getMonth() !== this.currentDate.getMonth() || current.getFullYear() !== this.currentDate.getFullYear()) {
536
+ this.updateCalendar();
537
+ }
538
+ this.setCurrentDate(focus);
539
+ }
540
+
541
+ // day navigation
542
+ focusNextDay() {
543
+ const date = new Date(this.currentDate);
544
+ date.setDate(date.getDate() + 1);
545
+ this.goToDate(date);
546
+ }
547
+ focusPreviousDay() {
548
+ const date = new Date(this.currentDate);
549
+ date.setDate(date.getDate() - 1);
550
+ this.goToDate(date);
551
+ }
552
+
553
+ // week navigation
554
+ focusNextWeek() {
555
+ const date = new Date(this.currentDate);
556
+ date.setDate(date.getDate() + 7);
557
+ this.goToDate(date);
558
+ }
559
+ focusPreviousWeek() {
560
+ const date = new Date(this.currentDate);
561
+ date.setDate(date.getDate() - 7);
562
+ this.goToDate(date);
563
+ }
564
+ focusFirstDayOfWeek() {
565
+ const date = new Date(this.currentDate);
566
+ const firstDayOfWeekIndex = this.config.weekStartDay === 'sunday' ? 0 : 1;
567
+ const dayOfWeek = date.getDay();
568
+ const diff = dayOfWeek >= firstDayOfWeekIndex ? dayOfWeek - firstDayOfWeekIndex : 6 - dayOfWeek;
569
+ date.setDate(date.getDate() - diff);
570
+ date.setHours(0, 0, 0, 0);
571
+ this.goToDate(date);
572
+ }
573
+ focusLastDayOfWeek() {
574
+ const date = new Date(this.currentDate);
575
+ const lastDayOfWeekIndex = this.config.weekStartDay === 'sunday' ? 6 : 0;
576
+ const dayOfWeek = date.getDay();
577
+ const diff = dayOfWeek <= lastDayOfWeekIndex ? lastDayOfWeekIndex - dayOfWeek : 7 - dayOfWeek;
578
+ date.setDate(date.getDate() + diff);
579
+ date.setHours(0, 0, 0, 0);
580
+ this.goToDate(date);
581
+ }
582
+
583
+ // month navigation
584
+ focusNextMonth(event, focus = true) {
585
+ event.preventDefault();
586
+ const date = new Date(this.currentDate);
587
+ date.setMonth(date.getMonth() + 1, 1);
588
+ this.goToDate(date, focus);
557
589
  }
558
-
559
- firstOfMonth.setDate(firstOfMonth.getDate() - dayOfWeek);
560
-
561
- const thisDay = new Date(firstOfMonth);
562
-
563
- // loop through our days
564
- for (let i = 0; i < this.calendarDays.length; i++) {
565
- const hidden = thisDay.getMonth() !== day.getMonth();
566
- const disabled = this.isExcludedDate(thisDay);
567
-
568
- this.calendarDays[i].update(thisDay, hidden, disabled);
569
-
570
- thisDay.setDate(thisDay.getDate() + 1);
590
+ focusPreviousMonth(event, focus = true) {
591
+ event.preventDefault();
592
+ const date = new Date(this.currentDate);
593
+ date.setMonth(date.getMonth() - 1, 1);
594
+ this.goToDate(date, focus);
571
595
  }
572
- };
573
-
574
- DatePicker.prototype.setCurrentDate = function (focus = true) {
575
- const { currentDate } = this;
576
- this.calendarDays.forEach((calendarDay) => {
577
- calendarDay.button.classList.add('moj-datepicker__button');
578
- calendarDay.button.classList.add('moj-datepicker__calendar-day');
579
- calendarDay.button.setAttribute('tabindex', -1);
580
- calendarDay.button.classList.remove(this.selectedDayButtonClass);
581
- const calendarDayDate = calendarDay.date;
582
- calendarDayDate.setHours(0, 0, 0, 0);
583
-
584
- const today = new Date();
585
- today.setHours(0, 0, 0, 0);
586
596
 
587
- if (
588
- calendarDayDate.getTime() ===
589
- currentDate.getTime() /* && !calendarDay.button.disabled */
590
- ) {
591
- if (focus) {
592
- calendarDay.button.setAttribute('tabindex', 0);
593
- calendarDay.button.focus();
594
- calendarDay.button.classList.add(this.selectedDayButtonClass);
597
+ // year navigation
598
+ focusNextYear(event, focus = true) {
599
+ event.preventDefault();
600
+ const date = new Date(this.currentDate);
601
+ date.setFullYear(date.getFullYear() + 1, date.getMonth(), 1);
602
+ this.goToDate(date, focus);
603
+ }
604
+ focusPreviousYear(event, focus = true) {
605
+ event.preventDefault();
606
+ const date = new Date(this.currentDate);
607
+ date.setFullYear(date.getFullYear() - 1, date.getMonth(), 1);
608
+ this.goToDate(date, focus);
609
+ }
610
+
611
+ /**
612
+ * Parse dataset
613
+ *
614
+ * @param {Schema} schema - Component class
615
+ * @param {DOMStringMap} dataset - HTML element dataset
616
+ * @returns {object} Normalised dataset
617
+ */
618
+ parseDataset(schema, dataset) {
619
+ const parsed = {};
620
+ for (const [field,,] of Object.entries(schema.properties)) {
621
+ if (field in dataset) {
622
+ parsed[field] = dataset[field];
595
623
  }
596
624
  }
597
-
598
- if (
599
- this.inputDate &&
600
- calendarDayDate.getTime() === this.inputDate.getTime()
601
- ) {
602
- calendarDay.button.classList.add(this.currentDayButtonClass);
603
- calendarDay.button.setAttribute('aria-current', 'date');
625
+ return parsed;
626
+ }
627
+
628
+ /**
629
+ * Config merging function
630
+ *
631
+ * Takes any number of objects and combines them together, with
632
+ * greatest priority on the LAST item passed in.
633
+ *
634
+ * @param {...{ [key: string]: unknown }} configObjects - Config objects to merge
635
+ * @returns {{ [key: string]: unknown }} A merged config object
636
+ */
637
+ mergeConfigs(...configObjects) {
638
+ const formattedConfigObject = {};
639
+
640
+ // Loop through each of the passed objects
641
+ for (const configObject of configObjects) {
642
+ for (const key of Object.keys(configObject)) {
643
+ const option = formattedConfigObject[key];
644
+ const override = configObject[key];
645
+
646
+ // Push their keys one-by-one into formattedConfigObject. Any duplicate
647
+ // keys with object values will be merged, otherwise the new value will
648
+ // override the existing value.
649
+ if (typeof option === 'object' && typeof override === 'object') {
650
+ // @ts-expect-error Index signature for type 'string' is missing
651
+ formattedConfigObject[key] = this.mergeConfigs(option, override);
652
+ } else {
653
+ formattedConfigObject[key] = override;
654
+ }
655
+ }
656
+ }
657
+ return formattedConfigObject;
658
+ }
659
+ }
660
+ class DSCalendarDay {
661
+ /**
662
+ *
663
+ * @param {HTMLElement} button
664
+ * @param {number} index
665
+ * @param {number} row
666
+ * @param {number} column
667
+ * @param {DatePicker} picker
668
+ */
669
+ constructor(button, index, row, column, picker) {
670
+ this.index = index;
671
+ this.row = row;
672
+ this.column = column;
673
+ this.button = button;
674
+ this.picker = picker;
675
+ this.date = new Date();
676
+ this.button.addEventListener('keydown', this.keyPress.bind(this));
677
+ this.button.addEventListener('click', this.click.bind(this));
678
+ }
679
+
680
+ /**
681
+ * @param {Date} day - the Date for the calendar day
682
+ * @param {boolean} hidden - visibility of the day
683
+ * @param {boolean} disabled - is the day selectable or excluded
684
+ */
685
+ update(day, hidden, disabled) {
686
+ const label = day.getDate();
687
+ let accessibleLabel = this.picker.formattedDateHuman(day);
688
+ if (disabled) {
689
+ this.button.setAttribute('aria-disabled', 'true');
690
+ accessibleLabel = `Excluded date, ${accessibleLabel}`;
604
691
  } else {
605
- calendarDay.button.classList.remove(this.currentDayButtonClass);
606
- calendarDay.button.removeAttribute('aria-current');
692
+ this.button.removeAttribute('aria-disabled');
607
693
  }
608
-
609
- if (calendarDayDate.getTime() === today.getTime()) {
610
- calendarDay.button.classList.add(this.todayButtonClass);
694
+ if (hidden) {
695
+ this.button.style.display = 'none';
611
696
  } else {
612
- calendarDay.button.classList.remove(this.todayButtonClass);
697
+ this.button.style.display = 'block';
613
698
  }
614
- });
615
-
616
- // if no date is tab-able, make the first non-disabled date tab-able
617
- if (!focus) {
618
- const enabledDays = this.calendarDays.filter((calendarDay) => {
619
- return (
620
- window.getComputedStyle(calendarDay.button).display === 'block' &&
621
- !calendarDay.button.disabled
622
- )
623
- });
624
-
625
- enabledDays[0].button.setAttribute('tabindex', 0);
626
-
627
- this.currentDate = enabledDays[0].date;
628
- }
629
- };
630
-
631
- DatePicker.prototype.selectDate = function (date) {
632
- if (this.isExcludedDate(date)) {
633
- return
699
+ this.button.setAttribute('data-testid', this.picker.formattedDateFromDate(day));
700
+ this.button.innerHTML = `<span class="govuk-visually-hidden">${accessibleLabel}</span><span aria-hidden="true">${label}</span>`;
701
+ this.date = new Date(day);
634
702
  }
635
-
636
- this.$calendarButton.querySelector('span').innerText =
637
- `Choose date. Selected date is ${this.formattedDateHuman(date)}`;
638
- this.$input.value = this.formattedDateFromDate(date);
639
-
640
- const changeEvent = new Event('change', { bubbles: true, cancelable: true });
641
- this.$input.dispatchEvent(changeEvent);
642
-
643
- this.closeDialog();
644
- };
645
-
646
- DatePicker.prototype.isOpen = function () {
647
- return this.$dialog.classList.contains('moj-datepicker__dialog--open')
648
- };
649
-
650
- DatePicker.prototype.toggleDialog = function (event) {
651
- event.preventDefault();
652
- if (this.isOpen()) {
653
- this.closeDialog();
654
- } else {
655
- this.setMinAndMaxDatesOnCalendar();
656
- this.openDialog();
657
- }
658
- };
659
-
660
- DatePicker.prototype.openDialog = function () {
661
- this.$dialog.hidden = false;
662
- this.$dialog.classList.add('moj-datepicker__dialog--open');
663
- this.$calendarButton.setAttribute('aria-expanded', 'true');
664
-
665
- // position the dialog
666
- // if input is wider than dialog pin it to the right
667
- if (this.$input.offsetWidth > this.$dialog.offsetWidth) {
668
- this.$dialog.style.right = `0px`;
669
- }
670
- this.$dialog.style.top = `${this.$input.offsetHeight + 3}px`;
671
-
672
- // get the date from the input element
673
- this.inputDate = this.formattedDateFromString(this.$input.value);
674
- this.currentDate = this.inputDate;
675
- this.currentDate.setHours(0, 0, 0, 0);
676
-
677
- this.updateCalendar();
678
- this.setCurrentDate();
679
- };
680
-
681
- DatePicker.prototype.closeDialog = function () {
682
- this.$dialog.hidden = true;
683
- this.$dialog.classList.remove('moj-datepicker__dialog--open');
684
- this.$calendarButton.setAttribute('aria-expanded', 'false');
685
- this.$calendarButton.focus();
686
- };
687
-
688
- DatePicker.prototype.goToDate = function (date, focus) {
689
- const current = this.currentDate;
690
- this.currentDate = date;
691
-
692
- if (
693
- current.getMonth() !== this.currentDate.getMonth() ||
694
- current.getFullYear() !== this.currentDate.getFullYear()
695
- ) {
696
- this.updateCalendar();
703
+ click(event) {
704
+ this.picker.goToDate(this.date);
705
+ this.picker.selectDate(this.date);
706
+ event.stopPropagation();
707
+ event.preventDefault();
697
708
  }
698
-
699
- this.setCurrentDate(focus);
700
- };
701
-
702
- // day navigation
703
- DatePicker.prototype.focusNextDay = function () {
704
- const date = new Date(this.currentDate);
705
- date.setDate(date.getDate() + 1);
706
- this.goToDate(date);
707
- };
708
-
709
- DatePicker.prototype.focusPreviousDay = function () {
710
- const date = new Date(this.currentDate);
711
- date.setDate(date.getDate() - 1);
712
- this.goToDate(date);
713
- };
714
-
715
- // week navigation
716
- DatePicker.prototype.focusNextWeek = function () {
717
- const date = new Date(this.currentDate);
718
- date.setDate(date.getDate() + 7);
719
- this.goToDate(date);
720
- };
721
-
722
- DatePicker.prototype.focusPreviousWeek = function () {
723
- const date = new Date(this.currentDate);
724
- date.setDate(date.getDate() - 7);
725
- this.goToDate(date);
726
- };
727
-
728
- DatePicker.prototype.focusFirstDayOfWeek = function () {
729
- const date = new Date(this.currentDate);
730
- const firstDayOfWeekIndex = this.config.weekStartDay === 'sunday' ? 0 : 1;
731
- const dayOfWeek = date.getDay();
732
- const diff =
733
- dayOfWeek >= firstDayOfWeekIndex
734
- ? dayOfWeek - firstDayOfWeekIndex
735
- : 6 - dayOfWeek;
736
-
737
- date.setDate(date.getDate() - diff);
738
- date.setHours(0, 0, 0, 0);
739
-
740
- this.goToDate(date);
741
- };
742
-
743
- DatePicker.prototype.focusLastDayOfWeek = function () {
744
- const date = new Date(this.currentDate);
745
- const lastDayOfWeekIndex = this.config.weekStartDay === 'sunday' ? 6 : 0;
746
- const dayOfWeek = date.getDay();
747
- const diff =
748
- dayOfWeek <= lastDayOfWeekIndex
749
- ? lastDayOfWeekIndex - dayOfWeek
750
- : 7 - dayOfWeek;
751
-
752
- date.setDate(date.getDate() + diff);
753
- date.setHours(0, 0, 0, 0);
754
-
755
- this.goToDate(date);
756
- };
757
-
758
- // month navigation
759
- DatePicker.prototype.focusNextMonth = function (event, focus = true) {
760
- event.preventDefault();
761
- const date = new Date(this.currentDate);
762
- date.setMonth(date.getMonth() + 1, 1);
763
- this.goToDate(date, focus);
764
- };
765
-
766
- DatePicker.prototype.focusPreviousMonth = function (event, focus = true) {
767
- event.preventDefault();
768
- const date = new Date(this.currentDate);
769
- date.setMonth(date.getMonth() - 1, 1);
770
- this.goToDate(date, focus);
771
- };
772
-
773
- // year navigation
774
- DatePicker.prototype.focusNextYear = function (event, focus = true) {
775
- event.preventDefault();
776
- const date = new Date(this.currentDate);
777
- date.setFullYear(date.getFullYear() + 1, date.getMonth(), 1);
778
- this.goToDate(date, focus);
779
- };
780
-
781
- DatePicker.prototype.focusPreviousYear = function (event, focus = true) {
782
- event.preventDefault();
783
- const date = new Date(this.currentDate);
784
- date.setFullYear(date.getFullYear() - 1, date.getMonth(), 1);
785
- this.goToDate(date, focus);
786
- };
787
-
788
- /**
789
- * Parse dataset
790
- *
791
- * @param {Schema} schema - Component class
792
- * @param {DOMStringMap} dataset - HTML element dataset
793
- * @returns {object} Normalised dataset
794
- */
795
- DatePicker.prototype.parseDataset = function (schema, dataset) {
796
- const parsed = {};
797
-
798
- for (const [field, ,] of Object.entries(schema.properties)) {
799
- if (field in dataset) {
800
- parsed[field] = dataset[field];
709
+ keyPress(event) {
710
+ let calendarNavKey = true;
711
+ switch (event.key) {
712
+ case 'ArrowLeft':
713
+ this.picker.focusPreviousDay();
714
+ break;
715
+ case 'ArrowRight':
716
+ this.picker.focusNextDay();
717
+ break;
718
+ case 'ArrowUp':
719
+ this.picker.focusPreviousWeek();
720
+ break;
721
+ case 'ArrowDown':
722
+ this.picker.focusNextWeek();
723
+ break;
724
+ case 'Home':
725
+ this.picker.focusFirstDayOfWeek();
726
+ break;
727
+ case 'End':
728
+ this.picker.focusLastDayOfWeek();
729
+ break;
730
+ case 'PageUp':
731
+ {
732
+ if (event.shiftKey) {
733
+ this.picker.focusPreviousYear(event);
734
+ } else {
735
+ this.picker.focusPreviousMonth(event);
736
+ }
737
+ break;
738
+ }
739
+ case 'PageDown':
740
+ {
741
+ if (event.shiftKey) {
742
+ this.picker.focusNextYear(event);
743
+ } else {
744
+ this.picker.focusNextMonth(event);
745
+ }
746
+ break;
747
+ }
748
+ default:
749
+ calendarNavKey = false;
750
+ break;
801
751
  }
802
- }
803
-
804
- return parsed
805
- };
806
-
807
- /**
808
- * Config merging function
809
- *
810
- * Takes any number of objects and combines them together, with
811
- * greatest priority on the LAST item passed in.
812
- *
813
- * @param {...{ [key: string]: unknown }} configObjects - Config objects to merge
814
- * @returns {{ [key: string]: unknown }} A merged config object
815
- */
816
- DatePicker.prototype.mergeConfigs = function (...configObjects) {
817
- const formattedConfigObject = {};
818
-
819
- // Loop through each of the passed objects
820
- for (const configObject of configObjects) {
821
- for (const key of Object.keys(configObject)) {
822
- const option = formattedConfigObject[key];
823
- const override = configObject[key];
824
-
825
- // Push their keys one-by-one into formattedConfigObject. Any duplicate
826
- // keys with object values will be merged, otherwise the new value will
827
- // override the existing value.
828
- if (typeof option === 'object' && typeof override === 'object') {
829
- // @ts-expect-error Index signature for type 'string' is missing
830
- formattedConfigObject[key] = this.mergeConfigs(option, override);
831
- } else {
832
- formattedConfigObject[key] = override;
833
- }
752
+ if (calendarNavKey) {
753
+ event.preventDefault();
754
+ event.stopPropagation();
834
755
  }
835
756
  }
836
-
837
- return formattedConfigObject
838
- };
839
-
840
- /**
841
- *
842
- * @param {HTMLElement} button
843
- * @param {number} index
844
- * @param {number} row
845
- * @param {number} column
846
- * @param {DatePicker} picker
847
- * @class
848
- */
849
- function DSCalendarDay(button, index, row, column, picker) {
850
- this.index = index;
851
- this.row = row;
852
- this.column = column;
853
- this.button = button;
854
- this.picker = picker;
855
-
856
- this.date = new Date();
857
757
  }
858
758
 
859
- DSCalendarDay.prototype.init = function () {
860
- this.button.addEventListener('keydown', this.keyPress.bind(this));
861
- this.button.addEventListener('click', this.click.bind(this));
862
- };
863
-
864
759
  /**
865
- * @param {Date} day - the Date for the calendar day
866
- * @param {boolean} hidden - visibility of the day
867
- * @param {boolean} disabled - is the day selectable or excluded
868
- */
869
- DSCalendarDay.prototype.update = function (day, hidden, disabled) {
870
- const label = day.getDate();
871
- let accessibleLabel = this.picker.formattedDateHuman(day);
872
-
873
- if (disabled) {
874
- this.button.setAttribute('aria-disabled', true);
875
- accessibleLabel = `Excluded date, ${accessibleLabel}`;
876
- } else {
877
- this.button.removeAttribute('aria-disabled');
878
- }
879
-
880
- if (hidden) {
881
- this.button.style.display = 'none';
882
- } else {
883
- this.button.style.display = 'block';
884
- }
885
- this.button.setAttribute(
886
- 'data-testid',
887
- this.picker.formattedDateFromDate(day)
888
- );
889
-
890
- this.button.innerHTML = `<span class="govuk-visually-hidden">${accessibleLabel}</span><span aria-hidden="true">${label}</span>`;
891
- this.date = new Date(day);
892
- };
893
-
894
- DSCalendarDay.prototype.click = function (event) {
895
- this.picker.goToDate(this.date);
896
- this.picker.selectDate(this.date);
897
-
898
- event.stopPropagation();
899
- event.preventDefault();
900
- };
901
-
902
- DSCalendarDay.prototype.keyPress = function (event) {
903
- let calendarNavKey = true;
904
-
905
- switch (event.key) {
906
- case 'ArrowLeft':
907
- this.picker.focusPreviousDay();
908
- break
909
- case 'ArrowRight':
910
- this.picker.focusNextDay();
911
- break
912
- case 'ArrowUp':
913
- this.picker.focusPreviousWeek();
914
- break
915
- case 'ArrowDown':
916
- this.picker.focusNextWeek();
917
- break
918
- case 'Home':
919
- this.picker.focusFirstDayOfWeek();
920
- break
921
- case 'End':
922
- this.picker.focusLastDayOfWeek();
923
- break
924
- case 'PageUp':
925
- // eslint-disable-next-line no-unused-expressions
926
- event.shiftKey
927
- ? this.picker.focusPreviousYear(event)
928
- : this.picker.focusPreviousMonth(event);
929
- break
930
- case 'PageDown':
931
- // eslint-disable-next-line no-unused-expressions
932
- event.shiftKey
933
- ? this.picker.focusNextYear(event)
934
- : this.picker.focusNextMonth(event);
935
- break
936
- default:
937
- calendarNavKey = false;
938
- break
939
- }
940
-
941
- if (calendarNavKey) {
942
- event.preventDefault();
943
- event.stopPropagation();
944
- }
945
- };
946
-
947
- /**
948
- * Schema for component config
760
+ * Date picker config
949
761
  *
950
- * @typedef {object} Schema
951
- * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
762
+ * @typedef {object} DatePickerConfig
763
+ * @property {string} [excludedDates] - Dates that cannot be selected
764
+ * @property {string} [excludedDays] - Days that cannot be selected
765
+ * @property {boolean} [leadingZeroes] - Whether to add leading zeroes when populating the field
766
+ * @property {string} [minDate] - The earliest available date
767
+ * @property {string} [maxDate] - The latest available date
768
+ * @property {string} [weekStartDay] - First day of the week in calendar view
952
769
  */
953
770
 
954
771
  /**
955
- * Schema property for component config
956
- *
957
- * @typedef {object} SchemaProperty
958
- * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
772
+ * @import { Schema } from '../../all.mjs'
959
773
  */
960
774
 
961
775
  export { DatePicker };