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