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