@ministryofjustice/frontend 3.5.0 → 3.6.1

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 (37) hide show
  1. package/moj/all.jquery.min.js +1 -81
  2. package/moj/all.js +2577 -2853
  3. package/moj/all.mjs +126 -0
  4. package/moj/all.scss +1 -1
  5. package/moj/components/add-another/add-another.js +111 -132
  6. package/moj/components/add-another/add-another.mjs +106 -0
  7. package/moj/components/alert/alert.js +352 -479
  8. package/moj/components/alert/alert.mjs +251 -0
  9. package/moj/components/alert/alert.spec.helper.js +6 -24
  10. package/moj/components/alert/alert.spec.helper.mjs +66 -0
  11. package/moj/components/button-menu/button-menu.js +326 -343
  12. package/moj/components/button-menu/button-menu.mjs +329 -0
  13. package/moj/components/cookie-banner/_cookie-banner.scss +1 -1
  14. package/moj/components/date-picker/date-picker.js +905 -922
  15. package/moj/components/date-picker/date-picker.mjs +961 -0
  16. package/moj/components/filter-toggle-button/filter-toggle-button.js +98 -119
  17. package/moj/components/filter-toggle-button/filter-toggle-button.mjs +93 -0
  18. package/moj/components/form-validator/form-validator.js +201 -396
  19. package/moj/components/form-validator/form-validator.mjs +168 -0
  20. package/moj/components/multi-file-upload/multi-file-upload.js +227 -441
  21. package/moj/components/multi-file-upload/multi-file-upload.mjs +219 -0
  22. package/moj/components/multi-select/multi-select.js +82 -103
  23. package/moj/components/multi-select/multi-select.mjs +77 -0
  24. package/moj/components/password-reveal/password-reveal.js +40 -61
  25. package/moj/components/password-reveal/password-reveal.mjs +35 -0
  26. package/moj/components/rich-text-editor/rich-text-editor.js +162 -183
  27. package/moj/components/rich-text-editor/rich-text-editor.mjs +157 -0
  28. package/moj/components/search-toggle/search-toggle.js +52 -73
  29. package/moj/components/search-toggle/search-toggle.mjs +54 -0
  30. package/moj/components/sortable-table/sortable-table.js +143 -164
  31. package/moj/components/sortable-table/sortable-table.mjs +138 -0
  32. package/moj/helpers.js +196 -215
  33. package/moj/helpers.mjs +123 -0
  34. package/moj/moj-frontend.min.js +1 -81
  35. package/moj/version.js +6 -23
  36. package/moj/version.mjs +3 -0
  37. package/package.json +24 -6
@@ -1,252 +1,242 @@
1
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">
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
27
+ }
28
+
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
+ }
100
+
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
+ }
235
+ }
236
+ };
237
+
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">
250
240
  <span class="govuk-visually-hidden">Choose date</span>
251
241
  <svg width="32" height="24" focusable="false" class="moj-datepicker-icon" aria-hidden="true" role="img" viewBox="0 0 22 22">
252
242
  <path
@@ -259,16 +249,16 @@
259
249
  <rect x="16.8667" width="1.46667" height="5.13333" rx="0.733333" fill="currentColor"></rect>
260
250
  </svg>
261
251
  </button>`
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">
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">
272
262
  <div class="moj-datepicker__dialog-navbuttons">
273
263
  <button class="moj-datepicker__button moj-js-datepicker-prev-year">
274
264
  <span class="govuk-visually-hidden">Previous year</span>
@@ -318,669 +308,662 @@
318
308
  <button type="button" class="govuk-button moj-js-datepicker-ok">Select</button>
319
309
  <button type="button" class="govuk-button govuk-button--secondary moj-js-datepicker-cancel">Close</button>
320
310
  </div>`
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;
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
+ }
334
+
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
+ }
352
+ }
353
+
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
+ }
359
+ }
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);
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
+ }
408
+
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
+ };
417
+
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
+ }
427
+ }
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
452
+ }
453
+
454
+ // This comparison works as expected - the maxdate will not be excluded
455
+ if (this.maxDate && this.maxDate < date) {
456
+ return true
457
+ }
458
+
459
+ for (const excludedDate of this.excludedDates) {
460
+ if (date.toDateString() === excludedDate.toDateString()) {
461
+ return true
462
+ }
463
+ }
464
+
465
+ if (this.excludedDays.includes(date.getDay())) {
466
+ return true
467
+ }
468
+
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()
482
+ ) {
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})/;
486
+
487
+ if (!dateFormatPattern.test(dateString)) return fallback
488
+
489
+ const match = dateString.match(dateFormatPattern);
490
+ const day = match[1];
491
+ const month = match[3];
492
+ const year = match[4];
493
+
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
+ }
511
+
512
+ return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`
513
+ };
514
+
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
+ };
524
+
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
+ };
536
+
537
+ DatePicker.prototype.firstButtonKeydown = function (event) {
538
+ if (event.key === 'Tab' && event.shiftKey) {
539
+ this.$lastButtonInDialog.focus();
540
+ event.preventDefault();
541
+ }
542
+ };
543
+
544
+ DatePicker.prototype.lastButtonKeydown = function (event) {
545
+ if (event.key === 'Tab' && !event.shiftKey) {
546
+ this.$firstButtonInDialog.focus();
547
+ event.preventDefault();
548
+ }
549
+ };
550
+
551
+ // render calendar
552
+ DatePicker.prototype.updateCalendar = function () {
553
+ this.$dialogTitle.innerHTML = `${this.monthLabels[this.currentDate.getMonth()]} ${this.currentDate.getFullYear()}`;
554
+
555
+ const day = this.currentDate;
556
+ const firstOfMonth = new Date(day.getFullYear(), day.getMonth(), 1);
557
+ let dayOfWeek;
558
+
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
+ }
564
+
565
+ firstOfMonth.setDate(firstOfMonth.getDate() - dayOfWeek);
566
+
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);
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
+ }
603
+
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;
634
+ }
635
+ };
636
+
637
+ DatePicker.prototype.selectDate = function (date) {
638
+ if (this.isExcludedDate(date)) {
639
+ return
640
+ }
641
+
642
+ this.$calendarButton.querySelector('span').innerText =
643
+ `Choose date. Selected date is ${this.formattedDateHuman(date)}`;
644
+ this.$input.value = this.formattedDateFromDate(date);
645
+
646
+ const changeEvent = new Event('change', { bubbles: true, cancelable: true });
647
+ this.$input.dispatchEvent(changeEvent);
648
+
649
+ this.closeDialog();
650
+ };
651
+
652
+ DatePicker.prototype.isOpen = function () {
653
+ return this.$dialog.classList.contains('moj-datepicker__dialog--open')
654
+ };
655
+
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
+ };
665
+
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');
670
+
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`;
677
+
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);
682
+
683
+ this.updateCalendar();
684
+ this.setCurrentDate();
685
+ };
686
+
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
+ };
693
+
694
+ DatePicker.prototype.goToDate = function (date, focus) {
695
+ const current = this.currentDate;
696
+ this.currentDate = date;
697
+
698
+ if (
699
+ current.getMonth() !== this.currentDate.getMonth() ||
700
+ current.getFullYear() !== this.currentDate.getFullYear()
701
+ ) {
702
+ this.updateCalendar();
703
+ }
704
+
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];
807
+ }
808
+ }
809
+
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
+ }
842
+
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
+ }
885
+
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;
985
968
 
986
969
  }));