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