@ministryofjustice/frontend 4.0.1 → 5.1.0

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